diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf8d78fc..27f2c796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - shard: [1/10, 2/10, 3/10, 4/10, 5/10, 6/10, 7/10, 8/10, 9/10, 10/10] + 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] steps: - uses: actions/checkout@v6 @@ -80,7 +80,24 @@ jobs: fail-fast: false matrix: 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] + [ + 1/16, + 2/16, + 3/16, + 4/16, + 5/16, + 6/16, + 7/16, + 8/16, + 9/16, + 10/16, + 11/16, + 12/16, + 13/16, + 14/16, + 15/16, + 16/16, + ] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 85df00cc..bedffc6d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - shard: [1/10, 2/10, 3/10, 4/10, 5/10, 6/10, 7/10, 8/10, 9/10, 10/10] + 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] steps: - uses: actions/checkout@v6 diff --git a/README.md b/README.md index 1c3bd2b5..a8efbb38 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Algorithm visualization web app for learners. Step through algorithms with synch ## Features -- **367 Algorithms across 14 Categories** with interactive visualizations (bar charts, SVG graphs/trees, CSS grids, DP tables, and more) +- **452 Algorithms across 14 Categories** with interactive visualizations (bar charts, SVG graphs/trees, CSS grids, DP tables, and more) - **Multi-Language Code Display**: TypeScript, Python, and Java with synchronized line highlighting via Monaco Editor - **Step-by-Step Playback**: Play, pause, step forward/backward, scrub, adjustable speed (0.25x–4x) - **Category-Specific Input Editors**: Editable arrays, targets, grids, text patterns, and matrices @@ -19,7 +19,7 @@ Algorithm visualization web app for learners. Step through algorithms with synch ## Algorithms -**367 algorithms across 14 categories**: Sorting (53 algorithms across 9 technique subcategories), Searching, Graph (28 algorithms across 10 technique subcategories), Pathfinding (27 algorithms across 5 technique subcategories), Dynamic Programming (32 algorithms across 6 technique subcategories), Arrays (44 algorithms across 11 technique subcategories), Trees, Linked Lists, Heaps (28 algorithms across 4 technique subcategories), Stacks & Queues (28 algorithms across 8 technique subcategories), Hash Maps (28 algorithms across 8 technique subcategories), Strings (32 algorithms across 6 technique subcategories), Matrices (20 algorithms across 5 technique subcategories), and Sets (19 algorithms across 5 technique subcategories). +**452 algorithms across 14 categories**: Sorting (53 algorithms across 9 technique subcategories), Searching, Graph (28 algorithms across 10 technique subcategories), Pathfinding (27 algorithms across 5 technique subcategories), Dynamic Programming (32 algorithms across 6 technique subcategories), Arrays (44 algorithms across 11 technique subcategories), Trees (87 algorithms across 6 technique subcategories), Linked Lists, Heaps (28 algorithms across 4 technique subcategories), Stacks & Queues (28 algorithms across 8 technique subcategories), Hash Maps (28 algorithms across 8 technique subcategories), Strings (32 algorithms across 6 technique subcategories), Matrices (20 algorithms across 5 technique subcategories), and Sets (19 algorithms across 5 technique subcategories). See the [full Algorithm Catalog](docs/algorithms-catalog.md) for the complete listing with visualizer descriptions and technique subcategories. @@ -57,7 +57,7 @@ Welcome to the definitive map of AlgoFlow. We maintain 12 specialized guides map | Debug step-generation crashes | 🐛 [Debugging](docs/debugging.md) | | Work on UI layout or styling | 💅 [Design System](docs/design-system.md) | | Write algorithm learning modules | 📚 [Educational Content Guide](docs/educational-content-guide.md) | -| Browse all 367 algorithms | 🔍 [Algorithm Catalog](docs/algorithms-catalog.md) | +| Browse all 452 algorithms | 🔍 [Algorithm Catalog](docs/algorithms-catalog.md) | | Understand AI hooks & plugins | 🤖 [Development System](docs/claude-system.md) | > [!TIP] > **First-time contributors:** Do not try to hack around aimlessly. Start strictly at the [New Developer Onboarding Guide](docs/onboarding.md). It dictates the hard boundary between the UI logic and the engine. @@ -93,7 +93,7 @@ All complex operational instructions have been modularized: This repository leverages automatic pre-commit quality gates, git protection blocking direct pushes to `main`, and accessibility scanners. -Please review the 12 active session hooks in the [Development System Guide](docs/claude-system.md#session-hooks) to understand the terminal environments preventing bad commits. +Please review the 13 active session hooks in the [Development System Guide](docs/claude-system.md#session-hooks) to understand the terminal environments preventing bad commits. ## Development Plan diff --git a/docs/algorithms-catalog.md b/docs/algorithms-catalog.md index e6685509..a82867ba 100644 --- a/docs/algorithms-catalog.md +++ b/docs/algorithms-catalog.md @@ -2,22 +2,22 @@ # Algorithm Catalog -Complete listing of all 367 algorithms available in AlgoFlow, organized by category with visualizer descriptions and technique subcategories. +Complete listing of all 452 algorithms available in AlgoFlow, organized by category with visualizer descriptions and technique subcategories. > **Prerequisites:** None — this is a reference document. ## Contents - [Sorting (53)](#sorting-53-algorithms) -- [Searching (1)](#searching-1-algorithm) +- [Searching (18)](#searching-18-algorithms) - [Graph (28)](#graph-28-algorithms) - [Pathfinding (27)](#pathfinding-27-algorithms) - [Dynamic Programming (32)](#dynamic-programming-32-algorithms) - [Arrays (44)](#arrays-44-algorithms) -- [Trees (1)](#trees-1-algorithm) -- [Linked Lists (1)](#linked-lists-1-algorithm) +- [Trees (87)](#trees-87-algorithms) +- [Linked Lists (8)](#linked-lists-8-algorithms) - [Heaps (28)](#heaps-28-algorithms) -- [Stacks & Queues (1)](#stacks--queues-1-algorithm) +- [Stacks & Queues (28)](#stacks--queues-28-algorithms) - [Hash Maps (28)](#hash-maps-28-algorithms) - [Strings (32)](#strings-32-algorithms) - [Matrices (20)](#matrices-20-algorithms) @@ -87,7 +87,7 @@ Algorithms that arrange elements in a specific order. Algorithms live under `src --- -## Searching (1 algorithm) +## Searching (18 algorithms) Algorithms that find elements in data structures. Algorithms live under `src/algorithms/searching///`. @@ -333,17 +333,130 @@ The largest category, covering a wide range of array manipulation techniques. Al --- -## Trees (1 algorithm) +## Trees (87 algorithms) Algorithms that traverse or manipulate tree data structures. Algorithms live under `src/algorithms/trees///`. -| Technique | Algorithm | Visualizer | Source Directory | -| --------- | ---------------------- | ------------------------------------ | --------------------------------------------- | -| Traversal | BST In-Order Traversal | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-inorder/` | +### Traversal (13) + +| Technique | Algorithm | Visualizer | Source Directory | +| --------- | ---------------------------------- | ------------------------------------ | ------------------------------------------------------------------------- | +| Traversal | BST In-Order Traversal | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-inorder/` | +| Traversal | BST In-Order (Iterative) | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-inorder-iterative/` | +| Traversal | BST Pre-Order Traversal | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-preorder/` | +| Traversal | BST Pre-Order (Iterative) | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-preorder-iterative/` | +| Traversal | BST Post-Order Traversal | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-postorder/` | +| Traversal | BST Post-Order (Iterative) | SVG binary tree with traversal order | `src/algorithms/trees/traversal/bst-postorder-iterative/` | +| Traversal | Level-Order Traversal | SVG binary tree with level highlights | `src/algorithms/trees/traversal/level-order-traversal/` | +| Traversal | Reverse Level-Order | SVG binary tree with level highlights | `src/algorithms/trees/traversal/reverse-level-order/` | +| Traversal | Zigzag Level-Order | SVG binary tree with alternating direction | `src/algorithms/trees/traversal/zigzag-level-order/` | +| Traversal | Vertical Order Traversal | SVG binary tree with column grouping | `src/algorithms/trees/traversal/vertical-order-traversal/` | +| Traversal | Boundary Traversal | SVG binary tree with boundary path | `src/algorithms/trees/traversal/boundary-traversal/` | +| Traversal | Diagonal Traversal | SVG binary tree with diagonal groups | `src/algorithms/trees/traversal/diagonal-traversal/` | +| Traversal | Morris In-Order Traversal | SVG binary tree with thread links | `src/algorithms/trees/traversal/morris-inorder-traversal/` | + +### BST Operations (21) + +| Technique | Algorithm | Visualizer | Source Directory | +| -------------- | -------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------- | +| BST Operations | BST Search | SVG binary tree with search path | `src/algorithms/trees/bst-operations/bst-search/` | +| BST Operations | BST Search (Iterative) | SVG binary tree with search path | `src/algorithms/trees/bst-operations/bst-search-iterative/` | +| BST Operations | BST Insert | SVG binary tree with insertion point | `src/algorithms/trees/bst-operations/bst-insert/` | +| BST Operations | BST Insert (Iterative) | SVG binary tree with insertion point | `src/algorithms/trees/bst-operations/bst-insert-iterative/` | +| BST Operations | BST Delete | SVG binary tree with deletion steps | `src/algorithms/trees/bst-operations/bst-delete/` | +| BST Operations | BST Delete (Iterative) | SVG binary tree with deletion steps | `src/algorithms/trees/bst-operations/bst-delete-iterative/` | +| BST Operations | BST Validation | SVG binary tree with range bounds | `src/algorithms/trees/bst-operations/bst-validation/` | +| BST Operations | BST Validation (Iterative) | SVG binary tree with range bounds | `src/algorithms/trees/bst-operations/bst-validation-iterative/` | +| BST Operations | BST Kth Smallest | SVG binary tree with in-order count | `src/algorithms/trees/bst-operations/bst-kth-smallest/` | +| BST Operations | BST Kth Smallest (Iterative) | SVG binary tree with in-order count | `src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/` | +| BST Operations | BST Lowest Common Ancestor | SVG binary tree with ancestor path | `src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/` | +| BST Operations | BST Lowest Common Ancestor (Iterative) | SVG binary tree with ancestor path | `src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/` | +| BST Operations | BST from Sorted Array | SVG binary tree with construction steps | `src/algorithms/trees/bst-operations/bst-from-sorted-array/` | +| BST Operations | BST Iterator | SVG binary tree with stack state | `src/algorithms/trees/bst-operations/bst-iterator/` | +| BST Operations | BST Floor and Ceil | SVG binary tree with range search | `src/algorithms/trees/bst-operations/bst-floor-ceil/` | +| BST Operations | BST Floor and Ceil (Iterative) | SVG binary tree with range search | `src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/` | +| BST Operations | BST Range Sum | SVG binary tree with range highlights | `src/algorithms/trees/bst-operations/bst-range-sum/` | +| BST Operations | BST Range Sum (Iterative) | SVG binary tree with range highlights | `src/algorithms/trees/bst-operations/bst-range-sum-iterative/` | +| BST Operations | BST to Greater Tree | SVG binary tree with reverse in-order sum | `src/algorithms/trees/bst-operations/bst-to-greater-tree/` | +| BST Operations | BST to Greater Tree (Iterative) | SVG binary tree with reverse in-order sum | `src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/` | +| BST Operations | BST Recover Swapped | SVG binary tree with swapped node markers | `src/algorithms/trees/bst-operations/bst-recover-swapped/` | + +### Properties (21) + +| Technique | Algorithm | Visualizer | Source Directory | +| ---------- | -------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------- | +| Properties | Maximum Depth | SVG binary tree with depth annotation | `src/algorithms/trees/properties/maximum-depth/` | +| Properties | Maximum Depth (Iterative) | SVG binary tree with depth annotation | `src/algorithms/trees/properties/maximum-depth-iterative/` | +| Properties | Minimum Depth | SVG binary tree with depth annotation | `src/algorithms/trees/properties/minimum-depth/` | +| Properties | Minimum Depth (Iterative) | SVG binary tree with depth annotation | `src/algorithms/trees/properties/minimum-depth-iterative/` | +| Properties | Diameter of Binary Tree | SVG binary tree with longest path highlight | `src/algorithms/trees/properties/diameter-of-binary-tree/` | +| Properties | Is Balanced Tree | SVG binary tree with height balance check | `src/algorithms/trees/properties/is-balanced-tree/` | +| Properties | Is Balanced Tree (Iterative) | SVG binary tree with height balance check | `src/algorithms/trees/properties/is-balanced-tree-iterative/` | +| Properties | Is Symmetric Tree | SVG binary tree with mirror comparison | `src/algorithms/trees/properties/is-symmetric-tree/` | +| Properties | Is Symmetric Tree (Iterative) | SVG binary tree with mirror comparison | `src/algorithms/trees/properties/is-symmetric-tree-iterative/` | +| Properties | Count Complete Tree Nodes | SVG binary tree with node count | `src/algorithms/trees/properties/count-complete-tree-nodes/` | +| Properties | Path Sum | SVG binary tree with root-to-leaf path | `src/algorithms/trees/properties/path-sum/` | +| Properties | Path Sum (Iterative) | SVG binary tree with root-to-leaf path | `src/algorithms/trees/properties/path-sum-iterative/` | +| Properties | Maximum Path Sum | SVG binary tree with max path highlight | `src/algorithms/trees/properties/maximum-path-sum/` | +| Properties | Sum of Left Leaves | SVG binary tree with left leaf highlights | `src/algorithms/trees/properties/sum-of-left-leaves/` | +| Properties | Sum of Left Leaves (Iterative) | SVG binary tree with left leaf highlights | `src/algorithms/trees/properties/sum-of-left-leaves-iterative/` | +| Properties | All Root-to-Leaf Paths | SVG binary tree with path enumeration | `src/algorithms/trees/properties/all-root-to-leaf-paths/` | +| Properties | All Root-to-Leaf Paths (Iterative) | SVG binary tree with path enumeration | `src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/` | +| Properties | Binary Tree Tilt | SVG binary tree with tilt value annotation | `src/algorithms/trees/properties/binary-tree-tilt/` | +| Properties | Cousins in Binary Tree | SVG binary tree with level and parent check | `src/algorithms/trees/properties/cousins-in-binary-tree/` | +| Properties | Sum Root-to-Leaf Numbers | SVG binary tree with path number accumulation | `src/algorithms/trees/properties/sum-root-to-leaf-numbers/` | +| Properties | Sum Root-to-Leaf Numbers (Iterative) | SVG binary tree with path number accumulation | `src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/` | + +### Construction (6) + +| Technique | Algorithm | Visualizer | Source Directory | +| ------------ | --------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Construction | Build from Pre-Order + In-Order | SVG binary tree with construction steps | `src/algorithms/trees/construction/build-from-preorder-inorder/` | +| Construction | Build from Pre-Order + In-Order (Iter.) | SVG binary tree with construction steps | `src/algorithms/trees/construction/build-from-preorder-inorder-iterative/` | +| Construction | Build from Post-Order + In-Order | SVG binary tree with construction steps | `src/algorithms/trees/construction/build-from-postorder-inorder/` | +| Construction | Build from Post-Order + In-Order (Iter.)| SVG binary tree with construction steps | `src/algorithms/trees/construction/build-from-postorder-inorder-iterative/` | +| Construction | Serialize / Deserialize Tree | SVG binary tree with serialization buffer | `src/algorithms/trees/construction/serialize-deserialize-tree/` | +| Construction | Build from Level-Order | SVG binary tree with level-by-level insertion | `src/algorithms/trees/construction/build-from-level-order/` | + +### Manipulation (16) + +| Technique | Algorithm | Visualizer | Source Directory | +| ------------ | ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------- | +| Manipulation | Invert Binary Tree | SVG binary tree with child swap animation | `src/algorithms/trees/manipulation/invert-binary-tree/` | +| Manipulation | Invert Binary Tree (Iterative) | SVG binary tree with child swap animation | `src/algorithms/trees/manipulation/invert-binary-tree-iterative/` | +| Manipulation | Flatten to Linked List | SVG binary tree with right-chain formation | `src/algorithms/trees/manipulation/flatten-to-linked-list/` | +| Manipulation | Flatten to Linked List (Iterative) | SVG binary tree with right-chain formation | `src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/` | +| Manipulation | Right Side View | SVG binary tree with rightmost node highlight | `src/algorithms/trees/manipulation/right-side-view/` | +| Manipulation | Right Side View (Recursive) | SVG binary tree with rightmost node highlight | `src/algorithms/trees/manipulation/right-side-view-recursive/` | +| Manipulation | Same Tree | SVG dual binary tree with node comparison | `src/algorithms/trees/manipulation/same-tree/` | +| Manipulation | Same Tree (Iterative) | SVG dual binary tree with node comparison | `src/algorithms/trees/manipulation/same-tree-iterative/` | +| Manipulation | Merge Binary Trees | SVG dual binary tree with merge steps | `src/algorithms/trees/manipulation/merge-binary-trees/` | +| Manipulation | Merge Binary Trees (Iterative) | SVG dual binary tree with merge steps | `src/algorithms/trees/manipulation/merge-binary-trees-iterative/` | +| Manipulation | Subtree of Another Tree | SVG binary tree with subtree matching | `src/algorithms/trees/manipulation/subtree-of-another-tree/` | +| Manipulation | Lowest Common Ancestor | SVG binary tree with ancestor path | `src/algorithms/trees/manipulation/lowest-common-ancestor/` | +| Manipulation | Lowest Common Ancestor (Iterative) | SVG binary tree with ancestor path | `src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/` | +| Manipulation | Delete Leaves with Value | SVG binary tree with leaf pruning steps | `src/algorithms/trees/manipulation/delete-leaves-with-value/` | +| Manipulation | Distribute Coins | SVG binary tree with coin flow animation | `src/algorithms/trees/manipulation/distribute-coins/` | +| Manipulation | Flip Equivalent Trees | SVG dual binary tree with flip comparison | `src/algorithms/trees/manipulation/flip-equivalent-trees/` | + +### Advanced (10) + +| Technique | Algorithm | Visualizer | Source Directory | +| --------- | ------------------------------ | -------------------------------------------------- | ----------------------------------------------------------------------- | +| Advanced | AVL Insert with Rotation | SVG binary tree with rotation animation | `src/algorithms/trees/advanced/avl-insert-rotation/` | +| Advanced | Red-Black Insert | SVG binary tree with color and rotation steps | `src/algorithms/trees/advanced/red-black-insert/` | +| Advanced | Segment Tree Range Sum | SVG segment tree with range query highlight | `src/algorithms/trees/advanced/segment-tree-range-sum/` | +| Advanced | Segment Tree Range Min | SVG segment tree with range query highlight | `src/algorithms/trees/advanced/segment-tree-range-min/` | +| Advanced | Binary Indexed Tree | SVG BIT array with prefix sum visualization | `src/algorithms/trees/advanced/binary-indexed-tree/` | +| Advanced | Tree to Doubly Linked List | SVG binary tree with in-order pointer rewiring | `src/algorithms/trees/advanced/tree-to-doubly-linked-list/` | +| Advanced | Binary Tree Pruning | SVG binary tree with subtree removal steps | `src/algorithms/trees/advanced/binary-tree-pruning/` | +| Advanced | N-Ary Tree Traversal | SVG n-ary tree with child expansion | `src/algorithms/trees/advanced/n-ary-tree-traversal/` | +| Advanced | Huffman Coding Tree | SVG Huffman tree with frequency-based construction | `src/algorithms/trees/advanced/huffman-coding-tree/` | +| Advanced | Expression Tree Evaluation | SVG expression tree with operator evaluation | `src/algorithms/trees/advanced/expression-tree-evaluation/` | --- -## Linked Lists (1 algorithm) +## Linked Lists (8 algorithms) Algorithms that manipulate linked list nodes. Algorithms live under `src/algorithms/linked-lists///`. @@ -390,7 +503,7 @@ Algorithms involving heap/priority queue operations. Algorithms live under `src/ --- -## Stacks & Queues (1 algorithm) +## Stacks & Queues (28 algorithms) Algorithms using stack or queue data structures. Algorithms live under `src/algorithms/stacks-queues///`. diff --git a/docs/architecture.md b/docs/architecture.md index 10564e39..215442d7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,7 +21,7 @@ AlgoFlow uses a **registry-driven** architecture with **pre-computed execution s ## Development System -The `.claude/` directory defines 11 agents, 18 skills, 14 session hooks, and 17 plugins for development workflow automation and quality enforcement. See [Development System](claude-system.md) for the full reference with tables of all agents, skills, hooks, and plugins. +The `.claude/` directory defines 11 agents, 18 skills, 13 session hooks, and 17 plugins for development workflow automation and quality enforcement. See [Development System](claude-system.md) for the full reference with tables of all agents, skills, hooks, and plugins. --- @@ -165,7 +165,7 @@ Breakpoint values are defined in `BREAKPOINTS` (`src/utils/constants.ts`). Layou ## Input Editors -The `InputEditor` component is split into 6 focused files, each handling a specific input shape: +The `InputEditor` component is split into 10 focused files, each handling a specific input shape: | File | Handles | | ------------------------- | ----------------------------------------------------------------------- | @@ -192,6 +192,7 @@ Each algorithm category has a tailored input editor rendered above the visualiza | Strings | Text string + pattern string | | Matrices | Textarea (one row per line, comma-separated) | | Sets | Two comma-separated arrays (A and B) | +| Trees | No input editor — tree structure is fixed per algorithm | > [!IMPORTANT] > All input edits are **temporary and non-persistent**. Edits reset on algorithm switch or page reload. No localStorage, URL state, or server persistence. @@ -272,7 +273,7 @@ src/ │ ├── pathfinding/ # e.g. pathfinding/shortest-path/dijkstra/ │ ├── dynamic-programming/ # 32 algorithms across 1d-linear, optimization, counting, subsequence, knapsack, string-dp │ ├── arrays/ # 44 algorithms across sliding-window, two-pointer, prefix-sum, and more -│ ├── trees/ # e.g. trees/traversal/bst-inorder/ +│ ├── trees/ # 87 algorithms across traversal, bst-operations, properties, construction, manipulation, advanced │ ├── linked-lists/ # e.g. linked-lists/manipulation/reverse-linked-list/ │ ├── heaps/ # e.g. heaps/construction/build-min-heap/ │ ├── stacks-queues/ # e.g. stacks-queues/validation/valid-parentheses/ diff --git a/docs/claude-system.md b/docs/claude-system.md index 089429c5..9b54d8be 100644 --- a/docs/claude-system.md +++ b/docs/claude-system.md @@ -11,7 +11,7 @@ AlgoFlow uses a structured development workflow powered by agents, skills, sessi - [Overview](#overview) - [Agents (10)](#agents-10) - [Skills (16)](#skills-16) -- [Session Hooks (12)](#session-hooks-12) +- [Session Hooks (13)](#session-hooks-13) - [Plugins (17)](#plugins-17) - [Branch Naming and Plugin Auto-Detection](#branch-naming-and-plugin-auto-detection) - [Rules Files and Path Scoping](#rules-files-and-path-scoping) @@ -76,7 +76,7 @@ Skill definitions live in `.claude/skills//SKILL.md`. --- -## Session Hooks (12) +## Session Hooks (13) Hooks run automatically during development sessions. They are configured in `.claude/settings.json`. diff --git a/docs/contributing.md b/docs/contributing.md index 8600bbd3..6b375fe7 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -260,7 +260,12 @@ export function generateMyAlgorithmSteps(input: number[]): ExecutionStep[] { | Pathfinding | `PathfindingTracker` | `initialize`, `visit`, `updateDistance`, `reconstructPath`, `complete`, `setCurrentNode`, `updateCost`, `openNodeReverse`, `closeNodeReverse`, `carveCell`, `markJumpPoint`, `buildWall`, `mergeCells`, `traceReversePath`, `setPhase` | | Dynamic Programming | `DPTracker` | `initialize`, `compute`, `lookup`, `complete` | | Arrays | `ArrayTracker` | `initialize`, `moveWindow`, `expandWindow`, `shrinkWindow`, `visit`, `swap`, `compareTwo`, `markElement`, `setWindowActive`, `setSecondaryArray`, `updateSecondaryElement`, `complete` | -| Trees | `TreeTracker` | `initialize`, `visit`, `traverse`, `complete` | +| Trees (traversal) | `TreeTracker` | `initialize`, `visit`, `traverse`, `complete` | +| Trees (BST ops) | `BSTOperationTracker` | `initialize`, `search`, `insert`, `delete`, `validate`, `complete` | +| Trees (properties) | `TreePropertyTracker` | `initialize`, `computeDepth`, `checkBalance`, `checkSymmetry`, `tracePath`, `complete` | +| Trees (construction) | `TreeConstructionTracker` | `initialize`, `placeNode`, `linkChild`, `deserialize`, `complete` | +| Trees (manipulation) | `TreeManipulationTracker` | `initialize`, `invertNodes`, `flattenNode`, `mergeNodes`, `findAncestor`, `complete` | +| Trees (advanced) | `AdvancedTreeTracker` | `initialize`, `rotate`, `recolor`, `buildSegment`, `queryRange`, `encode`, `complete` | | Linked Lists | `LinkedListTracker` | `initialize`, `traverse`, `reverse`, `complete` | | Heaps | `HeapTracker` | `initialize`, `compare`, `swap`, `siftDown`, `complete` | | Stacks & Queues | `StackQueueTracker` | `initialize`, `push`, `pop`, `check`, `complete` | diff --git a/docs/debugging.md b/docs/debugging.md index cb47f2f9..d78d87d7 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -14,6 +14,7 @@ This guide covers the most common failure modes in AlgoFlow and how to fix them - [Visualizer Rendering Problems](#visualizer-rendering-problems) - [?fn Import Pipeline](#fn-import-pipeline) - [Registry & Self-Registration](#registry--self-registration) +- [Tree-Specific Debugging](#tree-specific-debugging) - [E2E Test Failures](#e2e-test-failures) - [See Also](#see-also) @@ -100,22 +101,27 @@ Cross-reference each key in the map against the step `type` values produced by ` The visualization panel renders nothing (blank panel) when the `VisualState.kind` produced by the tracker does not match any registered visualizer. -**Valid `kind` values** — the `VisualState` discriminated union currently has 12 members: - -| `kind` | Use case | -| ------------- | ------------------------------------------ | -| `array` | Sorting, searching, sliding window | -| `graph` | BFS, DFS, graph traversal | -| `grid` | Pathfinding (Dijkstra, A\*) | -| `dp-table` | Dynamic programming (Fibonacci tabulation) | -| `tree` | Binary trees, heaps (display) | -| `linked-list` | Linked list algorithms | -| `heap` | Priority queue / heap operations | -| `stack-queue` | Stack or queue walkthroughs | -| `hash-map` | Hash table algorithms | -| `string` | String matching, palindromes | -| `matrix` | 2-D matrix traversals | -| `set` | Set operations | +**Valid `kind` values** — the `VisualState` discriminated union currently has 17 members: + +| `kind` | Use case | +| ------------------- | ------------------------------------------ | +| `array` | Sorting, searching, sliding window | +| `graph` | BFS, DFS, graph traversal | +| `grid` | Pathfinding (Dijkstra, A\*) | +| `dp-table` | Dynamic programming (Fibonacci tabulation) | +| `tree` | Binary trees, heaps (display) | +| `linked-list` | Linked list algorithms | +| `heap` | Priority queue / heap operations | +| `stack-queue` | Stack or queue walkthroughs | +| `hash-map` | Hash table algorithms | +| `string` | General string matching | +| `string-palindrome` | Palindrome detection algorithms | +| `string-frequency` | Character frequency and anagram algorithms | +| `string-transform` | String edit and transformation algorithms | +| `string-trie` | Trie construction and search algorithms | +| `string-distance` | Edit distance and alignment algorithms | +| `matrix` | 2-D matrix traversals | +| `set` | Set operations | **Common causes:** @@ -123,7 +129,7 @@ The visualization panel renders nothing (blank panel) when the `VisualState.kind - Copying a tracker from a different category and forgetting to change the `kind` field in the `visualState` builder - The visualizer switch/dispatch has not been updated to handle a newly added `kind` -**Debug pattern:** Log the `visualState.kind` of each step and confirm it matches one of the 12 values above: +**Debug pattern:** Log the `visualState.kind` of each step and confirm it matches one of the 17 values above: ```ts const steps = generateSteps(knownInput); @@ -184,6 +190,36 @@ Verify the `meta.id` is unique across all algorithm definitions. --- +## Tree-Specific Debugging + +Tree algorithms use the `tree` VisualState kind and have several fields that require extra attention: + +### Dual-tree algorithms (`secondaryTree`) + +Some tree algorithms (e.g., LCA, symmetric tree check) operate on two trees simultaneously. These algorithms populate a `secondaryTree` field alongside the primary `nodes` and `edges` arrays. If the secondary tree renders blank, verify that `secondaryTree` is set on every step — omitting it on even one step causes the secondary panel to disappear mid-playback. + +### N-ary tree support (`childrenIds`) + +Each `TreeNode` carries a `childrenIds` array listing the IDs of its children. Binary tree algorithms always produce exactly 0, 1, or 2 entries. N-ary algorithms (tries, segment trees, expression trees) may produce many. If a child node renders disconnected from its parent, check that the parent's `childrenIds` includes the child's ID and that the child appears in the `nodes` array. + +**Debug pattern:** + +```ts +const steps = generateSteps(knownInput); +const step = steps[0]; +if (step.visualState.kind === "tree") { + step.visualState.nodes.forEach((node) => { + console.log(node.id, "->", node.childrenIds); + }); +} +``` + +### Segment tree queries (`queryRange`) + +Segment tree algorithms expose a `queryRange` field `[left, right]` on the `tree` VisualState to indicate the active query interval. If highlighted nodes do not match the expected range, confirm that `queryRange` is updated correctly each time the active segment changes. + +--- + ## E2E Test Failures The E2E suite uses `@playwright/test`. Spec files live in `e2e/specs/` (21 files, ~950 tests). Config is at `e2e/playwright.config.ts`. The `webServer` block auto-starts Vite on port 5174 so no manual dev server is needed. diff --git a/docs/deployment.md b/docs/deployment.md index 8c87be40..3c75b649 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -54,8 +54,8 @@ Triggers on all pull requests to `main`. Runs these jobs in parallel: | Job | What It Does | | ---------------------------- | ----------------------------------------------------------------------------------------------------- | | **Type Check & Lint** | `npm run typecheck`, `npm run lint`, `npm run format:check` | -| **Unit Tests** | `npm run test` — sharded 10 ways; results aggregated under the **Unit Tests Status** required check | -| **E2E Tests** | `npm run e2e` — sharded 12 ways (15-min timeout per shard); aggregated under the **E2E Status** check | +| **Unit Tests** | `npm run test` — sharded 12 ways; results aggregated under the **Unit Tests Status** required check | +| **E2E Tests** | `npm run e2e` — sharded 16 ways (15-min timeout per shard); aggregated under the **E2E Status** check | | **Storybook Build** | `npm run storybook:build` | | **Visual Tests (Chromatic)** | Runs after Storybook build; requires `CHROMATIC_PROJECT_TOKEN` secret | diff --git a/docs/design-system.md b/docs/design-system.md index daf102de..f1389e4c 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -80,6 +80,19 @@ These tokens map directly to algorithm visualization states. When building a new All defined in `src/index.css` under the `@theme` block. +### TreeNodeState Token Mapping + +When rendering tree nodes, `TreeNodeState` values map to visualization tokens as follows: + +| `TreeNodeState` | CSS Token | Color | +| --------------- | --------------------- | ------- | +| `found` | `--color-viz-found` | Emerald | +| `comparing` | `--color-viz-comparing` | Cyan | +| `target` | `--color-viz-current` | Rose | +| `highlighted` | `--color-viz-visiting` | Blue | + +Use these tokens in `TreeVisualizer` node renders — never raw hex values. + --- ## Category Accent Map diff --git a/docs/glossary.md b/docs/glossary.md index 54031e70..07b8514a 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -141,11 +141,34 @@ The 17 `kind` values are: --- +### TreeNodeState + +A string-literal union describing the visual state of a single node in the `TreeVisualizer`. Each value maps to a distinct highlight color so learners can distinguish node roles at a glance. + +**Defined in:** `src/types/execution.ts` (as part of the `tree` VisualState shape) + +The 8 `TreeNodeState` values are: + +| Value | Meaning | +| ------------- | ---------------------------------------------------------- | +| `default` | Unvisited node in its initial state | +| `visiting` | Node currently being entered or opened | +| `visited` | Node whose subtree has been fully processed | +| `current` | The node being actively examined at this step | +| `found` | Target node that satisfies the search condition | +| `comparing` | Node being compared against another node or value | +| `target` | A reference node used as a comparison anchor | +| `highlighted` | Auxiliary highlight for path reconstruction or emphasis | + +The `tree` VisualState also carries a `childrenIds` field on each `TreeNode`, which lists child node IDs for n-ary tree support. Binary trees use at most two entries; n-ary trees (e.g., tries, segment trees) may have many. + +--- + ### Tracker An abstract base class used inside `generateSteps()` to build up the `ExecutionStep[]` array. Each tracker subclass provides domain-specific recording methods that internally call `pushStep()`. You construct a tracker, call its methods as you trace through your algorithm logic, and at the end collect the completed step array. -There are 14 category-specific tracker subclasses (e.g. `SortingTracker`, `ArrayTracker`, `GraphTracker`). You never use `Tracker` directly — you use the appropriate subclass for your algorithm's data structure. +There are 34 category-specific tracker subclasses (e.g. `SortingTracker`, `ArrayTracker`, `GraphTracker`). You never use `Tracker` directly — you use the appropriate subclass for your algorithm's data structure. **Defined in:** `src/trackers/base-tracker.ts` **Used by:** every algorithm's `generateSteps()` function. diff --git a/docs/testing.md b/docs/testing.md index bd17b618..668ff7dd 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -20,7 +20,7 @@ npm run test:coverage # Run with coverage report npm run test:watch # Watch mode during development ``` -Tests cover algorithm correctness, step generation, tracker behavior, and store state transitions across all 367 algorithms in 14 categories. +Tests cover algorithm correctness, step generation, tracker behavior, and store state transitions across all 452 algorithms in 14 categories. ### Vitest Projects Configuration @@ -33,7 +33,7 @@ The Vitest config uses the `projects` feature to split the test suite into two i This avoids the overhead of loading `jsdom` for pure algorithm tests and removes the need for manual timeout configuration in `test-setup.ts`. -CI shards unit tests 8 ways (aggregated under the **Unit Tests Status** job) and E2E tests 12 ways (aggregated under the **E2E Status** job). +CI shards unit tests 12 ways (aggregated under the **Unit Tests Status** job) and E2E tests 16 ways (aggregated under the **E2E Status** job). > [!TIP] > Run a subset of tests with `npx vitest --filter ` (e.g., `npx vitest --filter bubble-sort`). @@ -140,7 +140,7 @@ e2e/ └── helpers/ # Shared Playwright helpers and selectors ``` -~950 tests across 21 spec files. Per-category spec files use `test.describe.configure({ mode: "serial" })` to run tests in declaration order. Workers: 2 locally, 4 on CI. In CI the suite is sharded 12 ways, aggregated under the **E2E Status** check. +~950 tests across 21 spec files. Per-category spec files use `test.describe.configure({ mode: "serial" })` to run tests in declaration order. Workers: 2 locally, 4 on CI. In CI the suite is sharded 16 ways, aggregated under the **E2E Status** check. ### What the Suite Covers @@ -192,7 +192,7 @@ npm run chromatic # Run Chromatic visual tests ### Story Inventory -**88 story files** organized into: +**483 story files** organized into: | Category | Location | Stories | | -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -204,7 +204,7 @@ npm run chromatic # Run Chromatic visual tests | **Input Editor** | `src/components/input-editor/` | ArrayInputEditor, InputEditor | | **Explanation Panel** | `src/components/explanation-panel/` | ExplanationPanel | | **Playback** | `src/components/playback/` | PlaybackControls | -| **Algorithm Pipelines** | `src/algorithms///` | 367 algorithm pipelines — initial, mid-execution, and final states using real step generators | +| **Algorithm Pipelines** | `src/algorithms///` | 452 algorithm pipelines — initial, mid-execution, and final states using real step generators | Pipeline stories (`*.Pipeline.stories.tsx`) live alongside their algorithm implementation, not with the visualizer components. Component stories remain co-located with their components in `src/components/`. diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/AVLInsertRotationPipeline.stories.tsx b/src/algorithms/trees/advanced/avl-insert-rotation/AVLInsertRotationPipeline.stories.tsx new file mode 100644 index 00000000..aae31066 --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/AVLInsertRotationPipeline.stories.tsx @@ -0,0 +1,42 @@ +/** + * Storybook pipeline stories for AVL Insert Rotation. + * Shows the tree at initialization, mid-rotation, and completion. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateAvlInsertRotationSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateAvlInsertRotationSteps({ values: [10, 20, 30, 25, 28, 27] }); + +const meta: Meta = { + title: "Algorithm Pipelines/AVL Insert Rotation", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty tree before insertions */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — tree after some insertions with rotations in progress */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — fully balanced AVL tree */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/avl-insert-rotation.test.ts b/src/algorithms/trees/advanced/avl-insert-rotation/avl-insert-rotation.test.ts new file mode 100644 index 00000000..590dbf77 --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/avl-insert-rotation.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { avlInsertRotation } from "./sources/avl-insert-rotation.ts?fn"; + +describe("avlInsertRotation", () => { + it("inserts a single value", () => { + expect(avlInsertRotation([5])).toEqual([5]); + }); + + it("produces sorted inorder output for default input", () => { + const result = avlInsertRotation([10, 20, 30, 25, 28, 27]) as number[]; + expect(result).toEqual([...result].sort((numA, numB) => numA - numB)); + }); + + it("handles RR rotation (ascending insert)", () => { + const result = avlInsertRotation([1, 2, 3]) as number[]; + expect(result).toEqual([1, 2, 3]); + }); + + it("handles LL rotation (descending insert)", () => { + const result = avlInsertRotation([3, 2, 1]) as number[]; + expect(result).toEqual([1, 2, 3]); + }); + + it("handles LR rotation", () => { + const result = avlInsertRotation([3, 1, 2]) as number[]; + expect(result).toEqual([1, 2, 3]); + }); + + it("handles RL rotation", () => { + const result = avlInsertRotation([1, 3, 2]) as number[]; + expect(result).toEqual([1, 2, 3]); + }); + + it("correctly sorts 6 values that trigger multiple rotations", () => { + const values = [10, 20, 30, 25, 28, 27]; + const result = avlInsertRotation(values) as number[]; + const expected = [...values].sort((numA, numB) => numA - numB); + expect(result).toEqual(expected); + }); + + it("returns empty array for empty input", () => { + expect(avlInsertRotation([])).toEqual([]); + }); +}); diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/educational.ts b/src/algorithms/trees/advanced/avl-insert-rotation/educational.ts new file mode 100644 index 00000000..9466f385 --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/educational.ts @@ -0,0 +1,50 @@ +import type { EducationalContent } from "@/types"; + +export const avlInsertRotationEducational: EducationalContent = { + overview: + "**AVL Tree Insertion** maintains a self-balancing binary search tree by applying rotations after each insertion. Named after Adelson-Velsky and Landis (1962), an AVL tree guarantees that for every node, the heights of its left and right subtrees differ by at most 1 — the **balance factor**.\n\nWhen an insertion violates this invariant, one of four rotation patterns (LL, RR, LR, RL) is applied to restore balance.", + + howItWorks: + "Insertion proceeds in two phases:\n\n" + + "1. **Standard BST insert** — place the new node as in a regular BST.\n" + + "2. **Rebalance on the way back** — update heights and check balance factors bottom-up. Apply the appropriate rotation when `|balance| > 1`:\n\n" + + "| Case | Condition | Fix |\n" + + "|------|-----------|-----|\n" + + "| **LL** | Left-heavy, inserted in left subtree | Single right rotation |\n" + + "| **RR** | Right-heavy, inserted in right subtree | Single left rotation |\n" + + "| **LR** | Left-heavy, inserted in left's right subtree | Left then right rotation |\n" + + "| **RL** | Right-heavy, inserted in right's left subtree | Right then left rotation |\n\n" + + "After each rotation, heights are recalculated and the tree is balanced again.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(log n)` per insertion**\n\n" + + "Because the AVL invariant guarantees height `O(log n)`, both traversal and rebalancing are bounded logarithmically.\n\n" + + "**Space Complexity: `O(log n)`** (call stack depth for recursive insertion).", + + bestAndWorstCase: + "**Best case:** Insertions require no rotations (already balanced) — `O(log n)` purely for traversal.\n\n" + + "**Worst case:** Every insertion requires a double rotation — still `O(log n)` per insert, but constant factors are higher than a plain BST. The classical [10, 20, 30, 25, 28, 27] sequence triggers all four rotation types in sequence.", + + realWorldUses: [ + "**Database indexing:** Used in some database engines for balanced in-memory B-tree nodes.", + "**Ordered map/set:** Languages like C++ (`std::map`) use red-black trees, but AVL trees provide faster lookups for read-heavy workloads.", + "**Network routing tables:** Where lookup speed is critical and insertions are infrequent.", + "**File system directories:** Some filesystems use balanced BSTs to index directory entries.", + ], + + strengthsAndLimitations: { + strengths: [ + "Strictly balanced — lookup is always `O(log n)`, faster than red-black trees on read-heavy workloads.", + "Deterministic height bound: height ≤ 1.44 log₂(n+2).", + "Conceptually straightforward rotations compared to more complex self-balancing schemes.", + ], + limitations: [ + "More rotations per insertion than red-black trees — slower for write-heavy workloads.", + "Requires storing a height (or balance factor) in each node — small memory overhead.", + "Deletion is complex and also requires rebalancing.", + ], + }, + + whenToUseIt: + "Use an AVL tree when you need guaranteed O(log n) lookup with a roughly equal mix of reads and writes. For write-heavy applications, prefer a red-black tree. For bulk-loaded, read-only sorted data, a sorted array with binary search is simpler.", +}; diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/index.ts b/src/algorithms/trees/advanced/avl-insert-rotation/index.ts new file mode 100644 index 00000000..bab13aab --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/index.ts @@ -0,0 +1,37 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { avlInsertRotation } from "./sources/avl-insert-rotation.ts?fn"; +import { generateAvlInsertRotationSteps } from "./step-generator"; +import type { AvlInsertRotationInput } from "./step-generator"; +import { avlInsertRotationEducational } from "./educational"; + +import typescriptSource from "./sources/avl-insert-rotation.ts?raw"; +import pythonSource from "./sources/avl-insert-rotation.py?raw"; +import javaSource from "./sources/AVLInsertRotation.java?raw"; + +function executeAvlInsertRotation(input: AvlInsertRotationInput): number[] { + return avlInsertRotation(input.values) as number[]; +} + +const avlInsertRotationDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.AVL_INSERT_ROTATION!, + name: "AVL Insert & Rotation", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Insert values into a self-balancing AVL tree, demonstrating LL/RR/LR/RL rotations that maintain O(log n) height", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(log n)" }, + spaceComplexity: "O(log n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [10, 20, 30, 25, 28, 27] }, + }, + execute: executeAvlInsertRotation, + generateSteps: generateAvlInsertRotationSteps, + educational: avlInsertRotationEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(avlInsertRotationDefinition); diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/sources/AVLInsertRotation.java b/src/algorithms/trees/advanced/avl-insert-rotation/sources/AVLInsertRotation.java new file mode 100644 index 00000000..06130d5b --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/sources/AVLInsertRotation.java @@ -0,0 +1,74 @@ +// AVL Tree Insertion with Rotations — maintains balance via LL/RR/LR/RL rotations +import java.util.ArrayList; +import java.util.List; + +class AVLNode { + int value, height; + AVLNode left, right; + AVLNode(int v) { value = v; height = 1; } +} + +class AVLInsertRotation { + private int height(AVLNode node) { + return node == null ? 0 : node.height; // @step:check-balance + } + + private void updateHeight(AVLNode node) { + node.height = 1 + Math.max(height(node.left), height(node.right)); // @step:update-height + } + + private int balanceFactor(AVLNode node) { + return height(node.left) - height(node.right); // @step:check-balance + } + + private AVLNode rotateRight(AVLNode pivot) { + AVLNode leftChild = pivot.left; // @step:rotate-right + pivot.left = leftChild.right; + leftChild.right = pivot; + updateHeight(pivot); + updateHeight(leftChild); + return leftChild; // @step:rotate-right + } + + private AVLNode rotateLeft(AVLNode pivot) { + AVLNode rightChild = pivot.right; // @step:rotate-left + pivot.right = rightChild.left; + rightChild.left = pivot; + updateHeight(pivot); + updateHeight(rightChild); + return rightChild; // @step:rotate-left + } + + private AVLNode insert(AVLNode node, int value) { + if (node == null) return new AVLNode(value); // @step:insert-node + + if (value < node.value) node.left = insert(node.left, value); // @step:traverse-left + else if (value > node.value) node.right = insert(node.right, value); // @step:traverse-right + else return node; // @step:visit + + updateHeight(node); + int balance = balanceFactor(node); // @step:check-balance + + if (balance > 1 && node.left != null && value < node.left.value) return rotateRight(node); // @step:rotate-right + if (balance < -1 && node.right != null && value > node.right.value) return rotateLeft(node); // @step:rotate-left + if (balance > 1 && node.left != null) { node.left = rotateLeft(node.left); return rotateRight(node); } // @step:rotate-left + if (balance < -1 && node.right != null) { node.right = rotateRight(node.right); return rotateLeft(node); } // @step:rotate-right + + return node; + } + + public List avlInsertRotation(int[] values) { + AVLNode root = null; // @step:initialize + for (int value : values) root = insert(root, value); // @step:insert-node + List result = new ArrayList<>(); + inorder(root, result); + return result; // @step:complete + } + + private void inorder(AVLNode node, List result) { + if (node == null) return; + inorder(node.left, result); + result.add(node.value); + inorder(node.right, result); + } +} diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/sources/avl-insert-rotation.py b/src/algorithms/trees/advanced/avl-insert-rotation/sources/avl-insert-rotation.py new file mode 100644 index 00000000..df43678e --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/sources/avl-insert-rotation.py @@ -0,0 +1,80 @@ +# AVL Tree Insertion with Rotations — maintains balance via LL/RR/LR/RL rotations + +class AVLNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + self.height = 1 + +def avl_insert_rotation(values: list) -> list: + root = None # @step:initialize + + def height(node) -> int: + return node.height if node else 0 # @step:check-balance + + def update_height(node) -> None: + node.height = 1 + max(height(node.left), height(node.right)) # @step:update-height + + def balance_factor(node) -> int: + return height(node.left) - height(node.right) # @step:check-balance + + def rotate_right(pivot_node): + left_child = pivot_node.left # @step:rotate-right + pivot_node.left = left_child.right + left_child.right = pivot_node + update_height(pivot_node) + update_height(left_child) + return left_child # @step:rotate-right + + def rotate_left(pivot_node): + right_child = pivot_node.right # @step:rotate-left + pivot_node.right = right_child.left + right_child.left = pivot_node + update_height(pivot_node) + update_height(right_child) + return right_child # @step:rotate-left + + def insert(node, value): + if not node: + return AVLNode(value) # @step:insert-node + + if value < node.value: + node.left = insert(node.left, value) # @step:traverse-left + elif value > node.value: + node.right = insert(node.right, value) # @step:traverse-right + else: + return node # @step:visit + + update_height(node) + balance = balance_factor(node) # @step:check-balance + + # LL case + if balance > 1 and node.left and value < node.left.value: + return rotate_right(node) # @step:rotate-right + # RR case + if balance < -1 and node.right and value > node.right.value: + return rotate_left(node) # @step:rotate-left + # LR case + if balance > 1 and node.left: + node.left = rotate_left(node.left) # @step:rotate-left + return rotate_right(node) # @step:rotate-right + # RL case + if balance < -1 and node.right: + node.right = rotate_right(node.right) # @step:rotate-right + return rotate_left(node) # @step:rotate-left + + return node + + for value in values: + root = insert(root, value) # @step:insert-node + + result = [] + def inorder(node): + if not node: + return + inorder(node.left) + result.append(node.value) + inorder(node.right) + inorder(root) + return result # @step:complete diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/sources/avl-insert-rotation.ts b/src/algorithms/trees/advanced/avl-insert-rotation/sources/avl-insert-rotation.ts new file mode 100644 index 00000000..26c1c222 --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/sources/avl-insert-rotation.ts @@ -0,0 +1,93 @@ +// AVL Tree Insertion with Rotations — maintains balance via LL/RR/LR/RL rotations + +interface AVLNode { + value: number; + left: AVLNode | null; + right: AVLNode | null; + height: number; +} + +function avlInsertRotation(values: number[]): number[] { + let root: AVLNode | null = null; // @step:initialize + + function height(node: AVLNode | null): number { + return node ? node.height : 0; // @step:check-balance + } + + function updateHeight(node: AVLNode): void { + node.height = 1 + Math.max(height(node.left), height(node.right)); // @step:update-height + } + + function balanceFactor(node: AVLNode): number { + return height(node.left) - height(node.right); // @step:check-balance + } + + function rotateRight(pivotNode: AVLNode): AVLNode { + const leftChild = pivotNode.left!; // @step:rotate-right + pivotNode.left = leftChild.right; + leftChild.right = pivotNode; + updateHeight(pivotNode); + updateHeight(leftChild); + return leftChild; // @step:rotate-right + } + + function rotateLeft(pivotNode: AVLNode): AVLNode { + const rightChild = pivotNode.right!; // @step:rotate-left + pivotNode.right = rightChild.left; + rightChild.left = pivotNode; + updateHeight(pivotNode); + updateHeight(rightChild); + return rightChild; // @step:rotate-left + } + + function insert(node: AVLNode | null, value: number): AVLNode { + if (!node) return { value, left: null, right: null, height: 1 }; // @step:insert-node + + if (value < node.value) { + node.left = insert(node.left, value); // @step:traverse-left + } else if (value > node.value) { + node.right = insert(node.right, value); // @step:traverse-right + } else { + return node; // @step:visit + } + + updateHeight(node); + const balance = balanceFactor(node); // @step:check-balance + + // LL case + if (balance > 1 && node.left && value < node.left.value) { + return rotateRight(node); // @step:rotate-right + } + // RR case + if (balance < -1 && node.right && value > node.right.value) { + return rotateLeft(node); // @step:rotate-left + } + // LR case + if (balance > 1 && node.left) { + node.left = rotateLeft(node.left); // @step:rotate-left + return rotateRight(node); // @step:rotate-right + } + // RL case + if (balance < -1 && node.right) { + node.right = rotateRight(node.right); // @step:rotate-right + return rotateLeft(node); // @step:rotate-left + } + + return node; + } + + for (const value of values) { + root = insert(root, value); // @step:insert-node + } + + // Return inorder traversal to verify sorted order + const result: number[] = []; + function inorder(node: AVLNode | null): void { + if (!node) return; + inorder(node.left); + result.push(node.value); + inorder(node.right); + } + inorder(root); + return result; // @step:complete +} diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/step-generator.test.ts b/src/algorithms/trees/advanced/avl-insert-rotation/step-generator.test.ts new file mode 100644 index 00000000..d5ed953e --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/step-generator.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { generateAvlInsertRotationSteps } from "./step-generator"; + +describe("generateAvlInsertRotationSteps", () => { + const defaultInput = { values: [10, 20, 30, 25, 28, 27] }; + + it("produces steps for default input", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces rotation steps for a sequence that triggers rotations", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + const rotationSteps = steps.filter( + (step) => step.type === "rotate-left" || step.type === "rotate-right", + ); + expect(rotationSteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("produces insert steps for each inserted value", () => { + const steps = generateAvlInsertRotationSteps(defaultInput); + const insertSteps = steps.filter((step) => step.type === "insert-child"); + expect(insertSteps.length).toBe(defaultInput.values.length); + }); +}); diff --git a/src/algorithms/trees/advanced/avl-insert-rotation/step-generator.ts b/src/algorithms/trees/advanced/avl-insert-rotation/step-generator.ts new file mode 100644 index 00000000..e06cec0a --- /dev/null +++ b/src/algorithms/trees/advanced/avl-insert-rotation/step-generator.ts @@ -0,0 +1,199 @@ +/** Step generator for AVL Insert Rotation — inserts values one by one, triggering rotations. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const AVL_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.AVL_INSERT_ROTATION!); + +export interface AvlInsertRotationInput { + values: number[]; +} + +interface AvlNode { + id: string; + value: number; + height: number; + leftId: string | null; + rightId: string | null; + parentId: string | null; +} + +function getHeight(nodes: Map, nodeId: string | null): number { + if (!nodeId) return 0; + return nodes.get(nodeId)?.height ?? 0; +} + +function updateHeightAvl(nodes: Map, nodeId: string): void { + const node = nodes.get(nodeId); + if (!node) return; + node.height = 1 + Math.max(getHeight(nodes, node.leftId), getHeight(nodes, node.rightId)); +} + +function balanceFactorAvl(nodes: Map, nodeId: string): number { + const node = nodes.get(nodeId); + if (!node) return 0; + return getHeight(nodes, node.leftId) - getHeight(nodes, node.rightId); +} + +function toTreeNodes(avlNodes: Map, rootId: string | null): TreeNode[] { + const posMap = new Map(); + + function assignPos(nodeId: string | null, depth: number, minX: number, maxX: number): void { + if (!nodeId) return; + const node = avlNodes.get(nodeId); + if (!node) return; + const midX = (minX + maxX) / 2; + posMap.set(nodeId, { x: midX, y: depth * 80 + 40 }); + assignPos(node.leftId, depth + 1, minX, midX); + assignPos(node.rightId, depth + 1, midX, maxX); + } + + assignPos(rootId, 0, 20, 480); + + return Array.from(avlNodes.values()).map((node) => ({ + id: node.id, + value: node.value, + parentId: node.parentId, + leftChildId: node.leftId, + rightChildId: node.rightId, + state: "default" as const, + position: posMap.get(node.id) ?? { x: 200, y: 40 }, + })); +} + +export function generateAvlInsertRotationSteps(input: AvlInsertRotationInput): ExecutionStep[] { + const { values } = input; + const avlNodes = new Map(); + let rootId: string | null = null; + let nodeCounter = 0; + + const tracker = new AdvancedTreeTracker([], "root", AVL_LINE_MAP); + tracker.initialize("AVL Insert Rotation", { values }); + + function makeId(): string { + nodeCounter += 1; + return `avl${nodeCounter}`; + } + + function syncTracker(): void { + const treeNodes = toTreeNodes(avlNodes, rootId); + tracker.updateNodes(treeNodes, rootId ?? "root"); + } + + function rotateRight(pivotId: string): string { + const pivot = avlNodes.get(pivotId)!; + const leftChildId = pivot.leftId!; + const leftChild = avlNodes.get(leftChildId)!; + + pivot.leftId = leftChild.rightId; + if (leftChild.rightId) { + avlNodes.get(leftChild.rightId)!.parentId = pivotId; + } + leftChild.rightId = pivotId; + leftChild.parentId = pivot.parentId; + pivot.parentId = leftChildId; + + if (rootId === pivotId) rootId = leftChildId; + + updateHeightAvl(avlNodes, pivotId); + updateHeightAvl(avlNodes, leftChildId); + + syncTracker(); + tracker.rotateRight(leftChildId, { + pivot: pivot.value, + newRoot: leftChild.value, + type: "right", + }); + return leftChildId; + } + + function rotateLeft(pivotId: string): string { + const pivot = avlNodes.get(pivotId)!; + const rightChildId = pivot.rightId!; + const rightChild = avlNodes.get(rightChildId)!; + + pivot.rightId = rightChild.leftId; + if (rightChild.leftId) { + avlNodes.get(rightChild.leftId)!.parentId = pivotId; + } + rightChild.leftId = pivotId; + rightChild.parentId = pivot.parentId; + pivot.parentId = rightChildId; + + if (rootId === pivotId) rootId = rightChildId; + + updateHeightAvl(avlNodes, pivotId); + updateHeightAvl(avlNodes, rightChildId); + + syncTracker(); + tracker.rotateLeft(rightChildId, { + pivot: pivot.value, + newRoot: rightChild.value, + type: "left", + }); + return rightChildId; + } + + function insert(nodeId: string | null, value: number, parentId: string | null): string { + if (!nodeId) { + const newId = makeId(); + avlNodes.set(newId, { + id: newId, + value, + height: 1, + leftId: null, + rightId: null, + parentId, + }); + syncTracker(); + tracker.insertNode(newId, value, { inserted: value, nodeId: newId }); + return newId; + } + + const node = avlNodes.get(nodeId)!; + if (value < node.value) { + node.leftId = insert(node.leftId, value, nodeId); + } else if (value > node.value) { + node.rightId = insert(node.rightId, value, nodeId); + } else { + return nodeId; + } + + updateHeightAvl(avlNodes, nodeId); + const balance = balanceFactorAvl(avlNodes, nodeId); + + syncTracker(); + tracker.checkBalance(nodeId, balance, { node: node.value, balance }); + + // LL case + if (balance > 1 && node.leftId && value < avlNodes.get(node.leftId)!.value) { + return rotateRight(nodeId); + } + // RR case + if (balance < -1 && node.rightId && value > avlNodes.get(node.rightId)!.value) { + return rotateLeft(nodeId); + } + // LR case + if (balance > 1 && node.leftId) { + node.leftId = rotateLeft(node.leftId); + return rotateRight(nodeId); + } + // RL case + if (balance < -1 && node.rightId) { + node.rightId = rotateRight(node.rightId); + return rotateLeft(nodeId); + } + + return nodeId; + } + + for (const value of values) { + rootId = insert(rootId, value, null); + } + + tracker.complete({ result: `AVL tree balanced with ${avlNodes.size} nodes`, rootId }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/BinaryIndexedTreePipeline.stories.tsx b/src/algorithms/trees/advanced/binary-indexed-tree/BinaryIndexedTreePipeline.stories.tsx new file mode 100644 index 00000000..37382709 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/BinaryIndexedTreePipeline.stories.tsx @@ -0,0 +1,47 @@ +/** + * Storybook pipeline stories for Binary Indexed Tree (Fenwick Tree). + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBinaryIndexedTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBinaryIndexedTreeSteps({ + array: [3, 2, 4, 5, 1, 1, 5, 3], + queries: [ + [0, 4], + [2, 6], + ], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Binary Indexed Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — BIT array before population */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — BIT partially built */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — all queries answered */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/binary-indexed-tree.test.ts b/src/algorithms/trees/advanced/binary-indexed-tree/binary-indexed-tree.test.ts new file mode 100644 index 00000000..14c485ae --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/binary-indexed-tree.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { binaryIndexedTree } from "./sources/binary-indexed-tree.ts?fn"; + +describe("binaryIndexedTree", () => { + it("computes range sums for default input", () => { + const result = binaryIndexedTree( + [3, 2, 4, 5, 1, 1, 5, 3], + [ + [0, 4], + [2, 6], + ], + ) as number[]; + expect(result[0]).toBe(15); // 3+2+4+5+1 + expect(result[1]).toBe(16); // 4+5+1+1+5 + }); + + it("handles single element query", () => { + const result = binaryIndexedTree([10, 20, 30], [[1, 1]]) as number[]; + expect(result[0]).toBe(20); + }); + + it("handles full range query", () => { + const arr = [1, 2, 3, 4, 5]; + const result = binaryIndexedTree(arr, [[0, 4]]) as number[]; + expect(result[0]).toBe(15); + }); + + it("handles multiple queries", () => { + const result = binaryIndexedTree( + [5, 3, 2, 8, 1], + [ + [0, 2], + [1, 4], + [2, 3], + ], + ) as number[]; + expect(result[0]).toBe(10); // 5+3+2 + expect(result[1]).toBe(14); // 3+2+8+1 + expect(result[2]).toBe(10); // 2+8 + }); +}); diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/educational.ts b/src/algorithms/trees/advanced/binary-indexed-tree/educational.ts new file mode 100644 index 00000000..69b70f78 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/educational.ts @@ -0,0 +1,52 @@ +import type { EducationalContent } from "@/types"; + +export const binaryIndexedTreeEducational: EducationalContent = { + overview: + "A **Binary Indexed Tree (BIT)**, also called a **Fenwick Tree**, is a space-efficient data structure for computing prefix sums and range sums with point updates in `O(log n)`. It stores partial sums in an array indexed using the least-significant bit (LSB) trick — `index & (-index)` reveals how many elements each position is responsible for.\n\nInvented by Peter Fenwick in 1994, BITs are simpler and faster in practice than segment trees for pure prefix-sum workloads.", + + howItWorks: + "**Key insight:** Every integer can be expressed as a sum of powers of 2. The BIT uses the binary representation of indices to determine responsibilities:\n\n" + + "- `bit[i]` stores the sum of `array[i - lsb(i) + 1 ... i]` where `lsb(i) = i & (-i)`.\n\n" + + "**Update `update(i, delta)`:** Add `delta` to index `i` and propagate up:\n" + + "```\n" + + "while i ≤ n: bit[i] += delta; i += i & (-i)\n" + + "```\n\n" + + "**Prefix sum `query(i)`:** Sum from index 1 to i:\n" + + "```\n" + + "while i > 0: sum += bit[i]; i -= i & (-i)\n" + + "```\n\n" + + "**Range sum `[L, R]`** = `query(R+1) - query(L)`.", + + timeAndSpaceComplexity: + "**Update: `O(log n)`** — at most `log₂(n)` additions per update.\n\n" + + "**Query: `O(log n)`** — at most `log₂(n)` additions per prefix sum.\n\n" + + "**Space: `O(n)`** — just one extra array of size n+1.", + + bestAndWorstCase: + "**Best case:** Single-element query — O(log n) but with very small constants.\n\n" + + "**Worst case:** Repeated updates followed by all range queries — O((n + q) log n).\n\n" + + "BITs are typically 2-3× faster than segment trees in practice due to lower constant factors.", + + realWorldUses: [ + "**Order statistics:** Count inversions in an array in O(n log n).", + "**Frequency tables:** Maintain dynamic frequency distributions for sorting.", + "**Competitive programming:** Standard tool for prefix sum with updates.", + "**Event simulation:** Track cumulative event counts in sliding window problems.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simpler implementation than segment trees — just 10-15 lines of code.", + "O(n) space — no 4× overhead unlike segment trees.", + "Cache-friendly array access pattern.", + ], + limitations: [ + "Only supports commutative and invertible operations (sum, XOR) — not min/max.", + "Range updates require a modified construction (difference BIT).", + "Conceptually less intuitive than segment trees.", + ], + }, + + whenToUseIt: + "Use a BIT when you need prefix sums or range sums with point updates and want minimum code complexity. For range minimum/maximum queries or range updates, use a segment tree instead.", +}; diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/index.ts b/src/algorithms/trees/advanced/binary-indexed-tree/index.ts new file mode 100644 index 00000000..c405fadd --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/index.ts @@ -0,0 +1,43 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { binaryIndexedTree } from "./sources/binary-indexed-tree.ts?fn"; +import { generateBinaryIndexedTreeSteps } from "./step-generator"; +import type { BinaryIndexedTreeInput } from "./step-generator"; +import { binaryIndexedTreeEducational } from "./educational"; + +import typescriptSource from "./sources/binary-indexed-tree.ts?raw"; +import pythonSource from "./sources/binary-indexed-tree.py?raw"; +import javaSource from "./sources/BinaryIndexedTree.java?raw"; + +function executeBinaryIndexedTree(input: BinaryIndexedTreeInput): number[] { + return binaryIndexedTree(input.array, input.queries) as number[]; +} + +const binaryIndexedTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BINARY_INDEXED_TREE!, + name: "Binary Indexed Tree (Fenwick)", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Build a Fenwick Tree for O(log n) prefix sum queries and point updates using the LSB bit trick", + timeComplexity: { best: "O(n + q log n)", average: "O(n + q log n)", worst: "O(n + q log n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + array: [3, 2, 4, 5, 1, 1, 5, 3], + queries: [ + [0, 4], + [2, 6], + ], + }, + }, + execute: executeBinaryIndexedTree, + generateSteps: generateBinaryIndexedTreeSteps, + educational: binaryIndexedTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(binaryIndexedTreeDefinition); diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/sources/BinaryIndexedTree.java b/src/algorithms/trees/advanced/binary-indexed-tree/sources/BinaryIndexedTree.java new file mode 100644 index 00000000..ebc5754c --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/sources/BinaryIndexedTree.java @@ -0,0 +1,38 @@ +// Binary Indexed Tree (Fenwick Tree) — prefix sum queries and point updates +import java.util.ArrayList; +import java.util.List; + +class BinaryIndexedTree { + private int[] bit; + private int n; + + private void update(int bitIdx, int delta) { + while (bitIdx <= n) { + bit[bitIdx] += delta; // @step:update-segment + bitIdx += bitIdx & (-bitIdx); + } + } + + private int prefixSum(int bitIdx) { + int total = 0; + while (bitIdx > 0) { + total += bit[bitIdx]; // @step:compute-prefix + bitIdx -= bitIdx & (-bitIdx); + } + return total; // @step:compute-prefix + } + + public List binaryIndexedTree(int[] array, int[][] queries) { + n = array.length; // @step:initialize + bit = new int[n + 1]; // @step:initialize + for (int pos = 0; pos < n; pos++) { + update(pos + 1, array[pos]); // @step:update-segment + } + List results = new ArrayList<>(); + for (int[] query : queries) { + int rangeSum = prefixSum(query[1] + 1) - prefixSum(query[0]); // @step:query-range + results.add(rangeSum); + } + return results; // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/sources/binary-indexed-tree.py b/src/algorithms/trees/advanced/binary-indexed-tree/sources/binary-indexed-tree.py new file mode 100644 index 00000000..06ef89f8 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/sources/binary-indexed-tree.py @@ -0,0 +1,26 @@ +# Binary Indexed Tree (Fenwick Tree) — prefix sum queries and point updates + +def binary_indexed_tree(array: list, queries: list) -> list: + n = len(array) # @step:initialize + bit = [0] * (n + 1) # @step:initialize + + def update(bit_idx, delta): + while bit_idx <= n: + bit[bit_idx] += delta # @step:update-segment + bit_idx += bit_idx & (-bit_idx) + + def prefix_sum(bit_idx): + total = 0 + while bit_idx > 0: + total += bit[bit_idx] # @step:compute-prefix + bit_idx -= bit_idx & (-bit_idx) + return total # @step:compute-prefix + + for pos in range(n): + update(pos + 1, array[pos]) # @step:update-segment + + results = [] + for q_low, q_high in queries: + range_sum = prefix_sum(q_high + 1) - prefix_sum(q_low) # @step:query-range + results.append(range_sum) + return results # @step:complete diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/sources/binary-indexed-tree.ts b/src/algorithms/trees/advanced/binary-indexed-tree/sources/binary-indexed-tree.ts new file mode 100644 index 00000000..1ebc8afc --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/sources/binary-indexed-tree.ts @@ -0,0 +1,36 @@ +// Binary Indexed Tree (Fenwick Tree) — prefix sum queries and point updates + +function binaryIndexedTree(array: number[], queries: [number, number][]): number[] { + const arrayLength = array.length; // @step:initialize + const bit: number[] = new Array(arrayLength + 1).fill(0); // @step:initialize + + // Build the BIT by inserting each element + function update(bitIndex: number, delta: number): void { + while (bitIndex <= arrayLength) { + bit[bitIndex] = (bit[bitIndex] ?? 0) + delta; // @step:update-segment + bitIndex += bitIndex & -bitIndex; // move to parent + } + } + + function prefixSum(bitIndex: number): number { + let totalSum = 0; + while (bitIndex > 0) { + totalSum += bit[bitIndex] ?? 0; // @step:compute-prefix + bitIndex -= bitIndex & -bitIndex; // move to responsible ancestor + } + return totalSum; // @step:compute-prefix + } + + // Build BIT from array (1-indexed) + for (let pos = 0; pos < arrayLength; pos++) { + update(pos + 1, array[pos] ?? 0); // @step:update-segment + } + + const results: number[] = []; + for (const [queryLow, queryHigh] of queries) { + // Range sum [queryLow, queryHigh] = prefix[queryHigh+1] - prefix[queryLow] + const rangeSumResult = prefixSum(queryHigh + 1) - prefixSum(queryLow); // @step:query-range + results.push(rangeSumResult); + } + return results; // @step:complete +} diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/step-generator.test.ts b/src/algorithms/trees/advanced/binary-indexed-tree/step-generator.test.ts new file mode 100644 index 00000000..5863df56 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/step-generator.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { generateBinaryIndexedTreeSteps } from "./step-generator"; + +describe("generateBinaryIndexedTreeSteps", () => { + const defaultInput = { + array: [3, 2, 4, 5, 1, 1, 5, 3], + queries: [ + [0, 4], + [2, 6], + ] as [number, number][], + }; + + it("produces steps for default input", () => { + const steps = generateBinaryIndexedTreeSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBinaryIndexedTreeSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBinaryIndexedTreeSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBinaryIndexedTreeSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces compute-prefix steps during queries", () => { + const steps = generateBinaryIndexedTreeSteps(defaultInput); + const prefixSteps = steps.filter((step) => step.type === "compute-prefix"); + expect(prefixSteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateBinaryIndexedTreeSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/binary-indexed-tree/step-generator.ts b/src/algorithms/trees/advanced/binary-indexed-tree/step-generator.ts new file mode 100644 index 00000000..760bfe93 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-indexed-tree/step-generator.ts @@ -0,0 +1,117 @@ +/** Step generator for Binary Indexed Tree (Fenwick Tree) — prefix sum queries. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BIT_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BINARY_INDEXED_TREE!); + +export interface BinaryIndexedTreeInput { + array: number[]; + queries: [number, number][]; +} + +/** + * Represent the BIT array as a visual tree. + * BIT index bitIdx is responsible for the range of length (bitIdx & -bitIdx). + * For display: node bitIdx's parent is bitIdx + (bitIdx & -bitIdx). + */ +function buildBitTreeNodes(bitArray: number[], arrayLength: number): TreeNode[] { + const treeNodes: TreeNode[] = []; + + // For layout, place nodes in a row with index as x position + for (let bitIdx = 1; bitIdx <= arrayLength; bitIdx++) { + const responsibleRange = bitIdx & -bitIdx; + const parentBitIdx = bitIdx + responsibleRange; + + treeNodes.push({ + id: `bit${bitIdx}`, + value: bitArray[bitIdx] ?? 0, + parentId: parentBitIdx <= arrayLength ? `bit${parentBitIdx}` : null, + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: (bitIdx - 1) * 55 + 30, y: (Math.floor(Math.log2(bitIdx)) + 1) * 70 }, + }); + } + + return treeNodes; +} + +export function generateBinaryIndexedTreeSteps(input: BinaryIndexedTreeInput): ExecutionStep[] { + const { array, queries } = input; + const arrayLength = array.length; + const bitArray: number[] = new Array(arrayLength + 1).fill(0); + + const tracker = new AdvancedTreeTracker([], "bit1", BIT_LINE_MAP); + tracker.initialize("Binary Indexed Tree", { array, queries }); + + function syncTracker(highlightedId?: string): void { + const treeNodes = buildBitTreeNodes(bitArray, arrayLength); + if (highlightedId) { + const highlightedNode = treeNodes.find((node) => node.id === highlightedId); + if (highlightedNode) highlightedNode.state = "current"; + } + tracker.updateNodes(treeNodes, "bit1"); + } + + // Build BIT with update steps + for (let pos = 0; pos < arrayLength; pos++) { + const value = array[pos] ?? 0; + let bitIdx = pos + 1; + while (bitIdx <= arrayLength) { + bitArray[bitIdx] = (bitArray[bitIdx] ?? 0) + value; + syncTracker(`bit${bitIdx}`); + tracker.updateSegment(`bit${bitIdx}`, bitArray[bitIdx] ?? 0, { + originalIndex: pos, + bitIndex: bitIdx, + delta: value, + bitValue: bitArray[bitIdx], + }); + bitIdx += bitIdx & -bitIdx; + } + } + + // Process each range query + for (const [queryLow, queryHigh] of queries) { + // Compute prefix sum up to queryHigh+1 + let highSum = 0; + let bitIdx = queryHigh + 1; + while (bitIdx > 0) { + highSum += bitArray[bitIdx] ?? 0; + syncTracker(`bit${bitIdx}`); + tracker.computePrefix(bitIdx, highSum, { + bitIndex: bitIdx, + partialSum: highSum, + phase: "high", + }); + bitIdx -= bitIdx & -bitIdx; + } + + // Compute prefix sum up to queryLow + let lowSum = 0; + bitIdx = queryLow; + while (bitIdx > 0) { + lowSum += bitArray[bitIdx] ?? 0; + syncTracker(`bit${bitIdx}`); + tracker.computePrefix(bitIdx, lowSum, { + bitIndex: bitIdx, + partialSum: lowSum, + phase: "low", + }); + bitIdx -= bitIdx & -bitIdx; + } + + const rangeResult = highSum - lowSum; + tracker.queryRange("bit1", queryLow, queryHigh, { + queryRange: [queryLow, queryHigh], + highPrefixSum: highSum, + lowPrefixSum: lowSum, + result: rangeResult, + }); + tracker.complete({ queryRange: [queryLow, queryHigh], result: rangeResult }); + } + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/BinaryTreePruningPipeline.stories.tsx b/src/algorithms/trees/advanced/binary-tree-pruning/BinaryTreePruningPipeline.stories.tsx new file mode 100644 index 00000000..c069333f --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/BinaryTreePruningPipeline.stories.tsx @@ -0,0 +1,107 @@ +/** + * Storybook pipeline stories for Binary Tree Pruning. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeNode, TreeVisualState } from "@/types"; +import { generateBinaryTreePruningSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n1", + value: 1, + parentId: null, + leftChildId: "n0a", + rightChildId: "n1b", + state: "default", + position: { x: 200, y: 40 }, + }, + { + id: "n0a", + value: 0, + parentId: "n1", + leftChildId: "n0c", + rightChildId: "n0d", + state: "default", + position: { x: 100, y: 120 }, + }, + { + id: "n1b", + value: 1, + parentId: "n1", + leftChildId: "n0e", + rightChildId: "n1f", + state: "default", + position: { x: 300, y: 120 }, + }, + { + id: "n0c", + value: 0, + parentId: "n0a", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 200 }, + }, + { + id: "n0d", + value: 0, + parentId: "n0a", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 200 }, + }, + { + id: "n0e", + value: 0, + parentId: "n1b", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 200 }, + }, + { + id: "n1f", + value: 1, + parentId: "n1b", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 200 }, + }, +]; + +const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Binary Tree Pruning", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full tree before pruning */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — some subtrees pruned */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — pruned tree */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/binary-tree-pruning.test.ts b/src/algorithms/trees/advanced/binary-tree-pruning/binary-tree-pruning.test.ts new file mode 100644 index 00000000..d2a2d995 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/binary-tree-pruning.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { binaryTreePruning } from "./sources/binary-tree-pruning.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} +function node( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("binaryTreePruning", () => { + it("returns null for a tree of all zeros", () => { + const root = node(0, node(0), node(0)); + expect(binaryTreePruning(root)).toBeNull(); + }); + + it("returns null for a single zero node", () => { + expect(binaryTreePruning(node(0))).toBeNull(); + }); + + it("keeps a single one node", () => { + const result = binaryTreePruning(node(1)); + expect(result).not.toBeNull(); + expect((result as BinaryNode).value).toBe(1); + }); + + it("prunes zero-only subtrees", () => { + // Root 1 with left subtree [0, 0, 0] and right [1, 0, 1] + const root = node(1, node(0, node(0), node(0)), node(1, node(0), node(1))); + const pruned = binaryTreePruning(root) as BinaryNode; + // Left subtree should be pruned + expect(pruned.left).toBeNull(); + // Right subtree should keep 1 nodes + expect(pruned.right).not.toBeNull(); + expect(pruned.right?.right?.value).toBe(1); + expect(pruned.right?.left).toBeNull(); + }); + + it("returns null for null input", () => { + expect(binaryTreePruning(null)).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/educational.ts b/src/algorithms/trees/advanced/binary-tree-pruning/educational.ts new file mode 100644 index 00000000..09316cbd --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/educational.ts @@ -0,0 +1,45 @@ +import type { EducationalContent } from "@/types"; + +export const binaryTreePruningEducational: EducationalContent = { + overview: + "**Binary Tree Pruning** removes every subtree from a binary tree that does not contain the value **1**. Node values are either 0 or 1. The algorithm uses **post-order traversal** — children are processed before their parent — so a parent can safely decide whether to prune itself based on its (already pruned) children.", + + howItWorks: + "The recursive algorithm works bottom-up:\n\n" + + "1. Recurse into the **left** subtree — prune it, potentially returning `null`.\n" + + "2. Recurse into the **right** subtree — prune it, potentially returning `null`.\n" + + "3. If the current node has **value 0** and **both children are null** (leaf with no 1s), return `null` (prune this node).\n" + + "4. Otherwise, return the current node (keep it).\n\n" + + "Because post-order processes leaves first, entire subtrees collapse upward as they are found to contain no 1s.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)`** — recursion stack depth equals tree height.", + + bestAndWorstCase: + "**Best case:** The root has value 1 — no nodes are pruned, minimal work.\n\n" + + "**Worst case:** Every node has value 0 — the entire tree is pruned bottom-up, visiting all `n` nodes.", + + realWorldUses: [ + "**Decision tree cleanup:** Prune branches in a decision tree that lead to trivial outcomes.", + "**Trie pruning:** Remove unreachable or unused paths from tries after bulk deletions.", + "**DOM tree cleanup:** Remove empty subtrees from a document object model.", + "**Game tree pruning:** Remove game branches where no winning moves exist.", + ], + + strengthsAndLimitations: { + strengths: [ + "In-place mutation — no new tree allocation needed.", + "Simple, elegant post-order recursion.", + "Easily extended to prune based on any predicate, not just value 0.", + ], + limitations: [ + "Mutates the original tree — not safe if the original needs to be preserved.", + "Recursive — stack overflow possible on very deep trees.", + "Specific to binary trees with 0/1 values; generalization requires changing the leaf predicate.", + ], + }, + + whenToUseIt: + "Use binary tree pruning when you need to remove subtrees that don't satisfy a condition and the original tree can be modified in-place. For non-destructive pruning, build a new tree instead.", +}; diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/index.ts b/src/algorithms/trees/advanced/binary-tree-pruning/index.ts new file mode 100644 index 00000000..35814061 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/index.ts @@ -0,0 +1,131 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { binaryTreePruning } from "./sources/binary-tree-pruning.ts?fn"; +import { generateBinaryTreePruningSteps } from "./step-generator"; +import type { BinaryTreePruningInput } from "./step-generator"; +import { binaryTreePruningEducational } from "./educational"; + +import typescriptSource from "./sources/binary-tree-pruning.ts?raw"; +import pythonSource from "./sources/binary-tree-pruning.py?raw"; +import javaSource from "./sources/BinaryTreePruning.java?raw"; + +/** Tree with some zero-only subtrees that will be pruned */ +const defaultNodes: TreeNode[] = [ + { + id: "n1", + value: 1, + parentId: null, + leftChildId: "n0a", + rightChildId: "n1b", + state: "default", + position: { x: 200, y: 40 }, + }, + { + id: "n0a", + value: 0, + parentId: "n1", + leftChildId: "n0c", + rightChildId: "n0d", + state: "default", + position: { x: 100, y: 120 }, + }, + { + id: "n1b", + value: 1, + parentId: "n1", + leftChildId: "n0e", + rightChildId: "n1f", + state: "default", + position: { x: 300, y: 120 }, + }, + { + id: "n0c", + value: 0, + parentId: "n0a", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 200 }, + }, + { + id: "n0d", + value: 0, + parentId: "n0a", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 200 }, + }, + { + id: "n0e", + value: 0, + parentId: "n1b", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 200 }, + }, + { + id: "n1f", + value: 1, + parentId: "n1b", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 200 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeBinaryTreePruning(input: BinaryTreePruningInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + const pruned = binaryTreePruning(root) as BinaryNode | null; + const result: number[] = []; + function inorder(bNode: BinaryNode | null): void { + if (!bNode) return; + inorder(bNode.left); + result.push(bNode.value); + inorder(bNode.right); + } + inorder(pruned); + return result; +} + +const binaryTreePruningDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BINARY_TREE_PRUNING!, + name: "Binary Tree Pruning", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Remove all subtrees that contain no 1s using post-order traversal — leaves with value 0 collapse upward", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n1" }, + }, + execute: executeBinaryTreePruning, + generateSteps: generateBinaryTreePruningSteps, + educational: binaryTreePruningEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(binaryTreePruningDefinition); diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/sources/BinaryTreePruning.java b/src/algorithms/trees/advanced/binary-tree-pruning/sources/BinaryTreePruning.java new file mode 100644 index 00000000..433ffc85 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/sources/BinaryTreePruning.java @@ -0,0 +1,22 @@ +// Binary Tree Pruning — remove all subtrees containing no 1s (post-order) + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int v) { value = v; } +} + +class BinaryTreePruning { + public BinaryNode binaryTreePruning(BinaryNode root) { + if (root == null) return null; // @step:initialize + + root.left = binaryTreePruning(root.left); // @step:traverse-left + root.right = binaryTreePruning(root.right); // @step:traverse-right + + if (root.value == 0 && root.left == null && root.right == null) { + return null; // @step:detach-node + } + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/sources/binary-tree-pruning.py b/src/algorithms/trees/advanced/binary-tree-pruning/sources/binary-tree-pruning.py new file mode 100644 index 00000000..b733b394 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/sources/binary-tree-pruning.py @@ -0,0 +1,19 @@ +# Binary Tree Pruning — remove all subtrees containing no 1s (post-order) + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def binary_tree_pruning(root) -> object: + if not root: + return None # @step:initialize + + root.left = binary_tree_pruning(root.left) # @step:traverse-left + root.right = binary_tree_pruning(root.right) # @step:traverse-right + + if root.value == 0 and not root.left and not root.right: + return None # @step:detach-node + + return root # @step:visit diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/sources/binary-tree-pruning.ts b/src/algorithms/trees/advanced/binary-tree-pruning/sources/binary-tree-pruning.ts new file mode 100644 index 00000000..955a5d3c --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/sources/binary-tree-pruning.ts @@ -0,0 +1,22 @@ +// Binary Tree Pruning — remove all subtrees containing no 1s (post-order) + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function binaryTreePruning(root: BinaryNode | null): BinaryNode | null { + if (!root) return null; // @step:initialize + + // Post-order: prune children first, then decide current node + root.left = binaryTreePruning(root.left); // @step:traverse-left + root.right = binaryTreePruning(root.right); // @step:traverse-right + + // If this leaf has value 0, prune it + if (root.value === 0 && !root.left && !root.right) { + return null; // @step:detach-node + } + + return root; // @step:visit +} diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/step-generator.test.ts b/src/algorithms/trees/advanced/binary-tree-pruning/step-generator.test.ts new file mode 100644 index 00000000..1f016922 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBinaryTreePruningSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n1", + value: 1, + parentId: null, + leftChildId: "n0a", + rightChildId: "n1b", + state: "default", + position: { x: 200, y: 40 }, + }, + { + id: "n0a", + value: 0, + parentId: "n1", + leftChildId: "n0c", + rightChildId: "n0d", + state: "default", + position: { x: 100, y: 120 }, + }, + { + id: "n1b", + value: 1, + parentId: "n1", + leftChildId: "n0e", + rightChildId: "n1f", + state: "default", + position: { x: 300, y: 120 }, + }, + { + id: "n0c", + value: 0, + parentId: "n0a", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 200 }, + }, + { + id: "n0d", + value: 0, + parentId: "n0a", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 200 }, + }, + { + id: "n0e", + value: 0, + parentId: "n1b", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 200 }, + }, + { + id: "n1f", + value: 1, + parentId: "n1b", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 200 }, + }, +]; + +describe("generateBinaryTreePruningSteps", () => { + it("produces steps for default input", () => { + const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize step", () => { + const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete step", () => { + const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces detach-node steps for pruned nodes", () => { + const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + const detachSteps = steps.filter((step) => step.type === "detach-node"); + expect(detachSteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateBinaryTreePruningSteps({ nodes: defaultNodes, rootId: "n1" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/binary-tree-pruning/step-generator.ts b/src/algorithms/trees/advanced/binary-tree-pruning/step-generator.ts new file mode 100644 index 00000000..4da10628 --- /dev/null +++ b/src/algorithms/trees/advanced/binary-tree-pruning/step-generator.ts @@ -0,0 +1,66 @@ +/** Step generator for Binary Tree Pruning — post-order removal of zero-only subtrees. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const PRUNING_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BINARY_TREE_PRUNING!); + +export interface BinaryTreePruningInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBinaryTreePruningSteps(input: BinaryTreePruningInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new AdvancedTreeTracker(nodes, rootId, PRUNING_LINE_MAP); + + // Mutable copy of the tree for pruning + const nodeMap = new Map(nodes.map((node) => [node.id, { ...node }])); + + tracker.initialize("Binary Tree Pruning", { rootId }); + + /** + * Returns the (possibly pruned) nodeId, or null if this subtree is pruned. + */ + function prune(nodeId: string | null): string | null { + if (!nodeId) return null; + const node = nodeMap.get(nodeId); + if (!node) return null; + + tracker.checkNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + }); + + node.leftChildId = prune(node.leftChildId); + node.rightChildId = prune(node.rightChildId); + + if (node.value === 0 && !node.leftChildId && !node.rightChildId) { + // This node is a leaf with value 0 — prune it + tracker.detachNode(nodeId, { + prunedNode: nodeId, + value: node.value, + reason: "leaf with value 0", + }); + return null; + } + + tracker.markProcessed(nodeId, { + nodeId, + value: node.value, + kept: true, + }); + return nodeId; + } + + const survivingRootId = prune(rootId); + + tracker.complete({ + result: survivingRootId ? `Root ${survivingRootId} kept` : "Entire tree pruned", + survivingRootId, + }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/ExpressionTreeEvaluationPipeline.stories.tsx b/src/algorithms/trees/advanced/expression-tree-evaluation/ExpressionTreeEvaluationPipeline.stories.tsx new file mode 100644 index 00000000..02283ccc --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/ExpressionTreeEvaluationPipeline.stories.tsx @@ -0,0 +1,41 @@ +/** + * Storybook pipeline stories for Expression Tree Evaluation. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateExpressionTreeEvaluationSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateExpressionTreeEvaluationSteps({ expression: "3 4 + 2 * 7 /" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Expression Tree Evaluation", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree construction started */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — expression tree partially built */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — expression evaluated */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/educational.ts b/src/algorithms/trees/advanced/expression-tree-evaluation/educational.ts new file mode 100644 index 00000000..81b67f4c --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/educational.ts @@ -0,0 +1,57 @@ +import type { EducationalContent } from "@/types"; + +export const expressionTreeEvaluationEducational: EducationalContent = { + overview: + "An **Expression Tree** is a binary tree where **leaf nodes** are operands (numbers) and **internal nodes** are operators (`+`, `-`, `*`, `/`). Given a **postfix (Reverse Polish Notation) expression**, the tree is built in one pass using a stack, then evaluated bottom-up using post-order traversal.\n\nExpression trees are the core data structure underlying compilers, interpreters, and formula evaluators.", + + howItWorks: + "**Build phase** (stack-based, one pass over postfix tokens):\n" + + "- If the token is a **number**: push a leaf node.\n" + + "- If the token is an **operator**: pop two nodes (`right`, `left`), create an operator node with them as children, push it.\n\n" + + "**Evaluation phase** (post-order traversal):\n" + + "- If the node is a **leaf**: return its numeric value.\n" + + "- Otherwise: recursively evaluate left and right, apply the operator.\n\n" + + "**Example:** Postfix `3 4 + 2 * 7 /` builds the tree:\n" + + "```\n" + + " /\n" + + " / \\\n" + + " * 7\n" + + " / \\\n" + + " + 2\n" + + " / \\\n" + + " 3 4\n" + + "```\n" + + "Evaluation: `(3+4)=7`, `7*2=14`, `14/7=2`.", + + timeAndSpaceComplexity: + "**Build: `O(n)`** — one pass over n tokens, each push/pop is O(1).\n\n" + + "**Evaluation: `O(n)`** — post-order visits every node once.\n\n" + + "**Space: `O(n)`** — tree has n nodes; stack during build has at most n/2 entries.", + + bestAndWorstCase: + "**Best case:** Simple expression like a single number — O(1).\n\n" + + "**Worst case:** Deeply nested expression like `a b + c + d + ...` — produces a right-skewed tree with O(n) recursion depth.", + + realWorldUses: [ + "**Compilers:** AST (Abstract Syntax Tree) generation and constant folding.", + "**Spreadsheets:** Excel and Google Sheets parse formulas into expression trees.", + "**Database query planners:** Expression trees represent filter predicates.", + "**Calculator apps:** RPN calculators use the postfix-to-tree-to-evaluate pipeline.", + ], + + strengthsAndLimitations: { + strengths: [ + "Clean separation between parsing and evaluation.", + "Easy to extend with new operators or functions.", + "Naturally handles operator precedence via tree structure.", + ], + limitations: [ + "Requires postfix input — infix expressions need a separate precedence parser (shunting-yard).", + "Recursive evaluation can overflow the stack for very deep expressions.", + "Not suitable for symbolic simplification without additional traversal logic.", + ], + }, + + whenToUseIt: + "Use an expression tree when you need to parse, transform, or evaluate a mathematical expression with operator precedence. For simple single-pass evaluation of postfix expressions, a stack-based direct evaluator is simpler and requires no tree construction.", +}; diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/expression-tree-evaluation.test.ts b/src/algorithms/trees/advanced/expression-tree-evaluation/expression-tree-evaluation.test.ts new file mode 100644 index 00000000..fc500d45 --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/expression-tree-evaluation.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { expressionTreeEvaluation } from "./sources/expression-tree-evaluation.ts?fn"; + +describe("expressionTreeEvaluation", () => { + it("evaluates default expression: 3 4 + 2 * 7 / = 2", () => { + expect(expressionTreeEvaluation("3 4 + 2 * 7 /")).toBe(2); + }); + + it("evaluates simple addition", () => { + expect(expressionTreeEvaluation("3 4 +")).toBe(7); + }); + + it("evaluates simple multiplication", () => { + expect(expressionTreeEvaluation("5 6 *")).toBe(30); + }); + + it("evaluates subtraction", () => { + expect(expressionTreeEvaluation("10 4 -")).toBe(6); + }); + + it("evaluates integer division (truncated)", () => { + expect(expressionTreeEvaluation("7 2 /")).toBe(3); + }); + + it("evaluates nested expression: 2 3 * 4 5 * +", () => { + // (2*3) + (4*5) = 6 + 20 = 26 + expect(expressionTreeEvaluation("2 3 * 4 5 * +")).toBe(26); + }); + + it("handles single number", () => { + expect(expressionTreeEvaluation("42")).toBe(42); + }); +}); diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/index.ts b/src/algorithms/trees/advanced/expression-tree-evaluation/index.ts new file mode 100644 index 00000000..1ceb1d0d --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/index.ts @@ -0,0 +1,37 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { expressionTreeEvaluation } from "./sources/expression-tree-evaluation.ts?fn"; +import { generateExpressionTreeEvaluationSteps } from "./step-generator"; +import type { ExpressionTreeEvaluationInput } from "./step-generator"; +import { expressionTreeEvaluationEducational } from "./educational"; + +import typescriptSource from "./sources/expression-tree-evaluation.ts?raw"; +import pythonSource from "./sources/expression-tree-evaluation.py?raw"; +import javaSource from "./sources/ExpressionTreeEvaluation.java?raw"; + +function executeExpressionTreeEvaluation(input: ExpressionTreeEvaluationInput): number { + return expressionTreeEvaluation(input.expression) as number; +} + +const expressionTreeEvaluationDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.EXPRESSION_TREE_EVALUATION!, + name: "Expression Tree Evaluation", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Build an expression tree from a postfix expression then evaluate bottom-up — leaves are operands, internal nodes are operators", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { expression: "3 4 + 2 * 7 /" }, + }, + execute: executeExpressionTreeEvaluation, + generateSteps: generateExpressionTreeEvaluationSteps, + educational: expressionTreeEvaluationEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(expressionTreeEvaluationDefinition); diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/sources/ExpressionTreeEvaluation.java b/src/algorithms/trees/advanced/expression-tree-evaluation/sources/ExpressionTreeEvaluation.java new file mode 100644 index 00000000..c544c938 --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/sources/ExpressionTreeEvaluation.java @@ -0,0 +1,46 @@ +// Expression Tree Evaluation — build expression tree from postfix, then evaluate +import java.util.Stack; + +class ExprNode { + String token; + ExprNode left, right; + ExprNode(String t) { token = t; } +} + +class ExpressionTreeEvaluation { + private boolean isOperator(String token) { + return token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/"); + } + + private int evaluate(ExprNode node) { + if (node == null) return 0; + if (!isOperator(node.token)) return Integer.parseInt(node.token); // @step:visit + int leftVal = evaluate(node.left); // @step:traverse-left + int rightVal = evaluate(node.right); // @step:traverse-right + switch (node.token) { + case "+": return leftVal + rightVal; // @step:visit + case "-": return leftVal - rightVal; // @step:visit + case "*": return leftVal * rightVal; // @step:visit + case "/": return leftVal / rightVal; // @step:visit + } + return 0; + } + + public int expressionTreeEvaluation(String expression) { + String[] tokens = expression.trim().split("\\s+"); // @step:initialize + Stack stack = new Stack<>(); // @step:initialize + for (String token : tokens) { + if (isOperator(token)) { + ExprNode rightOperand = stack.pop(); // @step:connect-child + ExprNode leftOperand = stack.pop(); // @step:connect-child + ExprNode newNode = new ExprNode(token); // @step:build-node + newNode.left = leftOperand; + newNode.right = rightOperand; + stack.push(newNode); // @step:build-node + } else { + stack.push(new ExprNode(token)); // @step:build-node + } + } + return evaluate(stack.peek()); // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/sources/expression-tree-evaluation.py b/src/algorithms/trees/advanced/expression-tree-evaluation/sources/expression-tree-evaluation.py new file mode 100644 index 00000000..3e89921c --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/sources/expression-tree-evaluation.py @@ -0,0 +1,42 @@ +# Expression Tree Evaluation — build expression tree from postfix, then evaluate + +class ExprNode: + def __init__(self, token, left=None, right=None): + self.token = token + self.left = left + self.right = right + +OPERATORS = {'+', '-', '*', '/'} + +def expression_tree_evaluation(expression: str) -> int: + tokens = expression.strip().split() # @step:initialize + stack = [] # @step:initialize + + for token in tokens: + if token in OPERATORS: + right_operand = stack.pop() # @step:connect-child + left_operand = stack.pop() # @step:connect-child + stack.append(ExprNode(token, left_operand, right_operand)) # @step:build-node + else: + stack.append(ExprNode(token)) # @step:build-node + + root = stack[0] if stack else None + + def evaluate(node): + if not node: + return 0 + if not node.left and not node.right: + return int(node.token) # @step:visit + left_val = evaluate(node.left) # @step:traverse-left + right_val = evaluate(node.right) # @step:traverse-right + if node.token == "+": + return left_val + right_val # @step:visit + elif node.token == "-": + return left_val - right_val # @step:visit + elif node.token == "*": + return left_val * right_val # @step:visit + elif node.token == "/": + return int(left_val / right_val) # @step:visit + return 0 + + return evaluate(root) # @step:complete diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/sources/expression-tree-evaluation.ts b/src/algorithms/trees/advanced/expression-tree-evaluation/sources/expression-tree-evaluation.ts new file mode 100644 index 00000000..d7b8f9c4 --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/sources/expression-tree-evaluation.ts @@ -0,0 +1,47 @@ +// Expression Tree Evaluation — build expression tree from postfix, then evaluate + +interface ExprNode { + token: string; + left: ExprNode | null; + right: ExprNode | null; +} + +function expressionTreeEvaluation(expression: string): number { + const tokens = expression.trim().split(/\s+/); // @step:initialize + const stack: ExprNode[] = []; // @step:initialize + + for (const token of tokens) { + if (!isNaN(Number(token))) { + stack.push({ token, left: null, right: null }); // @step:build-node + } else { + const rightOperand = stack.pop()!; // @step:connect-child + const leftOperand = stack.pop()!; // @step:connect-child + stack.push({ token, left: leftOperand, right: rightOperand }); // @step:build-node + } + } + + const root = stack[0] ?? null; + + function evaluate(node: ExprNode | null): number { + if (!node) return 0; + if (!node.left && !node.right) return Number(node.token); // @step:visit + + const leftValue = evaluate(node.left); // @step:traverse-left + const rightValue = evaluate(node.right); // @step:traverse-right + + switch (node.token) { + case "+": + return leftValue + rightValue; // @step:visit + case "-": + return leftValue - rightValue; // @step:visit + case "*": + return leftValue * rightValue; // @step:visit + case "/": + return Math.trunc(leftValue / rightValue); // @step:visit + default: + return 0; + } + } + + return evaluate(root); // @step:complete +} diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/step-generator.test.ts b/src/algorithms/trees/advanced/expression-tree-evaluation/step-generator.test.ts new file mode 100644 index 00000000..597bb10b --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/step-generator.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { generateExpressionTreeEvaluationSteps } from "./step-generator"; + +describe("generateExpressionTreeEvaluationSteps", () => { + const defaultInput = { expression: "3 4 + 2 * 7 /" }; + + it("produces steps for default input", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize step", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete step", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces build-node steps for each token", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + // 5 operands (3,4,2,7) + 3 operators (+,*,/) = 7 tokens + expect(buildSteps.length).toBe(7); + }); + + it("final complete step has result 2", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["result"]).toBe(2); + }); + + it("has incrementing step indices", () => { + const steps = generateExpressionTreeEvaluationSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/expression-tree-evaluation/step-generator.ts b/src/algorithms/trees/advanced/expression-tree-evaluation/step-generator.ts new file mode 100644 index 00000000..5d9fe8ba --- /dev/null +++ b/src/algorithms/trees/advanced/expression-tree-evaluation/step-generator.ts @@ -0,0 +1,174 @@ +/** Step generator for Expression Tree Evaluation — builds tree from postfix then evaluates. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const EXPR_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.EXPRESSION_TREE_EVALUATION!); + +export interface ExpressionTreeEvaluationInput { + expression: string; +} + +interface ExprInternalNode { + id: string; + token: string; + value: number; + leftId: string | null; + rightId: string | null; + parentId: string | null; +} + +const OPERATORS = new Set(["+", "-", "*", "/"]); + +function isOperator(token: string): boolean { + return OPERATORS.has(token); +} + +export function generateExpressionTreeEvaluationSteps( + input: ExpressionTreeEvaluationInput, +): ExecutionStep[] { + const { expression } = input; + const tokens = expression.trim().split(/\s+/); + const exprNodes = new Map(); + let nodeCounter = 0; + + const tracker = new AdvancedTreeTracker([], "eroot", EXPR_LINE_MAP); + tracker.initialize("Expression Tree Evaluation", { expression }); + + function makeId(): string { + nodeCounter += 1; + return `e${nodeCounter}`; + } + + function toTreeNodes(): TreeNode[] { + const rootNode = Array.from(exprNodes.values()).find((node) => !node.parentId); + const rootId = rootNode?.id ?? null; + + const posMap = new Map(); + function assignPos(nodeId: string | null, depth: number, minX: number, maxX: number): void { + if (!nodeId) return; + const node = exprNodes.get(nodeId); + if (!node) return; + const midX = (minX + maxX) / 2; + posMap.set(nodeId, { x: midX, y: depth * 80 + 40 }); + assignPos(node.leftId, depth + 1, minX, midX); + assignPos(node.rightId, depth + 1, midX, maxX); + } + assignPos(rootId, 0, 20, 480); + + return Array.from(exprNodes.values()).map((node) => ({ + id: node.id, + value: node.value, + parentId: node.parentId, + leftChildId: node.leftId, + rightChildId: node.rightId, + childrenIds: [node.leftId, node.rightId].filter((id): id is string => id !== null), + state: "default" as const, + position: posMap.get(node.id) ?? { x: 200, y: 40 }, + })); + } + + // Build phase: process postfix tokens + const stack: string[] = []; + + for (const token of tokens) { + if (!isOperator(token)) { + // Operand — create leaf + const leafId = makeId(); + exprNodes.set(leafId, { + id: leafId, + token, + value: Number(token), + leftId: null, + rightId: null, + parentId: null, + }); + stack.push(leafId); + + tracker.updateNodes(toTreeNodes(), leafId); + tracker.buildNode(leafId, Number(token), { token, isLeaf: true }); + } else { + // Operator — pop two operands, create internal node + const rightId = stack.pop()!; + const leftId = stack.pop()!; + + const opId = makeId(); + exprNodes.set(opId, { + id: opId, + token, + value: 0, // value computed in eval phase + leftId, + rightId, + parentId: null, + }); + exprNodes.get(leftId)!.parentId = opId; + exprNodes.get(rightId)!.parentId = opId; + + stack.push(opId); + + tracker.updateNodes(toTreeNodes(), opId); + tracker.buildNode(opId, 0, { token, isOperator: true }); + tracker.connectChild(opId, leftId, { side: "left" }); + tracker.connectChild(opId, rightId, { side: "right" }); + } + } + + const rootId = stack[0] ?? null; + if (rootId) { + tracker.updateNodes(toTreeNodes(), rootId); + } + + // Evaluation phase: post-order traversal + function evaluate(nodeId: string | null): number { + if (!nodeId) return 0; + const node = exprNodes.get(nodeId); + if (!node) return 0; + + if (!isOperator(node.token)) { + tracker.visitNode(nodeId, node.value, { + token: node.token, + value: node.value, + isLeaf: true, + }); + return node.value; + } + + const leftValue = evaluate(node.leftId); + const rightValue = evaluate(node.rightId); + + let result = 0; + switch (node.token) { + case "+": + result = leftValue + rightValue; + break; + case "-": + result = leftValue - rightValue; + break; + case "*": + result = leftValue * rightValue; + break; + case "/": + result = Math.trunc(leftValue / rightValue); + break; + } + + node.value = result; + tracker.updateNodes(toTreeNodes(), nodeId); + tracker.visitNode(nodeId, result, { + operator: node.token, + leftValue, + rightValue, + result, + }); + + return result; + } + + const finalResult = evaluate(rootId); + + tracker.complete({ expression, result: finalResult }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/HuffmanCodingTreePipeline.stories.tsx b/src/algorithms/trees/advanced/huffman-coding-tree/HuffmanCodingTreePipeline.stories.tsx new file mode 100644 index 00000000..b233165a --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/HuffmanCodingTreePipeline.stories.tsx @@ -0,0 +1,50 @@ +/** + * Storybook pipeline stories for Huffman Coding Tree. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateHuffmanCodingTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateHuffmanCodingTreeSteps({ + frequencies: [ + { char: "a", freq: 5 }, + { char: "b", freq: 9 }, + { char: "c", freq: 12 }, + { char: "d", freq: 13 }, + { char: "e", freq: 16 }, + { char: "f", freq: 45 }, + ], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Huffman Coding Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — leaf nodes created from frequencies */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — tree partially built */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — Huffman tree built and codes assigned */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/educational.ts b/src/algorithms/trees/advanced/huffman-coding-tree/educational.ts new file mode 100644 index 00000000..9c97a4f2 --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/educational.ts @@ -0,0 +1,48 @@ +import type { EducationalContent } from "@/types"; + +export const huffmanCodingTreeEducational: EducationalContent = { + overview: + "**Huffman Coding** is a greedy algorithm that assigns **variable-length, prefix-free binary codes** to characters based on their frequencies. More frequent characters receive shorter codes — minimizing the total number of bits needed to encode a message.\n\nDeveloped by David Huffman in 1952, it is the foundation of compression formats like ZIP, gzip, and JPEG.", + + howItWorks: + "1. **Create leaf nodes** for each character, weighted by frequency.\n" + + "2. **Build a min-heap** (priority queue) sorted by frequency.\n" + + "3. **Repeat until one node remains:**\n" + + " - Pop the two nodes with the lowest frequencies (`left`, `right`).\n" + + " - Create an internal node with `freq = left.freq + right.freq`.\n" + + " - Push the internal node back into the heap.\n" + + "4. The last remaining node is the **Huffman tree root**.\n" + + "5. **Assign codes:** Traverse the tree, appending `0` for left edges and `1` for right edges until reaching a leaf.\n\n" + + "**Example:** For `{a:5, b:9, c:12, d:13, e:16, f:45}`, `f` gets code `0` (shortest), while `a` gets `1100` (longest).", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n log n)`** — each of n iterations pops and pushes from a heap in O(log n).\n\n" + + "**Space Complexity: `O(n)`** — the Huffman tree has 2n−1 nodes for n characters.", + + bestAndWorstCase: + "**Best case:** All characters have equal frequency — all codes have the same length ⌈log₂ n⌉.\n\n" + + "**Worst case:** Frequencies follow Fibonacci sequence — produces the most unbalanced tree and the longest worst-case code.", + + realWorldUses: [ + "**File compression:** Used in Deflate (ZIP, gzip, PNG) as the entropy coding step.", + "**Image compression:** JPEG uses Huffman coding for DCT coefficient compression.", + "**Data transmission:** Reduce bandwidth by encoding frequent patterns with fewer bits.", + "**Programming language tokenizers:** Assign shorter codes to common tokens.", + ], + + strengthsAndLimitations: { + strengths: [ + "Provably optimal prefix-free code — no other prefix-free code achieves better compression.", + "Produces unique, deterministic codes for a given frequency distribution.", + "Decompression is O(n) — just follow the tree path bit by bit.", + ], + limitations: [ + "Requires knowing character frequencies in advance — two-pass algorithm for files.", + "Performance degrades when character distribution is nearly uniform (approaches fixed-length encoding).", + "Arithmetic coding can beat Huffman by fractional bits per character.", + ], + }, + + whenToUseIt: + "Use Huffman coding when you need to compress text or data with known character frequencies and want the optimal prefix-free encoding. For adaptive (online) compression without prior frequency knowledge, use adaptive Huffman or arithmetic coding.", +}; diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/huffman-coding-tree.test.ts b/src/algorithms/trees/advanced/huffman-coding-tree/huffman-coding-tree.test.ts new file mode 100644 index 00000000..a9021e55 --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/huffman-coding-tree.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { huffmanCodingTree } from "./sources/huffman-coding-tree.ts?fn"; + +describe("huffmanCodingTree", () => { + const defaultFreqs = [ + { char: "a", freq: 5 }, + { char: "b", freq: 9 }, + { char: "c", freq: 12 }, + { char: "d", freq: 13 }, + { char: "e", freq: 16 }, + { char: "f", freq: 45 }, + ]; + + it("produces encodings for all characters", () => { + const result = huffmanCodingTree(defaultFreqs) as Record; + const chars = defaultFreqs.map(({ char }) => char); + for (const char of chars) { + expect(result[char]).toBeDefined(); + expect(typeof result[char]).toBe("string"); + } + }); + + it("produces valid binary strings (only 0s and 1s)", () => { + const result = huffmanCodingTree(defaultFreqs) as Record; + for (const encoding of Object.values(result)) { + expect(/^[01]+$/.test(encoding)).toBe(true); + } + }); + + it("most frequent character gets shortest code", () => { + const result = huffmanCodingTree(defaultFreqs) as Record; + const fEncoding = result["f"]!; + const otherLengths = Object.entries(result) + .filter(([char]) => char !== "f") + .map(([, enc]) => enc.length); + expect(fEncoding.length).toBeLessThanOrEqual(Math.min(...otherLengths)); + }); + + it("all codes are prefix-free", () => { + const result = huffmanCodingTree(defaultFreqs) as Record; + const codes = Object.values(result); + for (let codeIdx = 0; codeIdx < codes.length; codeIdx++) { + for (let otherIdx = 0; otherIdx < codes.length; otherIdx++) { + if (codeIdx !== otherIdx) { + const codeA = codes[codeIdx]!; + const codeB = codes[otherIdx]!; + expect(codeA.startsWith(codeB) && codeA !== codeB).toBe(false); + } + } + } + }); + + it("handles single character", () => { + const result = huffmanCodingTree([{ char: "x", freq: 10 }]) as Record; + expect(result["x"]).toBe("0"); + }); +}); diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/index.ts b/src/algorithms/trees/advanced/huffman-coding-tree/index.ts new file mode 100644 index 00000000..6258f0aa --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/index.ts @@ -0,0 +1,46 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { huffmanCodingTree } from "./sources/huffman-coding-tree.ts?fn"; +import { generateHuffmanCodingTreeSteps } from "./step-generator"; +import type { HuffmanCodingTreeInput } from "./step-generator"; +import { huffmanCodingTreeEducational } from "./educational"; + +import typescriptSource from "./sources/huffman-coding-tree.ts?raw"; +import pythonSource from "./sources/huffman-coding-tree.py?raw"; +import javaSource from "./sources/HuffmanCodingTree.java?raw"; + +function executeHuffmanCodingTree(input: HuffmanCodingTreeInput): Record { + return huffmanCodingTree(input.frequencies) as Record; +} + +const huffmanCodingTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.HUFFMAN_CODING_TREE!, + name: "Huffman Coding Tree", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Build a Huffman tree from character frequencies to produce optimal variable-length prefix-free binary encodings", + timeComplexity: { best: "O(n log n)", average: "O(n log n)", worst: "O(n log n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + frequencies: [ + { char: "a", freq: 5 }, + { char: "b", freq: 9 }, + { char: "c", freq: 12 }, + { char: "d", freq: 13 }, + { char: "e", freq: 16 }, + { char: "f", freq: 45 }, + ], + }, + }, + execute: executeHuffmanCodingTree, + generateSteps: generateHuffmanCodingTreeSteps, + educational: huffmanCodingTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(huffmanCodingTreeDefinition); diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/sources/HuffmanCodingTree.java b/src/algorithms/trees/advanced/huffman-coding-tree/sources/HuffmanCodingTree.java new file mode 100644 index 00000000..15000bb6 --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/sources/HuffmanCodingTree.java @@ -0,0 +1,42 @@ +// Huffman Coding Tree — build optimal prefix-free encoding from character frequencies +import java.util.*; + +class HuffmanNode implements Comparable { + int freq; + Character ch; + HuffmanNode left, right; + HuffmanNode(int freq, Character ch) { this.freq = freq; this.ch = ch; } + public int compareTo(HuffmanNode other) { return this.freq - other.freq; } +} + +class HuffmanCodingTree { + private Map encodings = new HashMap<>(); + + private void generateCodes(HuffmanNode node, String code) { + if (node == null) return; + if (node.ch != null) { + encodings.put(node.ch, code.isEmpty() ? "0" : code); // @step:encode-char + return; + } + generateCodes(node.left, code + "0"); // @step:traverse-left + generateCodes(node.right, code + "1"); // @step:traverse-right + } + + public Map huffmanCodingTree(char[] chars, int[] freqs) { + PriorityQueue pq = new PriorityQueue<>(); // @step:initialize + for (int idx = 0; idx < chars.length; idx++) { + pq.add(new HuffmanNode(freqs[idx], chars[idx])); // @step:select-min-freq + } + while (pq.size() > 1) { + HuffmanNode left = pq.poll(); // @step:select-min-freq + HuffmanNode right = pq.poll(); // @step:select-min-freq + HuffmanNode merged = new HuffmanNode(left.freq + right.freq, null); // @step:build-node + merged.left = left; + merged.right = right; + pq.add(merged); // @step:build-node + } + encodings.clear(); + generateCodes(pq.poll(), ""); + return encodings; // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/sources/huffman-coding-tree.py b/src/algorithms/trees/advanced/huffman-coding-tree/sources/huffman-coding-tree.py new file mode 100644 index 00000000..4bd58153 --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/sources/huffman-coding-tree.py @@ -0,0 +1,37 @@ +# Huffman Coding Tree — build optimal prefix-free encoding from character frequencies +import heapq + +class HuffmanNode: + def __init__(self, freq, char=None, left=None, right=None): + self.freq = freq + self.char = char + self.left = left + self.right = right + + def __lt__(self, other): + return self.freq < other.freq + +def huffman_coding_tree(frequencies: list) -> dict: + min_heap = [HuffmanNode(item['freq'], item['char']) for item in frequencies] # @step:initialize + heapq.heapify(min_heap) # @step:select-min-freq + + while len(min_heap) > 1: + left = heapq.heappop(min_heap) # @step:select-min-freq + right = heapq.heappop(min_heap) # @step:select-min-freq + merged = HuffmanNode(left.freq + right.freq, None, left, right) # @step:build-node + heapq.heappush(min_heap, merged) # @step:build-node + + root = min_heap[0] if min_heap else None + encodings = {} + + def generate_codes(node, code): + if not node: + return + if node.char is not None: + encodings[node.char] = code or "0" # @step:encode-char + return + generate_codes(node.left, code + "0") # @step:traverse-left + generate_codes(node.right, code + "1") # @step:traverse-right + + generate_codes(root, "") + return encodings # @step:complete diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/sources/huffman-coding-tree.ts b/src/algorithms/trees/advanced/huffman-coding-tree/sources/huffman-coding-tree.ts new file mode 100644 index 00000000..8b3e16fc --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/sources/huffman-coding-tree.ts @@ -0,0 +1,56 @@ +// Huffman Coding Tree — build optimal prefix-free encoding from character frequencies + +interface HuffmanNode { + freq: number; + char: string | null; + left: HuffmanNode | null; + right: HuffmanNode | null; +} + +function huffmanCodingTree(frequencies: { char: string; freq: number }[]): Record { + // Build a min-heap (priority queue) from frequencies + const minHeap: HuffmanNode[] = frequencies.map(({ char, freq }) => ({ + freq, + char, + left: null, + right: null, + })); // @step:initialize + + // Sort ascending to simulate a min-heap + minHeap.sort((nodeA, nodeB) => nodeA.freq - nodeB.freq); // @step:select-min-freq + + while (minHeap.length > 1) { + // Extract two minimums + const leftNode = minHeap.shift()!; // @step:select-min-freq + const rightNode = minHeap.shift()!; // @step:select-min-freq + + // Merge into a new internal node + const merged: HuffmanNode = { + freq: leftNode.freq + rightNode.freq, + char: null, + left: leftNode, + right: rightNode, + }; // @step:build-node + + // Re-insert maintaining sorted order + const insertPos = minHeap.findIndex((node) => node.freq > merged.freq); + if (insertPos === -1) minHeap.push(merged); + else minHeap.splice(insertPos, 0, merged); // @step:build-node + } + + const huffmanRoot = minHeap[0] ?? null; + const encodings: Record = {}; + + function generateCodes(node: HuffmanNode | null, code: string): void { + if (!node) return; + if (node.char !== null) { + encodings[node.char] = code || "0"; // @step:encode-char + return; + } + generateCodes(node.left, code + "0"); // @step:traverse-left + generateCodes(node.right, code + "1"); // @step:traverse-right + } + + generateCodes(huffmanRoot, ""); + return encodings; // @step:complete +} diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/step-generator.test.ts b/src/algorithms/trees/advanced/huffman-coding-tree/step-generator.test.ts new file mode 100644 index 00000000..9658f512 --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/step-generator.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { generateHuffmanCodingTreeSteps } from "./step-generator"; + +describe("generateHuffmanCodingTreeSteps", () => { + const defaultInput = { + frequencies: [ + { char: "a", freq: 5 }, + { char: "b", freq: 9 }, + { char: "c", freq: 12 }, + { char: "d", freq: 13 }, + { char: "e", freq: 16 }, + { char: "f", freq: 45 }, + ], + }; + + it("produces steps for default input", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize step", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete step", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces build-node steps during tree construction", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBeGreaterThan(0); + }); + + it("produces encode-char steps for each character", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + const encodeSteps = steps.filter((step) => step.type === "encode-char"); + expect(encodeSteps.length).toBe(defaultInput.frequencies.length); + }); + + it("has incrementing step indices", () => { + const steps = generateHuffmanCodingTreeSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/huffman-coding-tree/step-generator.ts b/src/algorithms/trees/advanced/huffman-coding-tree/step-generator.ts new file mode 100644 index 00000000..401c0489 --- /dev/null +++ b/src/algorithms/trees/advanced/huffman-coding-tree/step-generator.ts @@ -0,0 +1,173 @@ +/** Step generator for Huffman Coding Tree — builds tree bottom-up from frequencies. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const HUFFMAN_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.HUFFMAN_CODING_TREE!); + +export interface HuffmanCodingTreeInput { + frequencies: { char: string; freq: number }[]; +} + +interface HuffmanInternalNode { + id: string; + freq: number; + char: string | null; + leftId: string | null; + rightId: string | null; + parentId: string | null; +} + +export function generateHuffmanCodingTreeSteps(input: HuffmanCodingTreeInput): ExecutionStep[] { + const { frequencies } = input; + const allNodes = new Map(); + let nodeCounter = 0; + + const tracker = new AdvancedTreeTracker([], "hroot", HUFFMAN_LINE_MAP); + tracker.initialize("Huffman Coding Tree", { frequencies }); + + function makeId(): string { + nodeCounter += 1; + return `h${nodeCounter}`; + } + + function toTreeNodes(): TreeNode[] { + // Layout: place leaf nodes and internal nodes in a tree + const posMap = new Map(); + // Find root (node with no parent) + const rootNode = Array.from(allNodes.values()).find((node) => !node.parentId); + const rootId = rootNode?.id ?? null; + + function assignPos(nodeId: string | null, depth: number, minX: number, maxX: number): void { + if (!nodeId) return; + const node = allNodes.get(nodeId); + if (!node) return; + const midX = (minX + maxX) / 2; + posMap.set(nodeId, { x: midX, y: depth * 80 + 40 }); + assignPos(node.leftId, depth + 1, minX, midX); + assignPos(node.rightId, depth + 1, midX, maxX); + } + + assignPos(rootId, 0, 20, 480); + + return Array.from(allNodes.values()).map((node) => ({ + id: node.id, + value: node.freq, + parentId: node.parentId, + leftChildId: node.leftId, + rightChildId: node.rightId, + childrenIds: [node.leftId, node.rightId].filter((id): id is string => id !== null), + state: "default" as const, + position: posMap.get(node.id) ?? { x: 200, y: 40 }, + })); + } + + // Initialize leaf nodes + const minHeap: HuffmanInternalNode[] = frequencies.map(({ char, freq }) => { + const newId = makeId(); + const leafNode: HuffmanInternalNode = { + id: newId, + freq, + char, + leftId: null, + rightId: null, + parentId: null, + }; + allNodes.set(newId, leafNode); + return leafNode; + }); + + // Sort ascending (min-heap simulation) + minHeap.sort((nodeA, nodeB) => nodeA.freq - nodeB.freq); + + tracker.updateNodes(toTreeNodes(), minHeap[0]?.id ?? "h1"); + + // Select each leaf node to show initial freq values + for (const leafNode of minHeap) { + tracker.selectMinFreq(leafNode.id, leafNode.freq, { + char: leafNode.char, + freq: leafNode.freq, + nodeId: leafNode.id, + }); + } + + // Build Huffman tree + while (minHeap.length > 1) { + const leftNode = minHeap.shift()!; + const rightNode = minHeap.shift()!; + + tracker.selectMinFreq(leftNode.id, leftNode.freq, { + selected: "left", + char: leftNode.char, + freq: leftNode.freq, + }); + tracker.selectMinFreq(rightNode.id, rightNode.freq, { + selected: "right", + char: rightNode.char, + freq: rightNode.freq, + }); + + const mergedId = makeId(); + const mergedNode: HuffmanInternalNode = { + id: mergedId, + freq: leftNode.freq + rightNode.freq, + char: null, + leftId: leftNode.id, + rightId: rightNode.id, + parentId: null, + }; + leftNode.parentId = mergedId; + rightNode.parentId = mergedId; + allNodes.set(mergedId, mergedNode); + + tracker.updateNodes(toTreeNodes(), mergedId); + tracker.buildNode(mergedId, mergedNode.freq, { + mergedFreq: mergedNode.freq, + leftFreq: leftNode.freq, + rightFreq: rightNode.freq, + }); + tracker.connectChild(mergedId, leftNode.id, { side: "left", childFreq: leftNode.freq }); + tracker.connectChild(mergedId, rightNode.id, { side: "right", childFreq: rightNode.freq }); + + // Re-insert in sorted order + const insertPos = minHeap.findIndex((node) => node.freq > mergedNode.freq); + if (insertPos === -1) minHeap.push(mergedNode); + else minHeap.splice(insertPos, 0, mergedNode); + } + + // Generate codes + const huffmanRoot = minHeap[0]; + if (huffmanRoot) { + tracker.updateNodes(toTreeNodes(), huffmanRoot.id); + } + + function generateCodes(nodeId: string | null, code: string): void { + if (!nodeId) return; + const node = allNodes.get(nodeId); + if (!node) return; + + if (node.char !== null) { + const encoding = code || "0"; + tracker.encodeChar(nodeId, node.char, encoding, { + char: node.char, + encoding, + freq: node.freq, + }); + return; + } + + generateCodes(node.leftId, code + "0"); + generateCodes(node.rightId, code + "1"); + } + + generateCodes(huffmanRoot?.id ?? null, ""); + + tracker.complete({ + result: `Huffman tree built for ${frequencies.length} characters`, + rootFreq: huffmanRoot?.freq, + }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/NAryTreeTraversalPipeline.stories.tsx b/src/algorithms/trees/advanced/n-ary-tree-traversal/NAryTreeTraversalPipeline.stories.tsx new file mode 100644 index 00000000..cb73dc53 --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/NAryTreeTraversalPipeline.stories.tsx @@ -0,0 +1,144 @@ +/** + * Storybook pipeline stories for N-ary Tree Traversal. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeNode, TreeVisualState } from "@/types"; +import { generateNAryTreeTraversalSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "r", + value: 1, + parentId: null, + leftChildId: null, + rightChildId: null, + childrenIds: ["c1", "c2", "c3"], + state: "default", + position: { x: 240, y: 40 }, + }, + { + id: "c1", + value: 3, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g1", "g2"], + state: "default", + position: { x: 100, y: 120 }, + }, + { + id: "c2", + value: 2, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g3", "g4"], + state: "default", + position: { x: 240, y: 120 }, + }, + { + id: "c3", + value: 4, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g5", "g6"], + state: "default", + position: { x: 380, y: 120 }, + }, + { + id: "g1", + value: 5, + parentId: "c1", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 50, y: 200 }, + }, + { + id: "g2", + value: 6, + parentId: "c1", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 130, y: 200 }, + }, + { + id: "g3", + value: 7, + parentId: "c2", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 200, y: 200 }, + }, + { + id: "g4", + value: 8, + parentId: "c2", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 280, y: 200 }, + }, + { + id: "g5", + value: 9, + parentId: "c3", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 340, y: 200 }, + }, + { + id: "g6", + value: 10, + parentId: "c3", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 420, y: 200 }, + }, +]; + +const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + +const meta: Meta = { + title: "Algorithm Pipelines/N-ary Tree Traversal", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — N-ary tree before traversal */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — some nodes visited */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — all nodes visited in preorder */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/educational.ts b/src/algorithms/trees/advanced/n-ary-tree-traversal/educational.ts new file mode 100644 index 00000000..a459777b --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const nAryTreeTraversalEducational: EducationalContent = { + overview: + "An **N-ary tree** is a tree where each node can have any number of children (not just left and right). **Preorder traversal** visits a node **before** recursing into its children — the root is always visited first, then children left-to-right.\n\nN-ary trees model hierarchical data like file systems, HTML/XML DOM trees, and organizational charts.", + + howItWorks: + "The preorder traversal uses a simple recursive pattern:\n\n" + + "1. **Visit** the current node (record its value).\n" + + "2. **Recurse** into each child from left to right.\n\n" + + "```\n" + + "function preorder(node):\n" + + " visit(node)\n" + + " for child in node.children:\n" + + " preorder(child)\n" + + "```\n\n" + + "For a 3-ary tree of 9 nodes (root with 3 children, each having 2 children), the preorder sequence is: **root → child₁ → grandchild₁₁ → grandchild₁₂ → child₂ → ...**", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)`** — recursion depth equals tree height `h`. For balanced n-ary trees, `h = O(log_k n)` where `k` is the branching factor.", + + bestAndWorstCase: + "**Best case:** Wide, shallow tree — minimal call stack depth.\n\n" + + "**Worst case:** Degenerate chain (each node has one child) — call stack depth `O(n)`.", + + realWorldUses: [ + "**File system traversal:** `find` and `ls -R` use preorder DFS to list directories before contents.", + "**XML/HTML parsing:** DOM tree traversal visits parent elements before children.", + "**Syntax trees:** Compilers use preorder traversal to serialize abstract syntax trees.", + "**Organizational charts:** Preorder traversal lists managers before their reports.", + ], + + strengthsAndLimitations: { + strengths: [ + "Generalizes binary tree traversal to arbitrary branching factors.", + "Simple recursive implementation — same pattern as binary preorder.", + "Naturally produces a prefix-order serialization of the tree.", + ], + limitations: [ + "Recursive — stack overflow on very deep trees.", + "Iterative version requires an explicit stack with reversed child ordering.", + "Does not produce sorted output (unlike BST inorder traversal).", + ], + }, + + whenToUseIt: + "Use n-ary preorder traversal when you need to process parent nodes before their children — for example, to serialize a tree, print directory listings, or copy a tree structure. For level-by-level processing, use BFS instead.", +}; diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/index.ts b/src/algorithms/trees/advanced/n-ary-tree-traversal/index.ts new file mode 100644 index 00000000..ded559f7 --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/index.ts @@ -0,0 +1,159 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { nAryTreeTraversal } from "./sources/n-ary-tree-traversal.ts?fn"; +import { generateNAryTreeTraversalSteps } from "./step-generator"; +import type { NAryTreeTraversalInput } from "./step-generator"; +import { nAryTreeTraversalEducational } from "./educational"; + +import typescriptSource from "./sources/n-ary-tree-traversal.ts?raw"; +import pythonSource from "./sources/n-ary-tree-traversal.py?raw"; +import javaSource from "./sources/NAryTreeTraversal.java?raw"; + +/** A 3-ary tree: root has 3 children, each has 2 children (9 nodes total) */ +const defaultNodes: TreeNode[] = [ + { + id: "r", + value: 1, + parentId: null, + leftChildId: null, + rightChildId: null, + childrenIds: ["c1", "c2", "c3"], + state: "default", + position: { x: 240, y: 40 }, + }, + { + id: "c1", + value: 3, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g1", "g2"], + state: "default", + position: { x: 100, y: 120 }, + }, + { + id: "c2", + value: 2, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g3", "g4"], + state: "default", + position: { x: 240, y: 120 }, + }, + { + id: "c3", + value: 4, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g5", "g6"], + state: "default", + position: { x: 380, y: 120 }, + }, + { + id: "g1", + value: 5, + parentId: "c1", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 50, y: 200 }, + }, + { + id: "g2", + value: 6, + parentId: "c1", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 130, y: 200 }, + }, + { + id: "g3", + value: 7, + parentId: "c2", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 200, y: 200 }, + }, + { + id: "g4", + value: 8, + parentId: "c2", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 280, y: 200 }, + }, + { + id: "g5", + value: 9, + parentId: "c3", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 340, y: 200 }, + }, + { + id: "g6", + value: 10, + parentId: "c3", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 420, y: 200 }, + }, +]; + +interface NAryNode { + value: number; + children: NAryNode[]; +} + +function executeNAryTreeTraversal(input: NAryTreeTraversalInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toNAryNode(id: string | null): NAryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + children: (node.childrenIds ?? []) + .map((childId) => toNAryNode(childId)) + .filter((child): child is NAryNode => child !== null), + }; + } + const root = toNAryNode(input.rootId); + return nAryTreeTraversal(root) as number[]; +} + +const nAryTreeTraversalDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.N_ARY_TREE_TRAVERSAL!, + name: "N-ary Tree Traversal", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Preorder traversal of an N-ary tree where each node can have any number of children — visits root before all children", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "r" }, + }, + execute: executeNAryTreeTraversal, + generateSteps: generateNAryTreeTraversalSteps, + educational: nAryTreeTraversalEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(nAryTreeTraversalDefinition); diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/n-ary-tree-traversal.test.ts b/src/algorithms/trees/advanced/n-ary-tree-traversal/n-ary-tree-traversal.test.ts new file mode 100644 index 00000000..d6c5c342 --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/n-ary-tree-traversal.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { nAryTreeTraversal } from "./sources/n-ary-tree-traversal.ts?fn"; + +interface NAryNode { + value: number; + children: NAryNode[]; +} +function node(value: number, ...children: NAryNode[]): NAryNode { + return { value, children }; +} + +describe("nAryTreeTraversal", () => { + it("returns empty for null root", () => { + expect(nAryTreeTraversal(null)).toEqual([]); + }); + + it("handles single node", () => { + expect(nAryTreeTraversal(node(5))).toEqual([5]); + }); + + it("preorder: root before all children", () => { + const root = node( + 1, + node(3, node(5), node(6)), + node(2, node(7), node(8)), + node(4, node(9), node(10)), + ); + const result = nAryTreeTraversal(root) as number[]; + expect(result[0]).toBe(1); // root first + expect(result[1]).toBe(3); // first child + expect(result).toHaveLength(10); + }); + + it("produces correct preorder for 3-level 3-ary tree", () => { + const root = node( + 1, + node(3, node(5), node(6)), + node(2, node(7), node(8)), + node(4, node(9), node(10)), + ); + const result = nAryTreeTraversal(root) as number[]; + expect(result).toEqual([1, 3, 5, 6, 2, 7, 8, 4, 9, 10]); + }); + + it("handles flat tree (no children)", () => { + expect(nAryTreeTraversal(node(42))).toEqual([42]); + }); +}); diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/NAryTreeTraversal.java b/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/NAryTreeTraversal.java new file mode 100644 index 00000000..e5e04479 --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/NAryTreeTraversal.java @@ -0,0 +1,27 @@ +// N-ary Tree Traversal — preorder visit using children list +import java.util.ArrayList; +import java.util.List; + +class NAryNode { + int value; + List children; + NAryNode(int v) { value = v; children = new ArrayList<>(); } +} + +class NAryTreeTraversal { + private void preorder(NAryNode node, List result) { + if (node == null) return; // @step:initialize + + result.add(node.value); // @step:visit + + for (NAryNode child : node.children) { + preorder(child, result); // @step:traverse-next + } + } + + public List nAryTreeTraversal(NAryNode root) { + List result = new ArrayList<>(); // @step:initialize + preorder(root, result); // @step:initialize + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/n-ary-tree-traversal.py b/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/n-ary-tree-traversal.py new file mode 100644 index 00000000..f6aa8b58 --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/n-ary-tree-traversal.py @@ -0,0 +1,21 @@ +# N-ary Tree Traversal — preorder visit using children list + +class NAryNode: + def __init__(self, value: int, children=None): + self.value = value + self.children = children or [] + +def n_ary_tree_traversal(root) -> list: + result = [] # @step:initialize + + def preorder(node): + if not node: + return # @step:initialize + + result.append(node.value) # @step:visit + + for child in node.children: + preorder(child) # @step:traverse-next + + preorder(root) # @step:initialize + return result # @step:complete diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/n-ary-tree-traversal.ts b/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/n-ary-tree-traversal.ts new file mode 100644 index 00000000..e5c1a36a --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/sources/n-ary-tree-traversal.ts @@ -0,0 +1,23 @@ +// N-ary Tree Traversal — preorder visit using childrenIds array + +interface NAryNode { + value: number; + children: NAryNode[]; +} + +function nAryTreeTraversal(root: NAryNode | null): number[] { + const result: number[] = []; // @step:initialize + + function preorder(node: NAryNode | null): void { + if (!node) return; // @step:initialize + + result.push(node.value); // @step:visit + + for (const child of node.children) { + preorder(child); // @step:traverse-next + } + } + + preorder(root); // @step:initialize + return result; // @step:complete +} diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/step-generator.test.ts b/src/algorithms/trees/advanced/n-ary-tree-traversal/step-generator.test.ts new file mode 100644 index 00000000..b05a37bd --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/step-generator.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateNAryTreeTraversalSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "r", + value: 1, + parentId: null, + leftChildId: null, + rightChildId: null, + childrenIds: ["c1", "c2", "c3"], + state: "default", + position: { x: 240, y: 40 }, + }, + { + id: "c1", + value: 3, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g1", "g2"], + state: "default", + position: { x: 100, y: 120 }, + }, + { + id: "c2", + value: 2, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g3", "g4"], + state: "default", + position: { x: 240, y: 120 }, + }, + { + id: "c3", + value: 4, + parentId: "r", + leftChildId: null, + rightChildId: null, + childrenIds: ["g5", "g6"], + state: "default", + position: { x: 380, y: 120 }, + }, + { + id: "g1", + value: 5, + parentId: "c1", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 50, y: 200 }, + }, + { + id: "g2", + value: 6, + parentId: "c1", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 130, y: 200 }, + }, + { + id: "g3", + value: 7, + parentId: "c2", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 200, y: 200 }, + }, + { + id: "g4", + value: 8, + parentId: "c2", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 280, y: 200 }, + }, + { + id: "g5", + value: 9, + parentId: "c3", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 340, y: 200 }, + }, + { + id: "g6", + value: 10, + parentId: "c3", + leftChildId: null, + rightChildId: null, + childrenIds: [], + state: "default", + position: { x: 420, y: 200 }, + }, +]; + +describe("generateNAryTreeTraversalSteps", () => { + it("produces steps for default 3-ary tree", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize step", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete step", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 10 nodes", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(10); + }); + + it("produces traverse-next steps for child traversal", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + const traverseSteps = steps.filter((step) => step.type === "traverse-next"); + expect(traverseSteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateNAryTreeTraversalSteps({ nodes: defaultNodes, rootId: "r" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/n-ary-tree-traversal/step-generator.ts b/src/algorithms/trees/advanced/n-ary-tree-traversal/step-generator.ts new file mode 100644 index 00000000..8769f202 --- /dev/null +++ b/src/algorithms/trees/advanced/n-ary-tree-traversal/step-generator.ts @@ -0,0 +1,49 @@ +/** Step generator for N-ary Tree Traversal — preorder traversal using childrenIds. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const N_ARY_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.N_ARY_TREE_TRAVERSAL!); + +export interface NAryTreeTraversalInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateNAryTreeTraversalSteps(input: NAryTreeTraversalInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new AdvancedTreeTracker(nodes, rootId, N_ARY_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("N-ary Tree Traversal (Preorder)", { rootId }); + + function preorder(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + // Visit current node first (preorder) + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + }); + + // Traverse all children + const childrenIds = node.childrenIds ?? []; + for (const childId of childrenIds) { + tracker.traverseNext(nodeId, childId, { + fromNode: nodeId, + toChild: childId, + }); + preorder(childId); + } + } + + preorder(rootId); + + tracker.complete({ result: `Visited all nodes in preorder` }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/red-black-insert/RedBlackInsertPipeline.stories.tsx b/src/algorithms/trees/advanced/red-black-insert/RedBlackInsertPipeline.stories.tsx new file mode 100644 index 00000000..6508453c --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/RedBlackInsertPipeline.stories.tsx @@ -0,0 +1,41 @@ +/** + * Storybook pipeline stories for Red-Black Tree Insert. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateRedBlackInsertSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateRedBlackInsertSteps({ values: [7, 3, 18, 10, 22, 8, 11, 26] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Red-Black Insert", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — before insertions begin */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — some nodes inserted with color fixes */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — fully balanced Red-Black tree */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/red-black-insert/educational.ts b/src/algorithms/trees/advanced/red-black-insert/educational.ts new file mode 100644 index 00000000..c0057265 --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/educational.ts @@ -0,0 +1,44 @@ +import type { EducationalContent } from "@/types"; + +export const redBlackInsertEducational: EducationalContent = { + overview: + "**Red-Black Tree Insertion** maintains a self-balancing BST using a two-color invariant. Every node is colored **red** or **black**, and four properties ensure the tree remains approximately balanced: (1) root is black, (2) red nodes have only black children, (3) every path from a node to its null descendants has the same number of black nodes, and (4) null leaves are black.\n\nAfter each insertion, the tree is fixed with a combination of **recoloring** and **rotations**.", + + howItWorks: + "Insertion follows three cases based on the uncle node's color:\n\n" + + "1. **Case 1 — Uncle is red:** Recolor parent, uncle to black and grandparent to red. Move up the tree.\n" + + "2. **Case 2 — Uncle is black, triangle:** Rotate the parent in the opposite direction to convert to Case 3.\n" + + "3. **Case 3 — Uncle is black, line:** Recolor and rotate the grandparent.\n\n" + + "At most 2 rotations and O(log n) recolorings are needed per insert.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(log n)`** per insertion (tree height is bounded by 2 log₂(n+1)).\n\n" + + "**Space Complexity: `O(log n)`** (call stack for fix-up traversal).", + + bestAndWorstCase: + "**Best case:** Insertion requires only recoloring with no rotations — O(log n).\n\n" + + "**Worst case:** Two rotations needed plus O(log n) recoloring passes — still O(log n). Red-black trees require fewer rotations than AVL trees on insert-heavy workloads.", + + realWorldUses: [ + "**C++ STL:** `std::map` and `std::set` use red-black trees internally.", + "**Java TreeMap/TreeSet:** Java's sorted collections are red-black tree implementations.", + "**Linux Kernel:** Completely Fair Scheduler (CFS) uses red-black trees for task scheduling.", + "**Nginx:** Uses red-black trees to manage timers.", + ], + + strengthsAndLimitations: { + strengths: [ + "At most 2 rotations per insert — faster insertions than AVL trees.", + "Well-studied with proven performance guarantees.", + "Used in OS schedulers and language standard libraries — battle-tested.", + ], + limitations: [ + "More complex to implement than AVL trees — 3 cases for insert, more for delete.", + "Lookup slightly slower than AVL trees (tree can be up to 2× as tall).", + "Color metadata per node — small memory overhead.", + ], + }, + + whenToUseIt: + "Use a red-black tree when you need fast insertions and deletions alongside O(log n) lookups. If reads dominate, an AVL tree gives better lookup performance. For mostly static data, a sorted array with binary search beats both.", +}; diff --git a/src/algorithms/trees/advanced/red-black-insert/index.ts b/src/algorithms/trees/advanced/red-black-insert/index.ts new file mode 100644 index 00000000..9dceeed3 --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/index.ts @@ -0,0 +1,37 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { redBlackInsert } from "./sources/red-black-insert.ts?fn"; +import { generateRedBlackInsertSteps } from "./step-generator"; +import type { RedBlackInsertInput } from "./step-generator"; +import { redBlackInsertEducational } from "./educational"; + +import typescriptSource from "./sources/red-black-insert.ts?raw"; +import pythonSource from "./sources/red-black-insert.py?raw"; +import javaSource from "./sources/RedBlackInsert.java?raw"; + +function executeRedBlackInsert(input: RedBlackInsertInput): number[] { + return redBlackInsert(input.values) as number[]; +} + +const redBlackInsertDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.RED_BLACK_INSERT!, + name: "Red-Black Tree Insert", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Insert values into a Red-Black tree with color fixes and rotations to maintain the red-black invariants", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(log n)" }, + spaceComplexity: "O(log n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [7, 3, 18, 10, 22, 8, 11, 26] }, + }, + execute: executeRedBlackInsert, + generateSteps: generateRedBlackInsertSteps, + educational: redBlackInsertEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(redBlackInsertDefinition); diff --git a/src/algorithms/trees/advanced/red-black-insert/red-black-insert.test.ts b/src/algorithms/trees/advanced/red-black-insert/red-black-insert.test.ts new file mode 100644 index 00000000..bdc03ed0 --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/red-black-insert.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { redBlackInsert } from "./sources/red-black-insert.ts?fn"; + +describe("redBlackInsert", () => { + it("inserts a single value", () => { + expect(redBlackInsert([5])).toEqual([5]); + }); + + it("produces sorted inorder output for default input", () => { + const values = [7, 3, 18, 10, 22, 8, 11, 26]; + const result = redBlackInsert(values) as number[]; + expect(result).toEqual([...values].sort((numA, numB) => numA - numB)); + }); + + it("handles ascending order insert", () => { + const result = redBlackInsert([1, 2, 3, 4, 5]) as number[]; + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles descending order insert", () => { + const result = redBlackInsert([5, 4, 3, 2, 1]) as number[]; + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + + it("returns empty array for empty input", () => { + expect(redBlackInsert([])).toEqual([]); + }); + + it("handles duplicate values gracefully", () => { + const result = redBlackInsert([5, 3, 5]) as number[]; + expect(result.length).toBeGreaterThan(0); + }); +}); diff --git a/src/algorithms/trees/advanced/red-black-insert/sources/RedBlackInsert.java b/src/algorithms/trees/advanced/red-black-insert/sources/RedBlackInsert.java new file mode 100644 index 00000000..baea7a79 --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/sources/RedBlackInsert.java @@ -0,0 +1,96 @@ +// Red-Black Tree Insertion — color rebalancing and rotations +import java.util.ArrayList; +import java.util.List; + +class RBNode { + int value; + String color; + RBNode left, right, parent; + RBNode(int v) { value = v; color = "red"; } +} + +class RedBlackInsert { + private RBNode root = null; + + private void rotateLeft(RBNode node) { + RBNode rightChild = node.right; // @step:rotate-left + node.right = rightChild.left; + if (rightChild.left != null) rightChild.left.parent = node; + rightChild.parent = node.parent; + if (node.parent == null) root = rightChild; + else if (node == node.parent.left) node.parent.left = rightChild; + else node.parent.right = rightChild; + rightChild.left = node; + node.parent = rightChild; // @step:rotate-left + } + + private void rotateRight(RBNode node) { + RBNode leftChild = node.left; // @step:rotate-right + node.left = leftChild.right; + if (leftChild.right != null) leftChild.right.parent = node; + leftChild.parent = node.parent; + if (node.parent == null) root = leftChild; + else if (node == node.parent.right) node.parent.right = leftChild; + else node.parent.left = leftChild; + leftChild.right = node; + node.parent = leftChild; // @step:rotate-right + } + + private void fixInsert(RBNode curr) { + while (curr.parent != null && curr.parent.color.equals("red")) { // @step:recolor-node + RBNode parent = curr.parent; + RBNode gp = parent.parent; + if (parent == gp.left) { + RBNode uncle = gp.right; + if (uncle != null && uncle.color.equals("red")) { + parent.color = "black"; uncle.color = "black"; gp.color = "red"; // @step:recolor-node + curr = gp; + } else { + if (curr == parent.right) { curr = parent; rotateLeft(curr); } // @step:rotate-left + curr.parent.color = "black"; gp.color = "red"; rotateRight(gp); // @step:rotate-right + } + } else { + RBNode uncle = gp.left; + if (uncle != null && uncle.color.equals("red")) { + parent.color = "black"; uncle.color = "black"; gp.color = "red"; // @step:recolor-node + curr = gp; + } else { + if (curr == parent.left) { curr = parent; rotateRight(curr); } // @step:rotate-right + curr.parent.color = "black"; gp.color = "red"; rotateLeft(gp); // @step:rotate-left + } + } + } + root.color = "black"; // @step:recolor-node + } + + private void insert(int value) { + RBNode newNode = new RBNode(value); // @step:insert-node + if (root == null) { root = newNode; root.color = "black"; return; } // @step:recolor-node + RBNode curr = root; + while (true) { + if (value < curr.value) { + if (curr.left == null) { curr.left = newNode; newNode.parent = curr; break; } + curr = curr.left; + } else { + if (curr.right == null) { curr.right = newNode; newNode.parent = curr; break; } + curr = curr.right; + } + } + fixInsert(newNode); // @step:recolor-node + } + + public List redBlackInsert(int[] values) { + root = null; // @step:initialize + for (int value : values) insert(value); // @step:insert-node + List result = new ArrayList<>(); + inorder(root, result); + return result; // @step:complete + } + + private void inorder(RBNode node, List result) { + if (node == null) return; + inorder(node.left, result); + result.add(node.value); + inorder(node.right, result); + } +} diff --git a/src/algorithms/trees/advanced/red-black-insert/sources/red-black-insert.py b/src/algorithms/trees/advanced/red-black-insert/sources/red-black-insert.py new file mode 100644 index 00000000..e0261ff5 --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/sources/red-black-insert.py @@ -0,0 +1,114 @@ +# Red-Black Tree Insertion — maintains balance via color rebalancing and rotations + +RED = "red" +BLACK = "black" + +class RBNode: + def __init__(self, value): + self.value = value + self.color = RED + self.left = None + self.right = None + self.parent = None + +def red_black_insert(values: list) -> list: + root = [None] # @step:initialize + + def rotate_left(node): + right_child = node.right # @step:rotate-left + node.right = right_child.left + if right_child.left: + right_child.left.parent = node + right_child.parent = node.parent + if not node.parent: + root[0] = right_child + elif node is node.parent.left: + node.parent.left = right_child + else: + node.parent.right = right_child + right_child.left = node + node.parent = right_child # @step:rotate-left + + def rotate_right(node): + left_child = node.left # @step:rotate-right + node.left = left_child.right + if left_child.right: + left_child.right.parent = node + left_child.parent = node.parent + if not node.parent: + root[0] = left_child + elif node is node.parent.right: + node.parent.right = left_child + else: + node.parent.left = left_child + left_child.right = node + node.parent = left_child # @step:rotate-right + + def fix_insert(current): + while current.parent and current.parent.color == RED: # @step:recolor-node + parent = current.parent + grandparent = parent.parent + if parent is grandparent.left: + uncle = grandparent.right + if uncle and uncle.color == RED: + parent.color = BLACK # @step:recolor-node + uncle.color = BLACK # @step:recolor-node + grandparent.color = RED # @step:recolor-node + current = grandparent + else: + if current is parent.right: + current = parent + rotate_left(current) # @step:rotate-left + current.parent.color = BLACK # @step:recolor-node + grandparent.color = RED # @step:recolor-node + rotate_right(grandparent) # @step:rotate-right + else: + uncle = grandparent.left + if uncle and uncle.color == RED: + parent.color = BLACK # @step:recolor-node + uncle.color = BLACK # @step:recolor-node + grandparent.color = RED # @step:recolor-node + current = grandparent + else: + if current is parent.left: + current = parent + rotate_right(current) # @step:rotate-right + current.parent.color = BLACK # @step:recolor-node + grandparent.color = RED # @step:recolor-node + rotate_left(grandparent) # @step:rotate-left + root[0].color = BLACK # @step:recolor-node + + def insert(value): + new_node = RBNode(value) # @step:insert-node + if not root[0]: + root[0] = new_node + root[0].color = BLACK # @step:recolor-node + return + current = root[0] + while True: + if value < current.value: + if not current.left: + current.left = new_node + new_node.parent = current + break + current = current.left + else: + if not current.right: + current.right = new_node + new_node.parent = current + break + current = current.right + fix_insert(new_node) # @step:recolor-node + + for value in values: + insert(value) # @step:insert-node + + result = [] + def inorder(node): + if not node: + return + inorder(node.left) + result.append(node.value) + inorder(node.right) + inorder(root[0]) + return result # @step:complete diff --git a/src/algorithms/trees/advanced/red-black-insert/sources/red-black-insert.ts b/src/algorithms/trees/advanced/red-black-insert/sources/red-black-insert.ts new file mode 100644 index 00000000..91d277fc --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/sources/red-black-insert.ts @@ -0,0 +1,129 @@ +// Red-Black Tree Insertion — maintains balance via color rebalancing and rotations + +type RBColor = "red" | "black"; + +interface RBNode { + value: number; + color: RBColor; + left: RBNode | null; + right: RBNode | null; + parent: RBNode | null; +} + +function redBlackInsert(values: number[]): number[] { + let root: RBNode | null = null; // @step:initialize + + function createNode(value: number): RBNode { + return { value, color: "red", left: null, right: null, parent: null }; // @step:insert-node + } + + function rotateLeft(node: RBNode): void { + const rightChild = node.right!; // @step:rotate-left + node.right = rightChild.left; + if (rightChild.left) rightChild.left.parent = node; + rightChild.parent = node.parent; + if (!node.parent) root = rightChild; + else if (node === node.parent.left) node.parent.left = rightChild; + else node.parent.right = rightChild; + rightChild.left = node; + node.parent = rightChild; // @step:rotate-left + } + + function rotateRight(node: RBNode): void { + const leftChild = node.left!; // @step:rotate-right + node.left = leftChild.right; + if (leftChild.right) leftChild.right.parent = node; + leftChild.parent = node.parent; + if (!node.parent) root = leftChild; + else if (node === node.parent.right) node.parent.right = leftChild; + else node.parent.left = leftChild; + leftChild.right = node; + node.parent = leftChild; // @step:rotate-right + } + + function fixInsert(inserted: RBNode): void { + let currentNode = inserted; + while (currentNode.parent?.color === "red") { + // @step:recolor-node + const parentNode = currentNode.parent; + const grandparent = parentNode.parent!; + if (parentNode === grandparent.left) { + const uncle = grandparent.right; + if (uncle?.color === "red") { + // Case 1: Uncle is red — recolor + parentNode.color = "black"; // @step:recolor-node + uncle.color = "black"; // @step:recolor-node + grandparent.color = "red"; // @step:recolor-node + currentNode = grandparent; + } else { + if (currentNode === parentNode.right) { + // Case 2: Triangle — rotate parent + currentNode = parentNode; + rotateLeft(currentNode); // @step:rotate-left + } + // Case 3: Line — rotate grandparent + currentNode.parent!.color = "black"; // @step:recolor-node + grandparent.color = "red"; // @step:recolor-node + rotateRight(grandparent); // @step:rotate-right + } + } else { + const uncle = grandparent.left; + if (uncle?.color === "red") { + parentNode.color = "black"; // @step:recolor-node + uncle.color = "black"; // @step:recolor-node + grandparent.color = "red"; // @step:recolor-node + currentNode = grandparent; + } else { + if (currentNode === parentNode.left) { + currentNode = parentNode; + rotateRight(currentNode); // @step:rotate-right + } + currentNode.parent!.color = "black"; // @step:recolor-node + grandparent.color = "red"; // @step:recolor-node + rotateLeft(grandparent); // @step:rotate-left + } + } + } + root!.color = "black"; // @step:recolor-node + } + + function insert(value: number): void { + const newNode = createNode(value); + if (!root) { + root = newNode; + root.color = "black"; // @step:recolor-node + return; + } + let currentNode: RBNode = root; + while (true) { + if (value < currentNode.value) { + if (!currentNode.left) { + currentNode.left = newNode; + newNode.parent = currentNode; + break; + } + currentNode = currentNode.left; + } else { + if (!currentNode.right) { + currentNode.right = newNode; + newNode.parent = currentNode; + break; + } + currentNode = currentNode.right; + } + } + fixInsert(newNode); // @step:recolor-node + } + + for (const value of values) insert(value); // @step:insert-node + + const result: number[] = []; + function inorder(node: RBNode | null): void { + if (!node) return; + inorder(node.left); + result.push(node.value); + inorder(node.right); + } + inorder(root); + return result; // @step:complete +} diff --git a/src/algorithms/trees/advanced/red-black-insert/step-generator.test.ts b/src/algorithms/trees/advanced/red-black-insert/step-generator.test.ts new file mode 100644 index 00000000..2f1c912a --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/step-generator.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { generateRedBlackInsertSteps } from "./step-generator"; + +describe("generateRedBlackInsertSteps", () => { + const defaultInput = { values: [7, 3, 18, 10, 22, 8, 11, 26] }; + + it("produces steps for default input", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces recolor steps", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + const recolorSteps = steps.filter((step) => step.type === "recolor-node"); + expect(recolorSteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("produces insert-child steps for each value", () => { + const steps = generateRedBlackInsertSteps(defaultInput); + const insertSteps = steps.filter((step) => step.type === "insert-child"); + expect(insertSteps.length).toBe(defaultInput.values.length); + }); +}); diff --git a/src/algorithms/trees/advanced/red-black-insert/step-generator.ts b/src/algorithms/trees/advanced/red-black-insert/step-generator.ts new file mode 100644 index 00000000..bd392f0e --- /dev/null +++ b/src/algorithms/trees/advanced/red-black-insert/step-generator.ts @@ -0,0 +1,259 @@ +/** Step generator for Red-Black Tree Insert — inserts values with color fixes and rotations. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const RB_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.RED_BLACK_INSERT!); + +export interface RedBlackInsertInput { + values: number[]; +} + +type RBColor = "red" | "black"; + +interface RBNode { + id: string; + value: number; + color: RBColor; + leftId: string | null; + rightId: string | null; + parentId: string | null; +} + +const NULL_ID = "__nil__"; + +function toTreeNodes(rbNodes: Map, rootId: string | null): TreeNode[] { + const posMap = new Map(); + + function assignPos(nodeId: string | null, depth: number, minX: number, maxX: number): void { + if (!nodeId || nodeId === NULL_ID) return; + const node = rbNodes.get(nodeId); + if (!node) return; + const midX = (minX + maxX) / 2; + posMap.set(nodeId, { x: midX, y: depth * 80 + 40 }); + assignPos(node.leftId, depth + 1, minX, midX); + assignPos(node.rightId, depth + 1, midX, maxX); + } + + assignPos(rootId, 0, 20, 480); + + return Array.from(rbNodes.values()) + .filter((node) => node.id !== NULL_ID) + .map((node) => ({ + id: node.id, + value: node.value, + parentId: node.parentId === NULL_ID ? null : node.parentId, + leftChildId: node.leftId === NULL_ID ? null : node.leftId, + rightChildId: node.rightId === NULL_ID ? null : node.rightId, + // Red nodes use "comparing" state, black use "visited" + state: node.color === "red" ? ("comparing" as const) : ("visited" as const), + position: posMap.get(node.id) ?? { x: 200, y: 40 }, + })); +} + +export function generateRedBlackInsertSteps(input: RedBlackInsertInput): ExecutionStep[] { + const { values } = input; + const rbNodes = new Map(); + let rootId: string | null = null; + let nodeCounter = 0; + + const tracker = new AdvancedTreeTracker([], "root", RB_LINE_MAP); + tracker.initialize("Red-Black Tree Insert", { values }); + + function makeId(): string { + nodeCounter += 1; + return `rb${nodeCounter}`; + } + + function syncTracker(): void { + const treeNodes = toTreeNodes(rbNodes, rootId); + tracker.updateNodes(treeNodes, rootId ?? "root"); + } + + function getNode(nodeId: string | null): RBNode | null { + if (!nodeId || nodeId === NULL_ID) return null; + return rbNodes.get(nodeId) ?? null; + } + + function rotateLeft(pivotId: string): void { + const pivot = getNode(pivotId)!; + const rightId = pivot.rightId!; + const rightNode = getNode(rightId)!; + + pivot.rightId = rightNode.leftId; + if (rightNode.leftId && rightNode.leftId !== NULL_ID) { + getNode(rightNode.leftId)!.parentId = pivotId; + } + rightNode.parentId = pivot.parentId; + if (!pivot.parentId || pivot.parentId === NULL_ID) { + rootId = rightId; + } else { + const parent = getNode(pivot.parentId)!; + if (parent.leftId === pivotId) parent.leftId = rightId; + else parent.rightId = rightId; + } + rightNode.leftId = pivotId; + pivot.parentId = rightId; + + syncTracker(); + tracker.rotateLeft(rightId, { pivot: pivot.value, newRoot: rightNode.value }); + } + + function rotateRight(pivotId: string): void { + const pivot = getNode(pivotId)!; + const leftId = pivot.leftId!; + const leftNode = getNode(leftId)!; + + pivot.leftId = leftNode.rightId; + if (leftNode.rightId && leftNode.rightId !== NULL_ID) { + getNode(leftNode.rightId)!.parentId = pivotId; + } + leftNode.parentId = pivot.parentId; + if (!pivot.parentId || pivot.parentId === NULL_ID) { + rootId = leftId; + } else { + const parent = getNode(pivot.parentId)!; + if (parent.rightId === pivotId) parent.rightId = leftId; + else parent.leftId = leftId; + } + leftNode.rightId = pivotId; + pivot.parentId = leftId; + + syncTracker(); + tracker.rotateRight(leftId, { pivot: pivot.value, newRoot: leftNode.value }); + } + + function fixInsert(insertedId: string): void { + let currentId = insertedId; + let currentNode = getNode(currentId)!; + + while (currentNode.parentId && getNode(currentNode.parentId)?.color === "red") { + const parentId = currentNode.parentId; + const parentNode = getNode(parentId)!; + const grandparentId = parentNode.parentId!; + const grandparentNode = getNode(grandparentId)!; + + if (parentId === grandparentNode.leftId) { + const uncleId = grandparentNode.rightId; + const uncleNode = getNode(uncleId ?? null); + if (uncleNode?.color === "red") { + // Case 1: recolor + parentNode.color = "black"; + uncleNode.color = "black"; + grandparentNode.color = "red"; + syncTracker(); + tracker.recolorNode(parentId, "black", { node: parentNode.value, color: "black" }); + tracker.recolorNode(uncleId!, "black", { node: uncleNode.value, color: "black" }); + tracker.recolorNode(grandparentId, "red", { node: grandparentNode.value, color: "red" }); + currentId = grandparentId; + currentNode = getNode(currentId)!; + } else { + if (currentId === parentNode.rightId) { + // Case 2: triangle — rotate left at parent + currentId = parentId; + currentNode = getNode(currentId)!; + rotateLeft(currentId); + } + // Case 3: line — rotate right at grandparent + const updatedParent = getNode(currentNode.parentId!)!; + updatedParent.color = "black"; + grandparentNode.color = "red"; + syncTracker(); + tracker.recolorNode(currentNode.parentId!, "black", { color: "black" }); + tracker.recolorNode(grandparentId, "red", { color: "red" }); + rotateRight(grandparentId); + } + } else { + const uncleId = grandparentNode.leftId; + const uncleNode = getNode(uncleId ?? null); + if (uncleNode?.color === "red") { + parentNode.color = "black"; + uncleNode.color = "black"; + grandparentNode.color = "red"; + syncTracker(); + tracker.recolorNode(parentId, "black", { node: parentNode.value, color: "black" }); + tracker.recolorNode(uncleId!, "black", { node: uncleNode.value, color: "black" }); + tracker.recolorNode(grandparentId, "red", { node: grandparentNode.value, color: "red" }); + currentId = grandparentId; + currentNode = getNode(currentId)!; + } else { + if (currentId === parentNode.leftId) { + currentId = parentId; + currentNode = getNode(currentId)!; + rotateRight(currentId); + } + const updatedParent = getNode(currentNode.parentId!)!; + updatedParent.color = "black"; + grandparentNode.color = "red"; + syncTracker(); + tracker.recolorNode(currentNode.parentId!, "black", { color: "black" }); + tracker.recolorNode(grandparentId, "red", { color: "red" }); + rotateLeft(grandparentId); + } + } + + if (!currentNode.parentId || currentNode.parentId === NULL_ID) break; + } + + if (rootId) { + const rootNode = getNode(rootId)!; + rootNode.color = "black"; + syncTracker(); + tracker.recolorNode(rootId, "black", { node: rootNode.value, color: "black (root)" }); + } + } + + function insertValue(value: number): void { + const newId = makeId(); + rbNodes.set(newId, { + id: newId, + value, + color: "red", + leftId: null, + rightId: null, + parentId: null, + }); + + if (!rootId) { + rootId = newId; + rbNodes.get(newId)!.color = "black"; + syncTracker(); + tracker.insertNode(newId, value, { inserted: value, color: "black" }); + return; + } + + let currentId: string = rootId; + while (true) { + const currentNode = getNode(currentId)!; + if (value < currentNode.value) { + if (!currentNode.leftId) { + currentNode.leftId = newId; + rbNodes.get(newId)!.parentId = currentId; + break; + } + currentId = currentNode.leftId; + } else { + if (!currentNode.rightId) { + currentNode.rightId = newId; + rbNodes.get(newId)!.parentId = currentId; + break; + } + currentId = currentNode.rightId; + } + } + + syncTracker(); + tracker.insertNode(newId, value, { inserted: value, color: "red" }); + fixInsert(newId); + } + + for (const value of values) { + insertValue(value); + } + + tracker.complete({ result: `Red-Black tree with ${rbNodes.size} nodes` }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/SegmentTreeRangeMinPipeline.stories.tsx b/src/algorithms/trees/advanced/segment-tree-range-min/SegmentTreeRangeMinPipeline.stories.tsx new file mode 100644 index 00000000..799e1be2 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/SegmentTreeRangeMinPipeline.stories.tsx @@ -0,0 +1,47 @@ +/** + * Storybook pipeline stories for Segment Tree Range Min. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateSegmentTreeRangeMinSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateSegmentTreeRangeMinSteps({ + array: [2, 5, 1, 4, 9, 3], + queries: [ + [0, 2], + [3, 5], + ], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Segment Tree Range Min", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — before tree is built */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — tree partially constructed */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — queries answered */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/educational.ts b/src/algorithms/trees/advanced/segment-tree-range-min/educational.ts new file mode 100644 index 00000000..c799521e --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const segmentTreeRangeMinEducational: EducationalContent = { + overview: + "A **Range Minimum Query (RMQ) Segment Tree** stores the minimum value of each subarray range in a binary tree structure. Given any query `[L, R]`, it finds the minimum element in that subrange in `O(log n)` time — much faster than scanning the array linearly.", + + howItWorks: + "**Build:** Each leaf stores its element. Each internal node stores the minimum of its two children's ranges.\n\n" + + "**Query `[L, R]`:**\n" + + "1. If the node's range is outside `[L, R]`, return `∞` (infinity).\n" + + "2. If completely inside `[L, R]`, return the stored minimum.\n" + + "3. Otherwise, recurse into both children and return `min(leftResult, rightResult)`.\n\n" + + "**Example:** Array `[2,5,1,4,9,3]`, query `[0,2]` → min of `[2,5,1]` = **1**.", + + timeAndSpaceComplexity: + "**Build: `O(n)`** — each of the ~2n nodes computed exactly once.\n\n" + + "**Query: `O(log n)`** per query.\n\n" + + "**Space: `O(4n)`** for the segment tree array.", + + bestAndWorstCase: + "**Best case:** Query exactly matches a node's range — answered in O(1).\n\n" + + "**Worst case:** Query spans many partial ranges — O(log n) nodes visited.", + + realWorldUses: [ + "**Competitive programming:** Solve range minimum problems in O(log n) per query.", + "**Bioinformatics:** Lowest Common Ancestor (LCA) reduces to RMQ.", + "**Database engines:** Range statistics over indexed numeric columns.", + "**Image processing:** Sliding window minimums for morphological erosion.", + ], + + strengthsAndLimitations: { + strengths: [ + "Handles both range queries and point updates in O(log n).", + "Same structure as range sum — trivial to adapt between sum, min, max, GCD.", + "Efficient for both sparse and dense query patterns.", + ], + limitations: [ + "4× memory overhead vs source array.", + "For static arrays with many queries, Sparse Table gives O(1) query but uses O(n log n) space and cannot handle updates.", + "2D extensions are significantly more complex.", + ], + }, + + whenToUseIt: + "Use a range-min segment tree when you need repeated range minimum queries with occasional updates. For static arrays with read-only queries, a Sparse Table is faster (O(1) query). For a single minimum query, a linear scan suffices.", +}; diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/index.ts b/src/algorithms/trees/advanced/segment-tree-range-min/index.ts new file mode 100644 index 00000000..f54ba91e --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/index.ts @@ -0,0 +1,43 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { segmentTreeRangeMin } from "./sources/segment-tree-range-min.ts?fn"; +import { generateSegmentTreeRangeMinSteps } from "./step-generator"; +import type { SegmentTreeRangeMinInput } from "./step-generator"; +import { segmentTreeRangeMinEducational } from "./educational"; + +import typescriptSource from "./sources/segment-tree-range-min.ts?raw"; +import pythonSource from "./sources/segment-tree-range-min.py?raw"; +import javaSource from "./sources/SegmentTreeRangeMin.java?raw"; + +function executeSegmentTreeRangeMin(input: SegmentTreeRangeMinInput): number[] { + return segmentTreeRangeMin(input.array, input.queries) as number[]; +} + +const segmentTreeRangeMinDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SEGMENT_TREE_RANGE_MIN!, + name: "Segment Tree Range Min", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Build a segment tree for range minimum queries — find the minimum element in any subrange in O(log n)", + timeComplexity: { best: "O(n + q log n)", average: "O(n + q log n)", worst: "O(n + q log n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + array: [2, 5, 1, 4, 9, 3], + queries: [ + [0, 2], + [3, 5], + ], + }, + }, + execute: executeSegmentTreeRangeMin, + generateSteps: generateSegmentTreeRangeMinSteps, + educational: segmentTreeRangeMinEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(segmentTreeRangeMinDefinition); diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/segment-tree-range-min.test.ts b/src/algorithms/trees/advanced/segment-tree-range-min/segment-tree-range-min.test.ts new file mode 100644 index 00000000..aaf6cf1f --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/segment-tree-range-min.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { segmentTreeRangeMin } from "./sources/segment-tree-range-min.ts?fn"; + +describe("segmentTreeRangeMin", () => { + it("queries range min for default input", () => { + const result = segmentTreeRangeMin( + [2, 5, 1, 4, 9, 3], + [ + [0, 2], + [3, 5], + ], + ) as number[]; + expect(result[0]).toBe(1); // min of [2,5,1] + expect(result[1]).toBe(3); // min of [4,9,3] + }); + + it("handles single element query", () => { + const result = segmentTreeRangeMin([4, 2, 6], [[1, 1]]) as number[]; + expect(result[0]).toBe(2); + }); + + it("handles full range query", () => { + const arr = [3, 1, 4, 1, 5, 9]; + const result = segmentTreeRangeMin(arr, [[0, 5]]) as number[]; + expect(result[0]).toBe(1); + }); + + it("handles multiple queries on same array", () => { + const result = segmentTreeRangeMin( + [10, 3, 8, 1, 7], + [ + [0, 2], + [1, 4], + [3, 4], + ], + ) as number[]; + expect(result[0]).toBe(3); + expect(result[1]).toBe(1); + expect(result[2]).toBe(1); + }); +}); diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/sources/SegmentTreeRangeMin.java b/src/algorithms/trees/advanced/segment-tree-range-min/sources/SegmentTreeRangeMin.java new file mode 100644 index 00000000..08b700cd --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/sources/SegmentTreeRangeMin.java @@ -0,0 +1,39 @@ +// Segment Tree — build from array then query range minimums +import java.util.ArrayList; +import java.util.List; + +class SegmentTreeRangeMin { + private int[] segTree; + private int n; + + private void build(int[] array, int nodeIdx, int low, int high) { + if (low == high) { + segTree[nodeIdx] = array[low]; // @step:build-node + return; + } + int mid = (low + high) / 2; + build(array, 2 * nodeIdx, low, mid); // @step:traverse-left + build(array, 2 * nodeIdx + 1, mid + 1, high); // @step:traverse-right + segTree[nodeIdx] = Math.min(segTree[2 * nodeIdx], segTree[2 * nodeIdx + 1]); // @step:update-segment + } + + private int queryMin(int nodeIdx, int low, int high, int qLow, int qHigh) { + if (qLow > high || qHigh < low) return Integer.MAX_VALUE; // @step:query-range + if (qLow <= low && high <= qHigh) return segTree[nodeIdx]; // @step:query-range + int mid = (low + high) / 2; + int leftMin = queryMin(2 * nodeIdx, low, mid, qLow, qHigh); // @step:traverse-left + int rightMin = queryMin(2 * nodeIdx + 1, mid + 1, high, qLow, qHigh); // @step:traverse-right + return Math.min(leftMin, rightMin); // @step:query-range + } + + public List segmentTreeRangeMin(int[] array, int[][] queries) { + n = array.length; // @step:initialize + segTree = new int[4 * n]; // @step:initialize + build(array, 1, 0, n - 1); // @step:build-node + List results = new ArrayList<>(); + for (int[] query : queries) { + results.add(queryMin(1, 0, n - 1, query[0], query[1])); // @step:query-range + } + return results; // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/sources/segment-tree-range-min.py b/src/algorithms/trees/advanced/segment-tree-range-min/sources/segment-tree-range-min.py new file mode 100644 index 00000000..4d0fdc96 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/sources/segment-tree-range-min.py @@ -0,0 +1,32 @@ +# Segment Tree — build from array then query range minimums +import math + +def segment_tree_range_min(array: list, queries: list) -> list: + n = len(array) # @step:initialize + seg_tree = [math.inf] * (4 * n) # @step:initialize + + def build(node_idx, low, high): + if low == high: + seg_tree[node_idx] = array[low] # @step:build-node + return + mid = (low + high) // 2 + build(2 * node_idx, low, mid) # @step:traverse-left + build(2 * node_idx + 1, mid + 1, high) # @step:traverse-right + seg_tree[node_idx] = min(seg_tree[2 * node_idx], seg_tree[2 * node_idx + 1]) # @step:update-segment + + def query_min(node_idx, low, high, q_low, q_high): + if q_low > high or q_high < low: + return math.inf # @step:query-range + if q_low <= low and high <= q_high: + return seg_tree[node_idx] # @step:query-range + mid = (low + high) // 2 + left_min = query_min(2 * node_idx, low, mid, q_low, q_high) # @step:traverse-left + right_min = query_min(2 * node_idx + 1, mid + 1, high, q_low, q_high) # @step:traverse-right + return min(left_min, right_min) # @step:query-range + + build(1, 0, n - 1) # @step:build-node + + results = [] + for q_low, q_high in queries: + results.append(query_min(1, 0, n - 1, q_low, q_high)) # @step:query-range + return results # @step:complete diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/sources/segment-tree-range-min.ts b/src/algorithms/trees/advanced/segment-tree-range-min/sources/segment-tree-range-min.ts new file mode 100644 index 00000000..20298542 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/sources/segment-tree-range-min.ts @@ -0,0 +1,43 @@ +// Segment Tree — build from array then query range minimums + +function segmentTreeRangeMin(array: number[], queries: [number, number][]): number[] { + const arrayLength = array.length; // @step:initialize + const segTree: number[] = new Array(4 * arrayLength).fill(Infinity); // @step:initialize + + function buildNode(nodeIndex: number, low: number, high: number): void { + if (low === high) { + segTree[nodeIndex] = array[low] ?? Infinity; // @step:build-node + return; + } + const mid = Math.floor((low + high) / 2); + buildNode(2 * nodeIndex, low, mid); // @step:traverse-left + buildNode(2 * nodeIndex + 1, mid + 1, high); // @step:traverse-right + segTree[nodeIndex] = Math.min( + segTree[2 * nodeIndex] ?? Infinity, + segTree[2 * nodeIndex + 1] ?? Infinity, + ); // @step:update-segment + } + + function queryMin( + nodeIndex: number, + low: number, + high: number, + qLow: number, + qHigh: number, + ): number { + if (qLow > high || qHigh < low) return Infinity; // @step:query-range + if (qLow <= low && high <= qHigh) return segTree[nodeIndex] ?? Infinity; // @step:query-range + const mid = Math.floor((low + high) / 2); + const leftMin = queryMin(2 * nodeIndex, low, mid, qLow, qHigh); // @step:traverse-left + const rightMin = queryMin(2 * nodeIndex + 1, mid + 1, high, qLow, qHigh); // @step:traverse-right + return Math.min(leftMin, rightMin); // @step:query-range + } + + buildNode(1, 0, arrayLength - 1); // @step:build-node + + const results: number[] = []; + for (const [qLow, qHigh] of queries) { + results.push(queryMin(1, 0, arrayLength - 1, qLow, qHigh)); // @step:query-range + } + return results; // @step:complete +} diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/step-generator.test.ts b/src/algorithms/trees/advanced/segment-tree-range-min/step-generator.test.ts new file mode 100644 index 00000000..b4891ddd --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/step-generator.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { generateSegmentTreeRangeMinSteps } from "./step-generator"; + +describe("generateSegmentTreeRangeMinSteps", () => { + const defaultInput = { + array: [2, 5, 1, 4, 9, 3], + queries: [ + [0, 2], + [3, 5], + ] as [number, number][], + }; + + it("produces steps for default input", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces build-node steps during construction", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBeGreaterThan(0); + }); + + it("produces query-range steps", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + const querySteps = steps.filter((step) => step.type === "query-range"); + expect(querySteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateSegmentTreeRangeMinSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/segment-tree-range-min/step-generator.ts b/src/algorithms/trees/advanced/segment-tree-range-min/step-generator.ts new file mode 100644 index 00000000..314122a2 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-min/step-generator.ts @@ -0,0 +1,137 @@ +/** Step generator for Segment Tree Range Min — builds tree then queries range minimums. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SEG_MIN_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SEGMENT_TREE_RANGE_MIN!); + +export interface SegmentTreeRangeMinInput { + array: number[]; + queries: [number, number][]; +} + +function buildMinSegTreeNodes(array: number[]): TreeNode[] { + const arrayLength = array.length; + const treeNodes: TreeNode[] = []; + const segValues: number[] = new Array(4 * arrayLength).fill(Infinity); + + function buildInner(nodeIndex: number, low: number, high: number): void { + if (low === high) { + segValues[nodeIndex] = array[low] ?? Infinity; + return; + } + const mid = Math.floor((low + high) / 2); + buildInner(nodeIndex * 2, low, mid); + buildInner(nodeIndex * 2 + 1, mid + 1, high); + segValues[nodeIndex] = Math.min( + segValues[nodeIndex * 2] ?? Infinity, + segValues[nodeIndex * 2 + 1] ?? Infinity, + ); + } + + buildInner(1, 0, arrayLength - 1); + + const posMap = new Map(); + + function collectNodes( + nodeIndex: number, + low: number, + high: number, + minX: number, + maxX: number, + ): void { + const midX = (minX + maxX) / 2; + posMap.set(nodeIndex, { x: midX, y: Math.floor(Math.log2(nodeIndex)) * 70 + 40 }); + + const parentIndex = Math.floor(nodeIndex / 2); + treeNodes.push({ + id: `min${nodeIndex}`, + value: segValues[nodeIndex] === Infinity ? 0 : (segValues[nodeIndex] ?? 0), + parentId: nodeIndex === 1 ? null : `min${parentIndex}`, + leftChildId: low < high ? `min${nodeIndex * 2}` : null, + rightChildId: low < high ? `min${nodeIndex * 2 + 1}` : null, + state: "default", + position: posMap.get(nodeIndex) ?? { x: midX, y: 40 }, + }); + + if (low < high) { + const mid = Math.floor((low + high) / 2); + collectNodes(nodeIndex * 2, low, mid, minX, midX); + collectNodes(nodeIndex * 2 + 1, mid + 1, high, midX, maxX); + } + } + + collectNodes(1, 0, arrayLength - 1, 20, 480); + return treeNodes; +} + +export function generateSegmentTreeRangeMinSteps(input: SegmentTreeRangeMinInput): ExecutionStep[] { + const { array, queries } = input; + const arrayLength = array.length; + const segValues: number[] = new Array(4 * arrayLength).fill(Infinity); + + const tracker = new AdvancedTreeTracker([], "min1", SEG_MIN_LINE_MAP); + tracker.initialize("Segment Tree Range Min", { array, queries }); + + function syncTracker(): void { + const treeNodes = buildMinSegTreeNodes(array); + for (const treeNode of treeNodes) { + const idx = parseInt(treeNode.id.replace("min", ""), 10); + const val = segValues[idx]; + treeNode.value = val === Infinity || val === undefined ? 0 : val; + } + tracker.updateNodes(treeNodes, "min1"); + } + + function buildRecursive(nodeIndex: number, low: number, high: number): void { + if (low === high) { + segValues[nodeIndex] = array[low] ?? Infinity; + syncTracker(); + tracker.buildNode(`min${nodeIndex}`, segValues[nodeIndex] ?? 0, { + nodeIndex, + range: [low, high], + value: segValues[nodeIndex], + }); + return; + } + const mid = Math.floor((low + high) / 2); + buildRecursive(nodeIndex * 2, low, mid); + buildRecursive(nodeIndex * 2 + 1, mid + 1, high); + segValues[nodeIndex] = Math.min( + segValues[nodeIndex * 2] ?? Infinity, + segValues[nodeIndex * 2 + 1] ?? Infinity, + ); + syncTracker(); + tracker.updateSegment(`min${nodeIndex}`, segValues[nodeIndex] ?? 0, { + nodeIndex, + range: [low, high], + leftMin: segValues[nodeIndex * 2], + rightMin: segValues[nodeIndex * 2 + 1], + }); + } + + buildRecursive(1, 0, arrayLength - 1); + + for (const [queryLow, queryHigh] of queries) { + const traverseMin = (nodeIndex: number, low: number, high: number): number => { + tracker.queryRange(`min${nodeIndex}`, queryLow, queryHigh, { + nodeIndex, + range: [low, high], + queryBounds: [queryLow, queryHigh], + }); + if (queryLow > high || queryHigh < low) return Infinity; + if (queryLow <= low && high <= queryHigh) return segValues[nodeIndex] ?? Infinity; + const mid = Math.floor((low + high) / 2); + const leftMin = traverseMin(nodeIndex * 2, low, mid); + const rightMin = traverseMin(nodeIndex * 2 + 1, mid + 1, high); + return Math.min(leftMin, rightMin); + }; + + const result = traverseMin(1, 0, arrayLength - 1); + tracker.complete({ queryRange: [queryLow, queryHigh], result }); + } + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/SegmentTreeRangeSumPipeline.stories.tsx b/src/algorithms/trees/advanced/segment-tree-range-sum/SegmentTreeRangeSumPipeline.stories.tsx new file mode 100644 index 00000000..f0d70912 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/SegmentTreeRangeSumPipeline.stories.tsx @@ -0,0 +1,47 @@ +/** + * Storybook pipeline stories for Segment Tree Range Sum. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateSegmentTreeRangeSumSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateSegmentTreeRangeSumSteps({ + array: [1, 3, 5, 7, 9, 11], + queries: [ + [1, 3], + [0, 5], + ], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Segment Tree Range Sum", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — before tree is built */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — tree partially built */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — all queries answered */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/educational.ts b/src/algorithms/trees/advanced/segment-tree-range-sum/educational.ts new file mode 100644 index 00000000..f7bb8c9f --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const segmentTreeRangeSumEducational: EducationalContent = { + overview: + "A **Segment Tree** is a binary tree where each node stores the aggregate (here, the sum) of a contiguous subarray. The root covers the entire array; each leaf covers a single element. This structure enables both **range queries** and **point updates** in `O(log n)` time — far faster than the `O(n)` scan required by a plain array.", + + howItWorks: + "**Build phase:** Recursively split the array in half. Each internal node's value is the sum of its two children. A leaf node stores its single element.\n\n" + + "**Query phase:** To find the sum of range `[L, R]`:\n" + + "1. If the current node's range is completely outside `[L, R]`, return 0.\n" + + "2. If completely inside `[L, R]`, return the node's stored value.\n" + + "3. Otherwise, recurse into both children and sum the results.\n\n" + + "**Example:** Array `[1,3,5,7,9,11]`, query `[1,3]` → sum of elements at indices 1-3 = 3+5+7 = **15**.", + + timeAndSpaceComplexity: + "**Build Time: `O(n)`** — each node is computed once.\n\n" + + "**Query Time: `O(log n)`** per query — at most 4 nodes are visited per level.\n\n" + + "**Space: `O(4n)`** — the tree array needs up to 4× the input size.", + + bestAndWorstCase: + "**Best case:** Query range exactly matches a stored node's range — answered in `O(1)`.\n\n" + + "**Worst case:** Query range straddles many node boundaries — still `O(log n)` nodes visited.", + + realWorldUses: [ + "**Range sum queries:** Database aggregations over indexed column ranges.", + "**Interval scheduling:** Checking overlapping reservations in calendar systems.", + "**Competitive programming:** A fundamental tool for range queries and updates.", + "**Game development:** Tracking region statistics (e.g., sum of unit health in a zone).", + ], + + strengthsAndLimitations: { + strengths: [ + "O(log n) query and update — dramatically faster than O(n) brute force for large arrays.", + "Versatile — the same structure works for sum, min, max, GCD, and XOR queries.", + "Simple recursive structure is easy to reason about.", + ], + limitations: [ + "4× memory overhead compared to the source array.", + "Build time is O(n) — not suitable for frequently changing array size.", + "More complex to extend to 2D arrays (2D segment trees).", + ], + }, + + whenToUseIt: + "Use a segment tree when you need repeated range queries (sum, min, max) on a static or infrequently updated array. For prefix sums on a static array, a prefix sum array is simpler. For frequent point updates, a Fenwick tree (BIT) may be easier to implement.", +}; diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/index.ts b/src/algorithms/trees/advanced/segment-tree-range-sum/index.ts new file mode 100644 index 00000000..4cc04773 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/index.ts @@ -0,0 +1,43 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { segmentTreeRangeSum } from "./sources/segment-tree-range-sum.ts?fn"; +import { generateSegmentTreeRangeSumSteps } from "./step-generator"; +import type { SegmentTreeRangeSumInput } from "./step-generator"; +import { segmentTreeRangeSumEducational } from "./educational"; + +import typescriptSource from "./sources/segment-tree-range-sum.ts?raw"; +import pythonSource from "./sources/segment-tree-range-sum.py?raw"; +import javaSource from "./sources/SegmentTreeRangeSum.java?raw"; + +function executeSegmentTreeRangeSum(input: SegmentTreeRangeSumInput): number[] { + return segmentTreeRangeSum(input.array, input.queries) as number[]; +} + +const segmentTreeRangeSumDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SEGMENT_TREE_RANGE_SUM!, + name: "Segment Tree Range Sum", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Build a segment tree from an array and answer range sum queries in O(log n) per query", + timeComplexity: { best: "O(n + q log n)", average: "O(n + q log n)", worst: "O(n + q log n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + array: [1, 3, 5, 7, 9, 11], + queries: [ + [1, 3], + [0, 5], + ], + }, + }, + execute: executeSegmentTreeRangeSum, + generateSteps: generateSegmentTreeRangeSumSteps, + educational: segmentTreeRangeSumEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(segmentTreeRangeSumDefinition); diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/segment-tree-range-sum.test.ts b/src/algorithms/trees/advanced/segment-tree-range-sum/segment-tree-range-sum.test.ts new file mode 100644 index 00000000..987d9455 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/segment-tree-range-sum.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { segmentTreeRangeSum } from "./sources/segment-tree-range-sum.ts?fn"; + +describe("segmentTreeRangeSum", () => { + it("queries range sum for default input", () => { + const result = segmentTreeRangeSum( + [1, 3, 5, 7, 9, 11], + [ + [1, 3], + [0, 5], + ], + ) as number[]; + expect(result[0]).toBe(15); // 3+5+7 + expect(result[1]).toBe(36); // 1+3+5+7+9+11 + }); + + it("handles single element query", () => { + const result = segmentTreeRangeSum([4, 2, 6], [[1, 1]]) as number[]; + expect(result[0]).toBe(2); + }); + + it("handles full range query", () => { + const arr = [1, 2, 3, 4, 5]; + const result = segmentTreeRangeSum(arr, [[0, 4]]) as number[]; + expect(result[0]).toBe(15); + }); + + it("handles multiple queries on same array", () => { + const result = segmentTreeRangeSum( + [10, 20, 30, 40, 50], + [ + [0, 1], + [2, 4], + [1, 3], + ], + ) as number[]; + expect(result[0]).toBe(30); + expect(result[1]).toBe(120); + expect(result[2]).toBe(90); + }); +}); diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/sources/SegmentTreeRangeSum.java b/src/algorithms/trees/advanced/segment-tree-range-sum/sources/SegmentTreeRangeSum.java new file mode 100644 index 00000000..62f48a10 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/sources/SegmentTreeRangeSum.java @@ -0,0 +1,39 @@ +// Segment Tree — build from array then query range sums +import java.util.ArrayList; +import java.util.List; + +class SegmentTreeRangeSum { + private int[] segTree; + private int n; + + private void build(int[] array, int nodeIdx, int low, int high) { + if (low == high) { + segTree[nodeIdx] = array[low]; // @step:build-node + return; + } + int mid = (low + high) / 2; + build(array, 2 * nodeIdx, low, mid); // @step:traverse-left + build(array, 2 * nodeIdx + 1, mid + 1, high); // @step:traverse-right + segTree[nodeIdx] = segTree[2 * nodeIdx] + segTree[2 * nodeIdx + 1]; // @step:update-segment + } + + private int query(int nodeIdx, int low, int high, int qLow, int qHigh) { + if (qLow > high || qHigh < low) return 0; // @step:query-range + if (qLow <= low && high <= qHigh) return segTree[nodeIdx]; // @step:query-range + int mid = (low + high) / 2; + int leftSum = query(2 * nodeIdx, low, mid, qLow, qHigh); // @step:traverse-left + int rightSum = query(2 * nodeIdx + 1, mid + 1, high, qLow, qHigh); // @step:traverse-right + return leftSum + rightSum; // @step:query-range + } + + public List segmentTreeRangeSum(int[] array, int[][] queries) { + n = array.length; // @step:initialize + segTree = new int[4 * n]; // @step:initialize + build(array, 1, 0, n - 1); // @step:build-node + List results = new ArrayList<>(); + for (int[] query : queries) { + results.add(query(1, 0, n - 1, query[0], query[1])); // @step:query-range + } + return results; // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/sources/segment-tree-range-sum.py b/src/algorithms/trees/advanced/segment-tree-range-sum/sources/segment-tree-range-sum.py new file mode 100644 index 00000000..9f269615 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/sources/segment-tree-range-sum.py @@ -0,0 +1,31 @@ +# Segment Tree — build from array then query range sums + +def segment_tree_range_sum(array: list, queries: list) -> list: + n = len(array) # @step:initialize + seg_tree = [0] * (4 * n) # @step:initialize + + def build(node_idx, low, high): + if low == high: + seg_tree[node_idx] = array[low] # @step:build-node + return + mid = (low + high) // 2 + build(2 * node_idx, low, mid) # @step:traverse-left + build(2 * node_idx + 1, mid + 1, high) # @step:traverse-right + seg_tree[node_idx] = seg_tree[2 * node_idx] + seg_tree[2 * node_idx + 1] # @step:update-segment + + def query(node_idx, low, high, q_low, q_high): + if q_low > high or q_high < low: + return 0 # @step:query-range + if q_low <= low and high <= q_high: + return seg_tree[node_idx] # @step:query-range + mid = (low + high) // 2 + left_sum = query(2 * node_idx, low, mid, q_low, q_high) # @step:traverse-left + right_sum = query(2 * node_idx + 1, mid + 1, high, q_low, q_high) # @step:traverse-right + return left_sum + right_sum # @step:query-range + + build(1, 0, n - 1) # @step:build-node + + results = [] + for q_low, q_high in queries: + results.append(query(1, 0, n - 1, q_low, q_high)) # @step:query-range + return results # @step:complete diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/sources/segment-tree-range-sum.ts b/src/algorithms/trees/advanced/segment-tree-range-sum/sources/segment-tree-range-sum.ts new file mode 100644 index 00000000..3d46ab8a --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/sources/segment-tree-range-sum.ts @@ -0,0 +1,40 @@ +// Segment Tree — build from array then query range sums + +function segmentTreeRangeSum(array: number[], queries: [number, number][]): number[] { + const arrayLength = array.length; // @step:initialize + const segTree: number[] = new Array(4 * arrayLength).fill(0); // @step:initialize + + function buildNode(nodeIndex: number, low: number, high: number): void { + if (low === high) { + segTree[nodeIndex] = array[low] ?? 0; // @step:build-node + return; + } + const mid = Math.floor((low + high) / 2); + buildNode(2 * nodeIndex, low, mid); // @step:traverse-left + buildNode(2 * nodeIndex + 1, mid + 1, high); // @step:traverse-right + segTree[nodeIndex] = (segTree[2 * nodeIndex] ?? 0) + (segTree[2 * nodeIndex + 1] ?? 0); // @step:update-segment + } + + function queryRange( + nodeIndex: number, + low: number, + high: number, + qLow: number, + qHigh: number, + ): number { + if (qLow > high || qHigh < low) return 0; // @step:query-range + if (qLow <= low && high <= qHigh) return segTree[nodeIndex] ?? 0; // @step:query-range + const mid = Math.floor((low + high) / 2); + const leftSum = queryRange(2 * nodeIndex, low, mid, qLow, qHigh); // @step:traverse-left + const rightSum = queryRange(2 * nodeIndex + 1, mid + 1, high, qLow, qHigh); // @step:traverse-right + return leftSum + rightSum; // @step:query-range + } + + buildNode(1, 0, arrayLength - 1); // @step:build-node + + const results: number[] = []; + for (const [qLow, qHigh] of queries) { + results.push(queryRange(1, 0, arrayLength - 1, qLow, qHigh)); // @step:query-range + } + return results; // @step:complete +} diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/step-generator.test.ts b/src/algorithms/trees/advanced/segment-tree-range-sum/step-generator.test.ts new file mode 100644 index 00000000..ca162294 --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/step-generator.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { generateSegmentTreeRangeSumSteps } from "./step-generator"; + +describe("generateSegmentTreeRangeSumSteps", () => { + const defaultInput = { + array: [1, 3, 5, 7, 9, 11], + queries: [ + [1, 3], + [0, 5], + ] as [number, number][], + }; + + it("produces steps for default input", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces build-node steps during construction", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBeGreaterThan(0); + }); + + it("produces query-range steps during queries", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + const querySteps = steps.filter((step) => step.type === "query-range"); + expect(querySteps.length).toBeGreaterThan(0); + }); + + it("has incrementing step indices", () => { + const steps = generateSegmentTreeRangeSumSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/segment-tree-range-sum/step-generator.ts b/src/algorithms/trees/advanced/segment-tree-range-sum/step-generator.ts new file mode 100644 index 00000000..9673b93f --- /dev/null +++ b/src/algorithms/trees/advanced/segment-tree-range-sum/step-generator.ts @@ -0,0 +1,136 @@ +/** Step generator for Segment Tree Range Sum — builds tree then queries ranges. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SEG_SUM_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SEGMENT_TREE_RANGE_SUM!); + +export interface SegmentTreeRangeSumInput { + array: number[]; + queries: [number, number][]; +} + +function buildSegTreeNodes(array: number[]): TreeNode[] { + const arrayLength = array.length; + const treeNodes: TreeNode[] = []; + const segValues: number[] = new Array(4 * arrayLength).fill(0); + const posMap = new Map(); + + function assignPos(nodeIndex: number, depth: number, minX: number, maxX: number): void { + const midX = (minX + maxX) / 2; + posMap.set(nodeIndex, { x: midX, y: depth * 70 + 40 }); + if (nodeIndex * 2 < 4 * arrayLength) { + assignPos(nodeIndex * 2, depth + 1, minX, midX); + assignPos(nodeIndex * 2 + 1, depth + 1, midX, maxX); + } + } + + function buildInner(nodeIndex: number, low: number, high: number): void { + if (low === high) { + segValues[nodeIndex] = array[low] ?? 0; + return; + } + const mid = Math.floor((low + high) / 2); + buildInner(nodeIndex * 2, low, mid); + buildInner(nodeIndex * 2 + 1, mid + 1, high); + segValues[nodeIndex] = (segValues[nodeIndex * 2] ?? 0) + (segValues[nodeIndex * 2 + 1] ?? 0); + } + + buildInner(1, 0, arrayLength - 1); + + function collectNodes(nodeIndex: number, low: number, high: number): void { + const maxIndex = Math.ceil(Math.log2(arrayLength)) + 2; + if (nodeIndex > Math.pow(2, maxIndex) || segValues[nodeIndex] === undefined) return; + + const parentIndex = Math.floor(nodeIndex / 2); + treeNodes.push({ + id: `seg${nodeIndex}`, + value: segValues[nodeIndex] ?? 0, + parentId: nodeIndex === 1 ? null : `seg${parentIndex}`, + leftChildId: low < high ? `seg${nodeIndex * 2}` : null, + rightChildId: low < high ? `seg${nodeIndex * 2 + 1}` : null, + state: "default", + position: posMap.get(nodeIndex) ?? { x: 200, y: 40 }, + }); + + if (low < high) { + const mid = Math.floor((low + high) / 2); + collectNodes(nodeIndex * 2, low, mid); + collectNodes(nodeIndex * 2 + 1, mid + 1, high); + } + } + + assignPos(1, 0, 20, 480); + collectNodes(1, 0, arrayLength - 1); + return treeNodes; +} + +export function generateSegmentTreeRangeSumSteps(input: SegmentTreeRangeSumInput): ExecutionStep[] { + const { array, queries } = input; + const arrayLength = array.length; + + const segValues: number[] = new Array(4 * arrayLength).fill(0); + const initialNodes: TreeNode[] = []; + + const tracker = new AdvancedTreeTracker(initialNodes, "seg1", SEG_SUM_LINE_MAP); + tracker.initialize("Segment Tree Range Sum", { array, queries }); + + function updateTrackerTree(): void { + const treeNodes = buildSegTreeNodes(array); + // Sync current seg values into nodes + for (const treeNode of treeNodes) { + const idx = parseInt(treeNode.id.replace("seg", ""), 10); + treeNode.value = segValues[idx] ?? 0; + } + tracker.updateNodes(treeNodes, "seg1"); + } + + function buildRecursive(nodeIndex: number, low: number, high: number): void { + if (low === high) { + segValues[nodeIndex] = array[low] ?? 0; + updateTrackerTree(); + tracker.buildNode(`seg${nodeIndex}`, segValues[nodeIndex] ?? 0, { + nodeIndex, + range: [low, high], + value: segValues[nodeIndex], + }); + return; + } + const mid = Math.floor((low + high) / 2); + buildRecursive(nodeIndex * 2, low, mid); + buildRecursive(nodeIndex * 2 + 1, mid + 1, high); + segValues[nodeIndex] = (segValues[nodeIndex * 2] ?? 0) + (segValues[nodeIndex * 2 + 1] ?? 0); + updateTrackerTree(); + tracker.updateSegment(`seg${nodeIndex}`, segValues[nodeIndex] ?? 0, { + nodeIndex, + range: [low, high], + leftSum: segValues[nodeIndex * 2], + rightSum: segValues[nodeIndex * 2 + 1], + }); + } + + buildRecursive(1, 0, arrayLength - 1); + + for (const [queryLow, queryHigh] of queries) { + const traverseQuery = (nodeIndex: number, low: number, high: number): number => { + tracker.queryRange(`seg${nodeIndex}`, queryLow, queryHigh, { + nodeIndex, + range: [low, high], + queryBounds: [queryLow, queryHigh], + }); + if (queryLow > high || queryHigh < low) return 0; + if (queryLow <= low && high <= queryHigh) return segValues[nodeIndex] ?? 0; + const mid = Math.floor((low + high) / 2); + const leftResult = traverseQuery(nodeIndex * 2, low, mid); + const rightResult = traverseQuery(nodeIndex * 2 + 1, mid + 1, high); + return leftResult + rightResult; + }; + + const result = traverseQuery(1, 0, arrayLength - 1); + tracker.complete({ queryRange: [queryLow, queryHigh], result }); + } + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/TreeToDoublyLinkedListPipeline.stories.tsx b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/TreeToDoublyLinkedListPipeline.stories.tsx new file mode 100644 index 00000000..cc785233 --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/TreeToDoublyLinkedListPipeline.stories.tsx @@ -0,0 +1,107 @@ +/** + * Storybook pipeline stories for BST to Doubly Linked List. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeNode, TreeVisualState } from "@/types"; +import { generateTreeToDoublyLinkedListSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Tree to Doubly Linked List", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — BST before conversion */ +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +/** Mid-operation — partial DLL formed */ +export const MidOperation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Operation complete — full circular DLL */ +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/educational.ts b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/educational.ts new file mode 100644 index 00000000..34b7214b --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const treeToDoublyLinkedListEducational: EducationalContent = { + overview: + "**BST to Sorted Circular Doubly Linked List** converts a binary search tree into a sorted circular doubly-linked list **in-place** — without allocating any new nodes. The key insight is that an in-order traversal of a BST produces nodes in sorted order, and the `left`/`right` pointers can be repurposed as `prev`/`next` DLL pointers respectively.", + + howItWorks: + "The algorithm performs an in-order traversal while maintaining a `tail` pointer to the last processed node:\n\n" + + "1. Recurse into the left subtree.\n" + + "2. **Link** the current node after `tail`: `tail.right = node; node.left = tail`.\n" + + "3. Advance `tail` to the current node.\n" + + "4. Recurse into the right subtree.\n\n" + + "After traversal, close the circle: `tail.right = head; head.left = tail`.\n\n" + + "**Result:** The BST becomes a sorted circular DLL where `left = prev` and `right = next`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)`** — the recursion stack reaches depth `h` (tree height).", + + bestAndWorstCase: + "**Best case:** Balanced BST — `O(log n)` stack depth.\n\n" + + "**Worst case:** Degenerate BST (linear chain) — `O(n)` stack depth.", + + realWorldUses: [ + "**Memory-efficient conversion:** Convert a BST to a DLL without extra node allocation.", + "**Doubly-linked sorted structure:** Enables O(1) insertion at known positions after conversion.", + "**Interview problems:** Classic in-place pointer manipulation question (Google Leetcode).", + "**Database cursors:** Efficiently iterate forward and backward through sorted records.", + ], + + strengthsAndLimitations: { + strengths: [ + "In-place — no additional memory beyond the recursion stack.", + "Produces a sorted list in one O(n) pass.", + "The circular linkage enables O(1) access to both head and tail.", + ], + limitations: [ + "Destroys the original tree structure — the BST invariant is lost after conversion.", + "Recursive — can stack overflow on very deep trees.", + "Requires a BST specifically — does not produce a sorted list from an arbitrary tree.", + ], + }, + + whenToUseIt: + "Use this when you need to convert a BST to a sorted DLL without allocating new nodes. If you need to keep the BST intact, create a new DLL by walking the tree in-order instead.", +}; diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/index.ts b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/index.ts new file mode 100644 index 00000000..e91d13cf --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/index.ts @@ -0,0 +1,129 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { treeToDoublyLinkedList } from "./sources/tree-to-doubly-linked-list.ts?fn"; +import { generateTreeToDoublyLinkedListSteps } from "./step-generator"; +import type { TreeToDoublyLinkedListInput } from "./step-generator"; +import { treeToDoublyLinkedListEducational } from "./educational"; + +import typescriptSource from "./sources/tree-to-doubly-linked-list.ts?raw"; +import pythonSource from "./sources/tree-to-doubly-linked-list.py?raw"; +import javaSource from "./sources/TreeToDoublyLinkedList.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface DLLNode { + value: number; + left: DLLNode | null; + right: DLLNode | null; +} + +function executeTreeToDoublyLinkedList(input: TreeToDoublyLinkedListInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toDLLNode(id: string | null): DLLNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toDLLNode(node.leftChildId), + right: toDLLNode(node.rightChildId), + }; + } + const root = toDLLNode(input.rootId); + const head = treeToDoublyLinkedList(root) as DLLNode | null; + const result: number[] = []; + let current = head; + while (current) { + result.push(current.value); + current = current.right; + if (current === head) break; // stop at circular reference + } + return result; +} + +const treeToDoublyLinkedListDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.TREE_TO_DOUBLY_LINKED_LIST!, + name: "BST to Doubly Linked List", + category: CATEGORY.TREES!, + technique: "advanced", + description: + "Convert a BST into a sorted circular doubly linked list in-place by repurposing tree pointers during in-order traversal", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeTreeToDoublyLinkedList, + generateSteps: generateTreeToDoublyLinkedListSteps, + educational: treeToDoublyLinkedListEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(treeToDoublyLinkedListDefinition); diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/TreeToDoublyLinkedList.java b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/TreeToDoublyLinkedList.java new file mode 100644 index 00000000..1cfabff4 --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/TreeToDoublyLinkedList.java @@ -0,0 +1,39 @@ +// BST to Sorted Circular Doubly Linked List — in-place pointer manipulation + +class DLLNode { + int value; + DLLNode left, right; + DLLNode(int v) { value = v; } +} + +class TreeToDoublyLinkedList { + private DLLNode head = null; + private DLLNode tail = null; + + private void inorder(DLLNode node) { + if (node == null) return; // @step:initialize + + inorder(node.left); // @step:traverse-left + + if (tail == null) { + head = node; // @step:visit + } else { + tail.right = node; // @step:visit + node.left = tail; // @step:visit + } + tail = node; // @step:visit + + inorder(node.right); // @step:traverse-right + } + + public DLLNode treeToDoublyLinkedList(DLLNode root) { + if (root == null) return null; // @step:initialize + head = null; tail = null; // @step:initialize + inorder(root); + if (head != null && tail != null) { + tail.right = head; // @step:visit + head.left = tail; // @step:visit + } + return head; // @step:complete + } +} diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/tree-to-doubly-linked-list.py b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/tree-to-doubly-linked-list.py new file mode 100644 index 00000000..513c8561 --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/tree-to-doubly-linked-list.py @@ -0,0 +1,37 @@ +# BST to Sorted Circular Doubly Linked List — in-place pointer manipulation + +class DLLNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def tree_to_doubly_linked_list(root) -> object: + if not root: + return None # @step:initialize + + head = [None] # @step:initialize + tail = [None] # @step:initialize + + def inorder(node): + if not node: + return # @step:initialize + + inorder(node.left) # @step:traverse-left + + if not tail[0]: + head[0] = node # @step:visit + else: + tail[0].right = node # @step:visit + node.left = tail[0] # @step:visit + tail[0] = node # @step:visit + + inorder(node.right) # @step:traverse-right + + inorder(root) + + if head[0] and tail[0]: + tail[0].right = head[0] # @step:visit + head[0].left = tail[0] # @step:visit + + return head[0] # @step:complete diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/tree-to-doubly-linked-list.ts b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/tree-to-doubly-linked-list.ts new file mode 100644 index 00000000..5e04df29 --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/sources/tree-to-doubly-linked-list.ts @@ -0,0 +1,41 @@ +// BST to Sorted Circular Doubly Linked List — in-place pointer manipulation + +interface DLLNode { + value: number; + left: DLLNode | null; + right: DLLNode | null; +} + +function treeToDoublyLinkedList(root: DLLNode | null): DLLNode | null { + if (!root) return null; // @step:initialize + + let head: DLLNode | null = null; // @step:initialize + let tail: DLLNode | null = null; // @step:initialize + + function inorder(node: DLLNode | null): void { + if (!node) return; // @step:initialize + + inorder(node.left); // @step:traverse-left + + // Visit: connect current node to the doubly linked list + if (!tail) { + head = node; // @step:visit + } else { + tail.right = node; // @step:visit + node.left = tail; // @step:visit + } + tail = node; // @step:visit + + inorder(node.right); // @step:traverse-right + } + + inorder(root); + + // Close the circular link + if (head && tail) { + tail.right = head; // @step:visit + head.left = tail; // @step:visit + } + + return head; // @step:complete +} diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/step-generator.test.ts b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/step-generator.test.ts new file mode 100644 index 00000000..799d2b12 --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateTreeToDoublyLinkedListSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateTreeToDoublyLinkedListSteps", () => { + it("produces steps for 7-node BST", () => { + const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize step", () => { + const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete step", () => { + const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes", () => { + const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateTreeToDoublyLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/step-generator.ts b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/step-generator.ts new file mode 100644 index 00000000..691c76ff --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/step-generator.ts @@ -0,0 +1,55 @@ +/** Step generator for Tree to Doubly Linked List — in-order conversion. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { AdvancedTreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const TREE_DLL_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.TREE_TO_DOUBLY_LINKED_LIST!); + +export interface TreeToDoublyLinkedListInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateTreeToDoublyLinkedListSteps( + input: TreeToDoublyLinkedListInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new AdvancedTreeTracker(nodes, rootId, TREE_DLL_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("BST to Doubly Linked List", { rootId }); + + const dllOrder: string[] = []; + + function inorder(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + if (node.leftChildId) { + inorder(node.leftChildId); + } + + dllOrder.push(nodeId); + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + dllOrder: [...dllOrder], + }); + + if (node.rightChildId) { + inorder(node.rightChildId); + } + } + + inorder(rootId); + + tracker.complete({ + result: `DLL with ${dllOrder.length} nodes in sorted order`, + dllOrder, + }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/advanced/tree-to-doubly-linked-list/tree-to-doubly-linked-list.test.ts b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/tree-to-doubly-linked-list.test.ts new file mode 100644 index 00000000..6afd2c8a --- /dev/null +++ b/src/algorithms/trees/advanced/tree-to-doubly-linked-list/tree-to-doubly-linked-list.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { treeToDoublyLinkedList } from "./sources/tree-to-doubly-linked-list.ts?fn"; + +interface DLLNode { + value: number; + left: DLLNode | null; + right: DLLNode | null; +} +function node(value: number, left: DLLNode | null = null, right: DLLNode | null = null): DLLNode { + return { value, left, right }; +} + +describe("treeToDoublyLinkedList", () => { + it("returns null for null input", () => { + expect(treeToDoublyLinkedList(null)).toBeNull(); + }); + + it("handles single node", () => { + const root = node(5); + const head = treeToDoublyLinkedList(root) as DLLNode; + expect(head.value).toBe(5); + expect(head.right).toBe(head); // circular + expect(head.left).toBe(head); + }); + + it("produces sorted DLL from a 3-node BST", () => { + const root = node(2, node(1), node(3)); + const head = treeToDoublyLinkedList(root) as DLLNode; + expect(head.value).toBe(1); + expect(head.right?.value).toBe(2); + expect(head.right?.right?.value).toBe(3); + // Circular: tail.right === head + expect(head.right?.right?.right).toBe(head); + }); + + it("produces sorted DLL from 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const head = treeToDoublyLinkedList(root) as DLLNode; + const values: number[] = []; + let current: DLLNode = head; + for (let step = 0; step < 7; step++) { + values.push(current.value); + current = current.right!; + } + expect(values).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/BSTDeleteIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-delete-iterative/BSTDeleteIterativePipeline.stories.tsx new file mode 100644 index 00000000..a371cc29 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/BSTDeleteIterativePipeline.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstDeleteIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstDeleteIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + deleteValue: 2, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Delete (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/bst-delete-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-delete-iterative/bst-delete-iterative.test.ts new file mode 100644 index 00000000..774bb50b --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/bst-delete-iterative.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { bstDeleteIterative } from "./sources/bst-delete-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstDeleteIterative", () => { + it("deletes a leaf node", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const result = bstDeleteIterative(tree, 7) as BSTNode | null; + expect(result?.right?.right).toBeNull(); + }); + it("deletes a node with two children", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const result = bstDeleteIterative(tree, 6) as BSTNode | null; + expect(result?.right?.value).toBe(7); + }); + it("returns null when deleting only node", () => { + expect(bstDeleteIterative(node(5), 5)).toBeNull(); + }); + it("returns unchanged tree when value absent", () => { + const tree = node(4, node(2), node(6)); + const result = bstDeleteIterative(tree, 99) as BSTNode | null; + expect(result?.value).toBe(4); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-delete-iterative/educational.ts new file mode 100644 index 00000000..9d300036 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/educational.ts @@ -0,0 +1,30 @@ +import type { EducationalContent } from "@/types"; + +export const bstDeleteIterativeEducational: EducationalContent = { + overview: + "**BST Delete (Iterative)** removes a node without recursion by explicitly tracking the parent pointer during the search phase.\n\nHandles the same three cases as the recursive version: leaf, one child, and two children (with in-order successor).", + + howItWorks: + "1. Walk with a `parent` pointer and `current` pointer until `current.value === deleteValue` or `current === null`.\n" + + "2. **Two children:** Find the successor (leftmost of right subtree), copy its value, reassign `current` to the successor.\n" + + "3. Determine the child to promote (left or right, or `null` for a leaf).\n" + + "4. Update `parent.left` or `parent.right` to point to the promoted child.", + + timeAndSpaceComplexity: "**Time: `O(h)`**\n\n**Space: `O(1)`** — no call stack.", + + bestAndWorstCase: + "**Best case:** Target at root with no children — O(1).\n\n**Worst case:** Degenerate tree — O(n).", + + realWorldUses: [ + "**Embedded BST implementations:** Where recursion overhead must be avoided.", + "**In-place data structure management:** Iterative deletion allows direct pointer manipulation.", + ], + + strengthsAndLimitations: { + strengths: ["O(1) auxiliary space.", "No function-call overhead."], + limitations: ["More pointer bookkeeping than the recursive version — harder to read."], + }, + + whenToUseIt: + "Choose iterative delete when the BST may be very deep or when stack usage must be bounded.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-delete-iterative/index.ts new file mode 100644 index 00000000..506ff4c2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstDeleteIterative } from "./sources/bst-delete-iterative.ts?fn"; +import { generateBstDeleteIterativeSteps } from "./step-generator"; +import type { BstDeleteIterativeInput } from "./step-generator"; +import { bstDeleteIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-delete-iterative.ts?raw"; +import pythonSource from "./sources/bst-delete-iterative.py?raw"; +import javaSource from "./sources/BSTDeleteIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstDeleteIterative(input: BstDeleteIterativeInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstDeleteIterative(root, input.deleteValue) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstDeleteIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_DELETE_ITERATIVE!, + name: "BST Delete (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Iterative BST deletion: find target with parent tracking, handle all 3 cases", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", deleteValue: 2 }, + }, + execute: executeBstDeleteIterative, + generateSteps: generateBstDeleteIterativeSteps, + educational: bstDeleteIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstDeleteIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/BSTDeleteIterative.java b/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/BSTDeleteIterative.java new file mode 100644 index 00000000..62ba08e9 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/BSTDeleteIterative.java @@ -0,0 +1,52 @@ +// BST Delete (Iterative) — 3 cases using while loop with parent tracking + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTDeleteIterative { + public BSTNode bstDeleteIterative(BSTNode root, int deleteValue) { + BSTNode parent = null; // @step:initialize + BSTNode current = root; + + // Find the node to delete and its parent + while (current != null && current.value != deleteValue) { + parent = current; + if (deleteValue < current.value) { + current = current.left; // @step:search-node + } else { + current = current.right; // @step:search-node + } + } + + if (current == null) return root; // @step:complete — value not found + + // Case: node has two children — replace with inorder successor + if (current.left != null && current.right != null) { + BSTNode successorParent = current; + BSTNode successor = current.right; + while (successor.left != null) { + successorParent = successor; + successor = successor.left; // @step:search-node + } + current.value = successor.value; // @step:delete-child + current = successor; + parent = successorParent; + } + + // Case: node has 0 or 1 child + BSTNode child = current.left != null ? current.left : current.right; + + if (parent == null) return child; // @step:delete-child — deleting root + + if (parent.left == current) { + parent.left = child; // @step:delete-child + } else { + parent.right = child; // @step:delete-child + } + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/bst-delete-iterative.py b/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/bst-delete-iterative.py new file mode 100644 index 00000000..53564aae --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/bst-delete-iterative.py @@ -0,0 +1,47 @@ +# BST Delete (Iterative) — 3 cases using while loop with parent tracking + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_delete_iterative(root, delete_value: int): + parent = None # @step:initialize + current = root + + # Find the node to delete and its parent + while current is not None and current.value != delete_value: + parent = current + if delete_value < current.value: + current = current.left # @step:search-node + else: + current = current.right # @step:search-node + + if current is None: # @step:complete + return root # value not found + + # Case: node has two children — replace with inorder successor + if current.left is not None and current.right is not None: + successor_parent = current + successor = current.right + while successor.left is not None: + successor_parent = successor + successor = successor.left # @step:search-node + current.value = successor.value # @step:delete-child + # Now delete the successor + current = successor + parent = successor_parent + + # Case: node has 0 or 1 child + child = current.left if current.left is not None else current.right + + if parent is None: # @step:delete-child + return child # deleting root + + if parent.left == current: + parent.left = child # @step:delete-child + else: + parent.right = child # @step:delete-child + + return root # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/bst-delete-iterative.ts b/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/bst-delete-iterative.ts new file mode 100644 index 00000000..61836dd8 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/sources/bst-delete-iterative.ts @@ -0,0 +1,51 @@ +// BST Delete (Iterative) — 3 cases using while loop with parent tracking + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstDeleteIterative(root: BSTNode | null, deleteValue: number): BSTNode | null { + let parent: BSTNode | null = null; // @step:initialize + let current = root; + + // Find the node to delete and its parent + while (current !== null && current.value !== deleteValue) { + parent = current; + if (deleteValue < current.value) { + current = current.left; // @step:search-node + } else { + current = current.right; // @step:search-node + } + } + + if (current === null) return root; // @step:complete — value not found + + // Case: node has two children — replace with inorder successor + if (current.left !== null && current.right !== null) { + let successorParent: BSTNode = current; + let successor: BSTNode = current.right; + while (successor.left !== null) { + successorParent = successor; + successor = successor.left; // @step:search-node + } + current.value = successor.value; // @step:delete-child + // Now delete the successor + current = successor; + parent = successorParent; + } + + // Case: node has 0 or 1 child + const child = current.left !== null ? current.left : current.right; + + if (parent === null) return child; // @step:delete-child — deleting root + + if (parent.left === current) { + parent.left = child; // @step:delete-child + } else { + parent.right = child; // @step:delete-child + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-delete-iterative/step-generator.test.ts new file mode 100644 index 00000000..42ca46b6 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/step-generator.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstDeleteIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstDeleteIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstDeleteIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + deleteValue: 2, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstDeleteIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + deleteValue: 2, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstDeleteIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + deleteValue: 2, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstDeleteIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + deleteValue: 2, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstDeleteIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + deleteValue: 2, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-delete-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-delete-iterative/step-generator.ts new file mode 100644 index 00000000..3dd4d778 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete-iterative/step-generator.ts @@ -0,0 +1,88 @@ +/** Step generator for BST Delete (Iterative) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_DELETE_ITERATIVE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_DELETE_ITERATIVE!); + +export interface BstDeleteIterativeInput { + nodes: TreeNode[]; + rootId: string; + deleteValue: number; +} + +export function generateBstDeleteIterativeSteps(input: BstDeleteIterativeInput): ExecutionStep[] { + const { nodes, rootId, deleteValue } = input; + + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker(mutableNodes, rootId, BST_DELETE_ITERATIVE_LINE_MAP); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(deleteValue, { rootId, deleteValue }); + + let parentId: string | null = null; + let currentId: string | null = rootId; + + // Find the node to delete + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + deleteValue, + nodeValue: node.value, + }); + + if (node.value === deleteValue) break; + + parentId = currentId; + currentId = deleteValue < node.value ? node.leftChildId : node.rightChildId; + } + + if (currentId !== null) { + const targetNode = nodeMap.get(currentId); + if (targetNode) { + // Two children case: find inorder successor + if (targetNode.leftChildId && targetNode.rightChildId) { + let successorParentId = currentId; + let successorId = targetNode.rightChildId; + let successorNode = nodeMap.get(successorId); + + while (successorNode?.leftChildId) { + successorParentId = successorId; + successorId = successorNode.leftChildId; + successorNode = nodeMap.get(successorId); + } + + if (successorNode) { + tracker.findSuccessor(successorId, { successorId, successorValue: successorNode.value }); + targetNode.value = successorNode.value; + parentId = successorParentId; + currentId = successorId; + } + } + + // Single child or leaf deletion + tracker.deleteNode(currentId, { deleteValue, case: "leaf-or-one-child" }); + + const nodeToDelete = nodeMap.get(currentId); + const childId = nodeToDelete?.leftChildId ?? nodeToDelete?.rightChildId ?? null; + const parentNode = parentId ? nodeMap.get(parentId) : null; + + if (parentNode) { + if (parentNode.leftChildId === currentId) { + parentNode.leftChildId = childId; + } else { + parentNode.rightChildId = childId; + } + } + } + } + + tracker.complete({ deleteValue, treeSize: mutableNodes.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-delete/BSTDeletePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-delete/BSTDeletePipeline.stories.tsx new file mode 100644 index 00000000..e25a4a3c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/BSTDeletePipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstDeleteSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstDeleteSteps({ nodes: defaultNodes, rootId: "n4", deleteValue: 2 }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Delete (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-delete/bst-delete.test.ts b/src/algorithms/trees/bst-operations/bst-delete/bst-delete.test.ts new file mode 100644 index 00000000..50dd9c30 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/bst-delete.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { bstDelete } from "./sources/bst-delete.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstDelete", () => { + it("deletes a leaf node", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const result = bstDelete(tree, 1) as BSTNode | null; + expect(result?.left?.left).toBeNull(); + }); + it("deletes a node with one child", () => { + const tree = node(4, node(2, node(1)), node(6)); + const result = bstDelete(tree, 2) as BSTNode | null; + expect(result?.left?.value).toBe(1); + }); + it("deletes a node with two children", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const result = bstDelete(tree, 4) as BSTNode | null; + expect(result).not.toBeNull(); + }); + it("returns null when deleting the only node", () => { + expect(bstDelete(node(5), 5)).toBeNull(); + }); + it("returns tree unchanged when value not found", () => { + const tree = node(4, node(2), node(6)); + const result = bstDelete(tree, 99) as BSTNode | null; + expect(result?.value).toBe(4); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-delete/educational.ts b/src/algorithms/trees/bst-operations/bst-delete/educational.ts new file mode 100644 index 00000000..b3d92888 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/educational.ts @@ -0,0 +1,34 @@ +import type { EducationalContent } from "@/types"; + +export const bstDeleteEducational: EducationalContent = { + overview: + "**BST Delete (Recursive)** removes a node while preserving the BST property. There are three distinct cases depending on the deleted node's children:\n1. **Leaf:** Simply remove it.\n2. **One child:** Replace the node with its only child.\n3. **Two children:** Replace the node's value with its in-order successor (smallest value in the right subtree), then delete the successor.", + + howItWorks: + "The algorithm searches for the target recursively. Once found:\n- **Leaf (no children):** Return `null` — the parent's pointer becomes `null`.\n- **One child:** Return the non-null child — the parent links directly to it.\n- **Two children:** Find the in-order successor (leftmost node in right subtree), copy its value into the target node, then recursively delete the successor from the right subtree.\n\nThe 'copy-value then delete' approach avoids restructuring large subtrees.", + + timeAndSpaceComplexity: + "**Time: `O(h)`** — search + successor find.\n\n**Space: `O(h)`** — call stack.", + + bestAndWorstCase: + "**Best case:** Target is a leaf or has one child — O(log n) on a balanced tree.\n\n**Worst case:** Degenerate tree — O(n).", + + realWorldUses: [ + "**Event scheduling:** Remove a processed event from an ordered BST of future events.", + "**Cache eviction:** Remove stale entries from an LRU-ordered BST.", + ], + + strengthsAndLimitations: { + strengths: [ + "Handles all three deletion cases cleanly and recursively.", + "In-order successor replacement is the canonical correct approach.", + ], + limitations: [ + "Two-children case requires finding the successor — an extra O(h) traversal.", + "Does not rebalance — tree may become lopsided after many deletions.", + ], + }, + + whenToUseIt: + "Use for ordered dynamic collections where deletions are occasional. For frequent deletions with performance guarantees, use a self-balancing BST.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-delete/index.ts b/src/algorithms/trees/bst-operations/bst-delete/index.ts new file mode 100644 index 00000000..7a290790 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/index.ts @@ -0,0 +1,118 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstDelete } from "./sources/bst-delete.ts?fn"; +import { generateBstDeleteSteps } from "./step-generator"; +import type { BstDeleteInput } from "./step-generator"; +import { bstDeleteEducational } from "./educational"; + +import typescriptSource from "./sources/bst-delete.ts?raw"; +import pythonSource from "./sources/bst-delete.py?raw"; +import javaSource from "./sources/BSTDelete.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstDelete(input: BstDeleteInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstDelete(root, input.deleteValue) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstDeleteDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_DELETE!, + name: "BST Delete (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Recursive BST deletion: handle leaf, one-child, and two-children cases with inorder successor", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", deleteValue: 2 }, + }, + execute: executeBstDelete, + generateSteps: generateBstDeleteSteps, + educational: bstDeleteEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstDeleteDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-delete/sources/BSTDelete.java b/src/algorithms/trees/bst-operations/bst-delete/sources/BSTDelete.java new file mode 100644 index 00000000..fcd392be --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/sources/BSTDelete.java @@ -0,0 +1,36 @@ +// BST Delete (Recursive) — 3 cases: leaf, one child, two children with inorder successor + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTDelete { + public BSTNode bstDelete(BSTNode root, int deleteValue) { + if (root == null) return null; // @step:initialize + + if (deleteValue < root.value) { + // Target is in the left subtree + root.left = bstDelete(root.left, deleteValue); // @step:search-node + } else if (deleteValue > root.value) { + // Target is in the right subtree + root.right = bstDelete(root.right, deleteValue); // @step:search-node + } else { + // Found the node to delete + if (root.left == null) return root.right; // @step:delete-child + if (root.right == null) return root.left; // @step:delete-child + + // Two children: find inorder successor (smallest in right subtree) + BSTNode successor = root.right; + while (successor.left != null) { + successor = successor.left; // @step:search-node + } + // Replace value with successor's value, then delete the successor + root.value = successor.value; // @step:delete-child + root.right = bstDelete(root.right, successor.value); + } + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-delete/sources/bst-delete.py b/src/algorithms/trees/bst-operations/bst-delete/sources/bst-delete.py new file mode 100644 index 00000000..78f719af --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/sources/bst-delete.py @@ -0,0 +1,34 @@ +# BST Delete (Recursive) — 3 cases: leaf, one child, two children with inorder successor + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_delete(root, delete_value: int): + if root is None: # @step:initialize + return None + + if delete_value < root.value: + # Target is in the left subtree + root.left = bst_delete(root.left, delete_value) # @step:search-node + elif delete_value > root.value: + # Target is in the right subtree + root.right = bst_delete(root.right, delete_value) # @step:search-node + else: + # Found the node to delete + if root.left is None: # @step:delete-child + return root.right + if root.right is None: # @step:delete-child + return root.left + + # Two children: find inorder successor (smallest in right subtree) + successor = root.right + while successor.left is not None: + successor = successor.left # @step:search-node + # Replace value with successor's value, then delete the successor + root.value = successor.value # @step:delete-child + root.right = bst_delete(root.right, successor.value) + + return root # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-delete/sources/bst-delete.ts b/src/algorithms/trees/bst-operations/bst-delete/sources/bst-delete.ts new file mode 100644 index 00000000..96c4c353 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/sources/bst-delete.ts @@ -0,0 +1,34 @@ +// BST Delete (Recursive) — 3 cases: leaf, one child, two children with inorder successor + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstDelete(root: BSTNode | null, deleteValue: number): BSTNode | null { + if (root === null) return null; // @step:initialize + + if (deleteValue < root.value) { + // Target is in the left subtree + root.left = bstDelete(root.left, deleteValue); // @step:search-node + } else if (deleteValue > root.value) { + // Target is in the right subtree + root.right = bstDelete(root.right, deleteValue); // @step:search-node + } else { + // Found the node to delete + if (root.left === null) return root.right; // @step:delete-child + if (root.right === null) return root.left; // @step:delete-child + + // Two children: find inorder successor (smallest in right subtree) + let successor = root.right; + while (successor.left !== null) { + successor = successor.left; // @step:search-node + } + // Replace value with successor's value, then delete the successor + root.value = successor.value; // @step:delete-child + root.right = bstDelete(root.right, successor.value); + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-delete/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-delete/step-generator.test.ts new file mode 100644 index 00000000..b31afc43 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstDeleteSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstDeleteSteps", () => { + it("produces steps", () => { + const steps = generateBstDeleteSteps({ nodes: defaultNodes, rootId: "n4", deleteValue: 2 }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstDeleteSteps({ nodes: defaultNodes, rootId: "n4", deleteValue: 2 }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstDeleteSteps({ nodes: defaultNodes, rootId: "n4", deleteValue: 2 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstDeleteSteps({ nodes: defaultNodes, rootId: "n4", deleteValue: 2 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstDeleteSteps({ nodes: defaultNodes, rootId: "n4", deleteValue: 2 }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-delete/step-generator.ts b/src/algorithms/trees/bst-operations/bst-delete/step-generator.ts new file mode 100644 index 00000000..1a427483 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-delete/step-generator.ts @@ -0,0 +1,89 @@ +/** Step generator for BST Delete (Recursive) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_DELETE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_DELETE!); + +export interface BstDeleteInput { + nodes: TreeNode[]; + rootId: string; + deleteValue: number; +} + +export function generateBstDeleteSteps(input: BstDeleteInput): ExecutionStep[] { + const { nodes, rootId, deleteValue } = input; + + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker(mutableNodes, rootId, BST_DELETE_LINE_MAP); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(deleteValue, { rootId, deleteValue }); + + function findSuccessor(nodeId: string): string { + let successorId = nodeId; + const node = nodeMap.get(nodeId); + if (!node?.leftChildId) return successorId; + let leftId: string | null = node.leftChildId; + while (leftId) { + successorId = leftId; + const leftNode = nodeMap.get(leftId); + leftId = leftNode?.leftChildId ?? null; + } + return successorId; + } + + function deleteNode(nodeId: string | null): string | null { + if (!nodeId) return null; + const node = nodeMap.get(nodeId); + if (!node) return null; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + deleteValue, + nodeValue: node.value, + }); + + if (deleteValue < node.value) { + const newLeft = deleteNode(node.leftChildId); + node.leftChildId = newLeft; + return nodeId; + } else if (deleteValue > node.value) { + const newRight = deleteNode(node.rightChildId); + node.rightChildId = newRight; + return nodeId; + } else { + // Found — handle 3 cases + if (!node.leftChildId && !node.rightChildId) { + tracker.deleteNode(nodeId, { case: "leaf", deleteValue }); + return null; + } + if (!node.leftChildId) { + tracker.deleteNode(nodeId, { case: "one-child-right", deleteValue }); + return node.rightChildId; + } + if (!node.rightChildId) { + tracker.deleteNode(nodeId, { case: "one-child-left", deleteValue }); + return node.leftChildId; + } + + // Two children — find inorder successor + const successorId = findSuccessor(node.rightChildId); + const successor = nodeMap.get(successorId); + if (successor) { + tracker.findSuccessor(successorId, { successorId, successorValue: successor.value }); + node.value = successor.value; + const newRight = deleteNode(node.rightChildId); + node.rightChildId = newRight; + } + return nodeId; + } + } + + deleteNode(rootId); + tracker.complete({ deleteValue, treeSize: mutableNodes.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/BSTFloorCeilIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/BSTFloorCeilIterativePipeline.stories.tsx new file mode 100644 index 00000000..1da580bd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/BSTFloorCeilIterativePipeline.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstFloorCeilIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstFloorCeilIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 4, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Floor and Ceil (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/bst-floor-ceil-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/bst-floor-ceil-iterative.test.ts new file mode 100644 index 00000000..a1297571 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/bst-floor-ceil-iterative.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { bstFloorCeilIterative } from "./sources/bst-floor-ceil-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstFloorCeilIterative", () => { + it("returns exact match for existing value", () => { + const result = bstFloorCeilIterative(tree, 3) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBe(3); + expect(result.ceil).toBe(3); + }); + it("finds floor and ceil for non-existing value between nodes", () => { + const result = bstFloorCeilIterative(tree, 4) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBe(4); + expect(result.ceil).toBe(4); + }); + it("returns null for value below all nodes", () => { + const result = bstFloorCeilIterative(tree, 0) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBeNull(); + expect(result.ceil).toBe(1); + }); + it("handles null tree", () => { + const result = bstFloorCeilIterative(null, 5) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBeNull(); + expect(result.ceil).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/educational.ts new file mode 100644 index 00000000..477c4662 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/educational.ts @@ -0,0 +1,31 @@ +import type { EducationalContent } from "@/types"; + +export const bstFloorCeilIterativeEducational: EducationalContent = { + overview: + "**BST Floor & Ceil (Iterative)** finds both boundary values in a single while-loop pass, tracking the best candidate seen so far for each:\n- **Floor candidate:** last node where `node.value ≤ target`\n- **Ceil candidate:** last node where `node.value ≥ target`", + + howItWorks: + "Start at root. At each node:\n- If `value === target`: exact match — return it as both floor and ceil immediately.\n- If `target < value`: current node is a ceil candidate; move left to find a smaller ceil.\n- If `target > value`: current node is a floor candidate; move right to find a larger floor.\n\nWhen the loop ends, the tracked candidates are the answer.", + + timeAndSpaceComplexity: + "**Time: `O(h)`** — single pass from root to a leaf.\n\n**Space: `O(1)`** — two pointer variables, no call stack.", + + bestAndWorstCase: + "**Best case:** Exact match at root — O(1).\n\n**Worst case:** Target outside the range of all stored values — must walk to a leaf.", + + realWorldUses: [ + "**Memory-constrained lookups:** O(1) space makes this suitable for embedded or real-time environments.", + "**Interval scheduling:** Quickly find the next available slot ≥ a given start time.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) space — no recursion.", + "Finds both floor and ceil in a single traversal pass.", + ], + limitations: ["Slightly more variable management than the recursive version."], + }, + + whenToUseIt: + "Use the iterative variant over the recursive one when O(1) space is a hard requirement or when the tree may be deep.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/index.ts new file mode 100644 index 00000000..df9abfeb --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/index.ts @@ -0,0 +1,122 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstFloorCeilIterative } from "./sources/bst-floor-ceil-iterative.ts?fn"; +import { generateBstFloorCeilIterativeSteps } from "./step-generator"; +import type { BstFloorCeilIterativeInput } from "./step-generator"; +import { bstFloorCeilIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-floor-ceil-iterative.ts?raw"; +import pythonSource from "./sources/bst-floor-ceil-iterative.py?raw"; +import javaSource from "./sources/BSTFloorCeilIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstFloorCeilIterative(input: BstFloorCeilIterativeInput): { + floor: number | null; + ceil: number | null; +} { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstFloorCeilIterative(root, input.targetValue) as { + floor: number | null; + ceil: number | null; + }; +} + +const bstFloorCeilIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_FLOOR_CEIL_ITERATIVE!, + name: "BST Floor & Ceil (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Find floor and ceil iteratively: track candidates while walking left or right", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetValue: 4 }, + }, + execute: executeBstFloorCeilIterative, + generateSteps: generateBstFloorCeilIterativeSteps, + educational: bstFloorCeilIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstFloorCeilIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/BSTFloorCeilIterative.java b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/BSTFloorCeilIterative.java new file mode 100644 index 00000000..9810e219 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/BSTFloorCeilIterative.java @@ -0,0 +1,36 @@ +// BST Floor & Ceil (Iterative) — while loop, track best floor/ceil candidates + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTFloorCeilIterative { + public int[] bstFloorCeilIterative(BSTNode root, int target) { + Integer floorValue = null; // @step:initialize + Integer ceilValue = null; + BSTNode current = root; + + while (current != null) { + if (current.value == target) { + // Exact match is both floor and ceil + return new int[]{ current.value, current.value }; // @step:found + } + + if (target < current.value) { + // Current node is a ceil candidate — go left for smaller ceil + ceilValue = current.value; // @step:search-node + current = current.left; + } else { + // Current node is a floor candidate — go right for larger floor + floorValue = current.value; // @step:search-node + current = current.right; + } + } + + int floor = floorValue != null ? floorValue : Integer.MIN_VALUE; + int ceil = ceilValue != null ? ceilValue : Integer.MAX_VALUE; + return new int[]{ floor, ceil }; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/bst-floor-ceil-iterative.py b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/bst-floor-ceil-iterative.py new file mode 100644 index 00000000..fd844ca1 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/bst-floor-ceil-iterative.py @@ -0,0 +1,28 @@ +# BST Floor & Ceil (Iterative) — while loop, track best floor/ceil candidates + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_floor_ceil_iterative(root, target: int) -> dict: + floor_value = None # @step:initialize + ceil_value = None + current = root + + while current is not None: + if current.value == target: + # Exact match is both floor and ceil + return {"floor": current.value, "ceil": current.value} # @step:found + + if target < current.value: + # Current node is a ceil candidate — go left for smaller ceil + ceil_value = current.value # @step:search-node + current = current.left + else: + # Current node is a floor candidate — go right for larger floor + floor_value = current.value # @step:search-node + current = current.right + + return {"floor": floor_value, "ceil": ceil_value} # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/bst-floor-ceil-iterative.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/bst-floor-ceil-iterative.ts new file mode 100644 index 00000000..7b7e998a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/sources/bst-floor-ceil-iterative.ts @@ -0,0 +1,37 @@ +// BST Floor & Ceil (Iterative) — while loop, track best floor/ceil candidates + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +interface FloorCeilResult { + floor: number | null; + ceil: number | null; +} + +function bstFloorCeilIterative(root: BSTNode | null, target: number): FloorCeilResult { + let floorValue: number | null = null; // @step:initialize + let ceilValue: number | null = null; + let current = root; + + while (current !== null) { + if (current.value === target) { + // Exact match is both floor and ceil + return { floor: current.value, ceil: current.value }; // @step:found + } + + if (target < current.value) { + // Current node is a ceil candidate — go left for smaller ceil + ceilValue = current.value; // @step:search-node + current = current.left; + } else { + // Current node is a floor candidate — go right for larger floor + floorValue = current.value; // @step:search-node + current = current.right; + } + } + + return { floor: floorValue, ceil: ceilValue }; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/step-generator.test.ts new file mode 100644 index 00000000..990d9cf2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/step-generator.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstFloorCeilIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstFloorCeilIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstFloorCeilIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 4, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstFloorCeilIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 4, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstFloorCeilIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 4, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstFloorCeilIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 4, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstFloorCeilIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 4, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/step-generator.ts new file mode 100644 index 00000000..4c98ebdf --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil-iterative/step-generator.ts @@ -0,0 +1,61 @@ +/** Step generator for BST Floor & Ceil (Iterative) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_FLOOR_CEIL_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.BST_FLOOR_CEIL_ITERATIVE!, +); + +export interface BstFloorCeilIterativeInput { + nodes: TreeNode[]; + rootId: string; + targetValue: number; +} + +export function generateBstFloorCeilIterativeSteps( + input: BstFloorCeilIterativeInput, +): ExecutionStep[] { + const { nodes, rootId, targetValue } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_FLOOR_CEIL_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(targetValue, { rootId, targetValue }); + + let floorValue: number | null = null; + let ceilValue: number | null = null; + let currentId: string | null = rootId; + + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + targetValue, + floor: floorValue, + ceil: ceilValue, + }); + + if (node.value === targetValue) { + floorValue = node.value; + ceilValue = node.value; + tracker.searchFound(currentId, { floor: floorValue, ceil: ceilValue }); + break; + } + + if (targetValue < node.value) { + ceilValue = node.value; + currentId = node.leftChildId; + } else { + floorValue = node.value; + currentId = node.rightChildId; + } + } + + tracker.complete({ floor: floorValue, ceil: ceilValue, targetValue }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/BSTFloorCeilPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-floor-ceil/BSTFloorCeilPipeline.stories.tsx new file mode 100644 index 00000000..977052d2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/BSTFloorCeilPipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstFloorCeilSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstFloorCeilSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 4 }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Floor and Ceil", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/bst-floor-ceil.test.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil/bst-floor-ceil.test.ts new file mode 100644 index 00000000..5bc5a3fe --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/bst-floor-ceil.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { bstFloorCeil } from "./sources/bst-floor-ceil.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstFloorCeil", () => { + it("returns exact match for existing value", () => { + const result = bstFloorCeil(tree, 5) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBe(5); + expect(result.ceil).toBe(5); + }); + it("returns floor and ceil for non-existing value", () => { + const result = bstFloorCeil(tree, 4) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBe(4); + expect(result.ceil).toBe(4); + }); + it("returns null floor for value less than all nodes", () => { + const result = bstFloorCeil(tree, 0) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBeNull(); + expect(result.ceil).toBe(1); + }); + it("returns null ceil for value greater than all nodes", () => { + const result = bstFloorCeil(tree, 8) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBe(7); + expect(result.ceil).toBeNull(); + }); + it("handles null tree", () => { + const result = bstFloorCeil(null, 5) as { floor: number | null; ceil: number | null }; + expect(result.floor).toBeNull(); + expect(result.ceil).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/educational.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil/educational.ts new file mode 100644 index 00000000..cda1e8d3 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/educational.ts @@ -0,0 +1,35 @@ +import type { EducationalContent } from "@/types"; + +export const bstFloorCeilEducational: EducationalContent = { + overview: + "**BST Floor & Ceil (Recursive)** finds two boundary values simultaneously:\n- **Floor:** the largest value in the BST that is ≤ the target.\n- **Ceil:** the smallest value in the BST that is ≥ the target.\n\nThese are useful for range queries, nearest-neighbor lookups, and scheduling algorithms.", + + howItWorks: + "**Floor:** At each node, if the target equals the node value, that value is the floor. If the target is smaller, the floor must be in the left subtree. If the target is larger, the current node is a candidate floor — check the right subtree for a better (larger) candidate.\n\n**Ceil:** Mirror logic — if the target is larger, check right; if smaller, the current node is a candidate ceil and a better one may exist in the left subtree.", + + timeAndSpaceComplexity: + "**Time: `O(h)`** — each of floor and ceil makes one root-to-leaf pass.\n\n**Space: `O(h)`** — recursive call stack depth equals tree height.", + + bestAndWorstCase: + "**Best case:** Target equals a node value — both floor and ceil are found at that node immediately.\n\n**Worst case:** Target falls between the smallest and largest values in the tree, requiring traversal to a leaf.", + + realWorldUses: [ + "**Range queries:** Find the nearest stored timestamps to a given query time.", + "**Price lookup:** Find the closest lower and upper price bands for a given amount.", + "**Sorted set operations:** Floor/ceil correspond to `lower_bound`/`upper_bound` in C++ STL.", + ], + + strengthsAndLimitations: { + strengths: [ + "Exploits BST order to avoid scanning all nodes — only one path is followed.", + "Finds both floor and ceil in two independent O(h) passes.", + ], + limitations: [ + "Requires a valid BST — violations cause incorrect results.", + "Two separate recursive passes (one for floor, one for ceil); iterative version can combine in one.", + ], + }, + + whenToUseIt: + "Use when you need the nearest stored value below or above a query in a dynamically maintained sorted collection.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/index.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil/index.ts new file mode 100644 index 00000000..42e27d19 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstFloorCeil } from "./sources/bst-floor-ceil.ts?fn"; +import { generateBstFloorCeilSteps } from "./step-generator"; +import type { BstFloorCeilInput } from "./step-generator"; +import { bstFloorCeilEducational } from "./educational"; + +import typescriptSource from "./sources/bst-floor-ceil.ts?raw"; +import pythonSource from "./sources/bst-floor-ceil.py?raw"; +import javaSource from "./sources/BSTFloorCeil.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstFloorCeil(input: BstFloorCeilInput): { + floor: number | null; + ceil: number | null; +} { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstFloorCeil(root, input.targetValue) as { floor: number | null; ceil: number | null }; +} + +const bstFloorCeilDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_FLOOR_CEIL!, + name: "BST Floor & Ceil (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Find largest value ≤ target (floor) and smallest value ≥ target (ceil) using recursion", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetValue: 4 }, + }, + execute: executeBstFloorCeil, + generateSteps: generateBstFloorCeilSteps, + educational: bstFloorCeilEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstFloorCeilDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/BSTFloorCeil.java b/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/BSTFloorCeil.java new file mode 100644 index 00000000..30eb5293 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/BSTFloorCeil.java @@ -0,0 +1,39 @@ +// BST Floor & Ceil (Recursive) — largest value ≤ target (floor), smallest value ≥ target (ceil) + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTFloorCeil { + public int[] bstFloorCeil(BSTNode root, int target) { + return new int[]{ findFloor(root, target), findCeil(root, target) }; + } + + private int findFloor(BSTNode node, int target) { + if (node == null) return Integer.MIN_VALUE; // @step:initialize + if (node.value == target) return node.value; // @step:found + + if (target < node.value) { + // Target smaller than node — floor must be in left subtree + return findFloor(node.left, target); // @step:search-node + } + // Target larger than node — this node is a candidate, check right + int rightFloor = findFloor(node.right, target); // @step:search-node + return rightFloor != Integer.MIN_VALUE ? rightFloor : node.value; // @step:complete + } + + private int findCeil(BSTNode node, int target) { + if (node == null) return Integer.MAX_VALUE; // @step:initialize + if (node.value == target) return node.value; // @step:found + + if (target > node.value) { + // Target larger than node — ceil must be in right subtree + return findCeil(node.right, target); // @step:search-node + } + // Target smaller than node — this node is a candidate, check left + int leftCeil = findCeil(node.left, target); // @step:search-node + return leftCeil != Integer.MAX_VALUE ? leftCeil : node.value; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/bst-floor-ceil.py b/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/bst-floor-ceil.py new file mode 100644 index 00000000..d55e2f45 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/bst-floor-ceil.py @@ -0,0 +1,38 @@ +# BST Floor & Ceil (Recursive) — largest value ≤ target (floor), smallest value ≥ target (ceil) + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_floor_ceil(root, target: int) -> dict: + def find_floor(node, target): + if node is None: # @step:initialize + return None + if node.value == target: # @step:found + return node.value + + if target < node.value: + # Target smaller than node — floor must be in left subtree + return find_floor(node.left, target) # @step:search-node + + # Target larger than node — this node is a candidate, check right + right_floor = find_floor(node.right, target) # @step:search-node + return right_floor if right_floor is not None else node.value # @step:complete + + def find_ceil(node, target): + if node is None: # @step:initialize + return None + if node.value == target: # @step:found + return node.value + + if target > node.value: + # Target larger than node — ceil must be in right subtree + return find_ceil(node.right, target) # @step:search-node + + # Target smaller than node — this node is a candidate, check left + left_ceil = find_ceil(node.left, target) # @step:search-node + return left_ceil if left_ceil is not None else node.value # @step:complete + + return {"floor": find_floor(root, target), "ceil": find_ceil(root, target)} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/bst-floor-ceil.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/bst-floor-ceil.ts new file mode 100644 index 00000000..62240f9c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/sources/bst-floor-ceil.ts @@ -0,0 +1,42 @@ +// BST Floor & Ceil (Recursive) — largest value ≤ target (floor), smallest value ≥ target (ceil) + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +interface FloorCeilResult { + floor: number | null; + ceil: number | null; +} + +function bstFloorCeil(root: BSTNode | null, target: number): FloorCeilResult { + function findFloor(node: BSTNode | null, target: number): number | null { + if (node === null) return null; // @step:initialize + if (node.value === target) return node.value; // @step:found + + if (target < node.value) { + // Target smaller than node — floor must be in left subtree + return findFloor(node.left, target); // @step:search-node + } + // Target larger than node — this node is a candidate, check right + const rightFloor = findFloor(node.right, target); // @step:search-node + return rightFloor !== null ? rightFloor : node.value; // @step:complete + } + + function findCeil(node: BSTNode | null, target: number): number | null { + if (node === null) return null; // @step:initialize + if (node.value === target) return node.value; // @step:found + + if (target > node.value) { + // Target larger than node — ceil must be in right subtree + return findCeil(node.right, target); // @step:search-node + } + // Target smaller than node — this node is a candidate, check left + const leftCeil = findCeil(node.left, target); // @step:search-node + return leftCeil !== null ? leftCeil : node.value; // @step:complete + } + + return { floor: findFloor(root, target), ceil: findCeil(root, target) }; +} diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil/step-generator.test.ts new file mode 100644 index 00000000..c7dbdf39 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstFloorCeilSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstFloorCeilSteps", () => { + it("produces steps", () => { + const steps = generateBstFloorCeilSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 4 }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstFloorCeilSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 4 }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstFloorCeilSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 4 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstFloorCeilSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 4 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstFloorCeilSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 4 }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-floor-ceil/step-generator.ts b/src/algorithms/trees/bst-operations/bst-floor-ceil/step-generator.ts new file mode 100644 index 00000000..92c99753 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-floor-ceil/step-generator.ts @@ -0,0 +1,58 @@ +/** Step generator for BST Floor & Ceil (Recursive) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_FLOOR_CEIL_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_FLOOR_CEIL!); + +export interface BstFloorCeilInput { + nodes: TreeNode[]; + rootId: string; + targetValue: number; +} + +export function generateBstFloorCeilSteps(input: BstFloorCeilInput): ExecutionStep[] { + const { nodes, rootId, targetValue } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_FLOOR_CEIL_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(targetValue, { rootId, targetValue }); + + let floorValue: number | null = null; + let ceilValue: number | null = null; + + function findBounds(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + targetValue, + floor: floorValue, + ceil: ceilValue, + }); + + if (node.value === targetValue) { + floorValue = node.value; + ceilValue = node.value; + tracker.searchFound(nodeId, { floor: floorValue, ceil: ceilValue }); + return; + } + + if (targetValue < node.value) { + ceilValue = node.value; + findBounds(node.leftChildId); + } else { + floorValue = node.value; + findBounds(node.rightChildId); + } + } + + findBounds(rootId); + tracker.complete({ floor: floorValue, ceil: ceilValue, targetValue }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/BSTFromSortedArrayPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-from-sorted-array/BSTFromSortedArrayPipeline.stories.tsx new file mode 100644 index 00000000..b23edd96 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/BSTFromSortedArrayPipeline.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBstFromSortedArraySteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3, 4, 5, 6, 7] }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST From Sorted Array", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/bst-from-sorted-array.test.ts b/src/algorithms/trees/bst-operations/bst-from-sorted-array/bst-from-sorted-array.test.ts new file mode 100644 index 00000000..bbe193ed --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/bst-from-sorted-array.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { bstFromSortedArray } from "./sources/bst-from-sorted-array.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +describe("bstFromSortedArray", () => { + it("creates a balanced BST with root at mid-value", () => { + const result = bstFromSortedArray([1, 2, 3, 4, 5, 6, 7]) as BSTNode | null; + expect(result?.value).toBe(4); + }); + it("creates a single-node tree from array of one", () => { + const result = bstFromSortedArray([42]) as BSTNode | null; + expect(result?.value).toBe(42); + expect(result?.left).toBeNull(); + expect(result?.right).toBeNull(); + }); + it("returns null for empty array", () => { + expect(bstFromSortedArray([])).toBeNull(); + }); + it("creates a two-node tree with correct BST structure", () => { + const result = bstFromSortedArray([1, 2]) as BSTNode | null; + expect(result?.value).toBe(1); + expect(result?.right?.value).toBe(2); + }); + it("produces a valid BST for 5 elements", () => { + const result = bstFromSortedArray([1, 2, 3, 4, 5]) as BSTNode | null; + expect(result?.value).toBe(3); + expect(result?.left?.value).toBe(1); + expect(result?.right?.value).toBe(4); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/educational.ts b/src/algorithms/trees/bst-operations/bst-from-sorted-array/educational.ts new file mode 100644 index 00000000..d6853423 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/educational.ts @@ -0,0 +1,40 @@ +import type { EducationalContent } from "@/types"; + +export const bstFromSortedArrayEducational: EducationalContent = { + overview: + "**BST from Sorted Array** builds a **height-balanced** BST from a sorted array by recursively choosing the middle element as the root at each level.\n\nChoosing the midpoint ensures equal-sized left and right subtrees, producing an optimally balanced tree with height `O(log n)`.", + + howItWorks: + "1. If the subarray is empty — return `null` (base case).\n" + + "2. Compute `mid = floor((left + right) / 2)`.\n" + + "3. Create a root node with `array[mid]`.\n" + + "4. Recursively build the left subtree from `array[left..mid-1]`.\n" + + "5. Recursively build the right subtree from `array[mid+1..right]`.\n\n" + + "The recursion halves the subarray at each level — the same divide-and-conquer strategy as merge sort.", + + timeAndSpaceComplexity: + "**Time: `O(n)`** — each element is processed exactly once.\n\n**Space: `O(n)`** — `n` nodes are created; call stack is `O(log n)` for the balanced result.", + + bestAndWorstCase: "Always O(n) time regardless of input — every element must become a node.", + + realWorldUses: [ + "**BST construction from sorted data:** Building an index from a sorted file.", + "**Static lookup tables:** Convert a sorted array into an optimal BST for repeated searches.", + "**Database B-tree initialization:** Pre-load sorted keys into a balanced structure.", + ], + + strengthsAndLimitations: { + strengths: [ + "Always produces a perfectly balanced BST — O(log n) search guaranteed.", + "Simple recursive divide-and-conquer structure.", + "O(n) time — optimal since all elements must be visited.", + ], + limitations: [ + "Requires the input to be sorted — add an O(n log n) sort step if not already sorted.", + "Produces a static tree — insertions after construction may unbalance it.", + ], + }, + + whenToUseIt: + "Use when you have a sorted array and need an optimal BST for lookups. For dynamic data, use a self-balancing BST (AVL, red-black) that maintains balance through insertions.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/index.ts b/src/algorithms/trees/bst-operations/bst-from-sorted-array/index.ts new file mode 100644 index 00000000..3f2c3aa1 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/index.ts @@ -0,0 +1,44 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstFromSortedArray } from "./sources/bst-from-sorted-array.ts?fn"; +import { generateBstFromSortedArraySteps } from "./step-generator"; +import type { BstFromSortedArrayInput } from "./step-generator"; +import { bstFromSortedArrayEducational } from "./educational"; + +import typescriptSource from "./sources/bst-from-sorted-array.ts?raw"; +import pythonSource from "./sources/bst-from-sorted-array.py?raw"; +import javaSource from "./sources/BSTFromSortedArray.java?raw"; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstFromSortedArray(input: BstFromSortedArrayInput): number | null { + const result = bstFromSortedArray(input.sortedArray) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstFromSortedArrayDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_FROM_SORTED_ARRAY!, + name: "BST from Sorted Array", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Build a height-balanced BST from a sorted array by recursively picking the middle element as root", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { sortedArray: [1, 2, 3, 4, 5, 6, 7] }, + }, + execute: executeBstFromSortedArray, + generateSteps: generateBstFromSortedArraySteps, + educational: bstFromSortedArrayEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstFromSortedArrayDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/BSTFromSortedArray.java b/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/BSTFromSortedArray.java new file mode 100644 index 00000000..c233f14e --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/BSTFromSortedArray.java @@ -0,0 +1,27 @@ +// BST From Sorted Array (Recursive) — pick middle as root, recurse on halves + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTFromSortedArray { + public BSTNode bstFromSortedArray(int[] sortedArray) { + return buildBST(sortedArray, 0, sortedArray.length - 1); + } + + private BSTNode buildBST(int[] sortedArray, int leftIndex, int rightIndex) { + if (leftIndex > rightIndex) return null; // @step:initialize + + // Pick the middle element as root to keep the tree balanced + int midIndex = (leftIndex + rightIndex) / 2; // @step:build-node + BSTNode node = new BSTNode(sortedArray[midIndex]); + + // Recursively build left and right subtrees + node.left = buildBST(sortedArray, leftIndex, midIndex - 1); // @step:connect-child + node.right = buildBST(sortedArray, midIndex + 1, rightIndex); // @step:connect-child + + return node; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/bst-from-sorted-array.py b/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/bst-from-sorted-array.py new file mode 100644 index 00000000..d6f4a3bb --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/bst-from-sorted-array.py @@ -0,0 +1,24 @@ +# BST From Sorted Array (Recursive) — pick middle as root, recurse on halves + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_from_sorted_array(sorted_array: list) -> object: + def build_bst(left_index: int, right_index: int): + if left_index > right_index: # @step:initialize + return None + + # Pick the middle element as root to keep the tree balanced + mid_index = (left_index + right_index) // 2 # @step:build-node + node = BSTNode(sorted_array[mid_index]) + + # Recursively build left and right subtrees + node.left = build_bst(left_index, mid_index - 1) # @step:connect-child + node.right = build_bst(mid_index + 1, right_index) # @step:connect-child + + return node # @step:complete + + return build_bst(0, len(sorted_array) - 1) diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/bst-from-sorted-array.ts b/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/bst-from-sorted-array.ts new file mode 100644 index 00000000..2ab84484 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/sources/bst-from-sorted-array.ts @@ -0,0 +1,28 @@ +// BST From Sorted Array (Recursive) — pick middle as root, recurse on halves + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstFromSortedArray(sortedArray: number[]): BSTNode | null { + function buildBST(leftIndex: number, rightIndex: number): BSTNode | null { + if (leftIndex > rightIndex) return null; // @step:initialize + + // Pick the middle element as root to keep the tree balanced + const midIndex = Math.floor((leftIndex + rightIndex) / 2); // @step:build-node + const midValue = sortedArray[midIndex]; + if (midValue === undefined) return null; + + const node: BSTNode = { value: midValue, left: null, right: null }; + + // Recursively build left and right subtrees + node.left = buildBST(leftIndex, midIndex - 1); // @step:connect-child + node.right = buildBST(midIndex + 1, rightIndex); // @step:connect-child + + return node; // @step:complete + } + + return buildBST(0, sortedArray.length - 1); +} diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-from-sorted-array/step-generator.test.ts new file mode 100644 index 00000000..b5b516c7 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/step-generator.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { generateBstFromSortedArraySteps } from "./step-generator"; + +describe("generateBstFromSortedArraySteps", () => { + it("produces steps for a sorted array", () => { + const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3, 4, 5, 6, 7] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize", () => { + const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3, 4, 5, 6, 7] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete", () => { + const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3, 4, 5, 6, 7] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3, 4, 5, 6, 7] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("inserts n nodes for array of length n", () => { + const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3] }); + const insertSteps = steps.filter((step) => step.type === "insert-child"); + expect(insertSteps.length).toBe(3); + }); + + it("has incrementing indices", () => { + const steps = generateBstFromSortedArraySteps({ sortedArray: [1, 2, 3, 4, 5] }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-from-sorted-array/step-generator.ts b/src/algorithms/trees/bst-operations/bst-from-sorted-array/step-generator.ts new file mode 100644 index 00000000..72191a7d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-from-sorted-array/step-generator.ts @@ -0,0 +1,74 @@ +/** Step generator for BST From Sorted Array (Recursive) — build balanced BST. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_FROM_SORTED_ARRAY_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_FROM_SORTED_ARRAY!); + +export interface BstFromSortedArrayInput { + sortedArray: number[]; +} + +export function generateBstFromSortedArraySteps(input: BstFromSortedArrayInput): ExecutionStep[] { + const { sortedArray } = input; + + // Build initial empty tree and root placeholder + const builtNodes: TreeNode[] = []; + const rootId = "root-placeholder"; + + const tracker = new BSTOperationTracker(builtNodes, rootId, BST_FROM_SORTED_ARRAY_LINE_MAP); + + tracker.initialize(null, { sortedArray: sortedArray.join(", "), length: sortedArray.length }); + + let nodeCounter = 0; + + function buildBST( + leftIndex: number, + rightIndex: number, + parentId: string | null, + depth: number, + ): string | null { + if (leftIndex > rightIndex) return null; + + const midIndex = Math.floor((leftIndex + rightIndex) / 2); + const midValue = sortedArray[midIndex]; + if (midValue === undefined) return null; + + nodeCounter++; + const newNodeId = `n${nodeCounter}`; + const xBase = 200; + const xOffset = (rightIndex - leftIndex) * 30; + const newNode: TreeNode = { + id: newNodeId, + value: midValue, + parentId, + leftChildId: null, + rightChildId: null, + state: "current", + position: { x: xBase + xOffset * (midIndex - sortedArray.length / 2), y: depth * 100 + 60 }, + }; + + builtNodes.push(newNode); + tracker.insertNode(parentId, newNodeId, { + midIndex, + midValue, + leftBound: leftIndex, + rightBound: rightIndex, + }); + + const leftChildId = buildBST(leftIndex, midIndex - 1, newNodeId, depth + 1); + const rightChildId = buildBST(midIndex + 1, rightIndex, newNodeId, depth + 1); + + newNode.leftChildId = leftChildId; + newNode.rightChildId = rightChildId; + + return newNodeId; + } + + buildBST(0, sortedArray.length - 1, null, 0); + tracker.complete({ nodesCreated: builtNodes.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/BSTInsertIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-insert-iterative/BSTInsertIterativePipeline.stories.tsx new file mode 100644 index 00000000..f909c5aa --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/BSTInsertIterativePipeline.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstInsertIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstInsertIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + insertValue: 8, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Insert (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/bst-insert-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-insert-iterative/bst-insert-iterative.test.ts new file mode 100644 index 00000000..318c6bbd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/bst-insert-iterative.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { bstInsertIterative } from "./sources/bst-insert-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstInsertIterative", () => { + it("inserts a value greater than all nodes", () => { + const result = bstInsertIterative(tree, 8) as BSTNode; + expect(result.right?.right?.right?.value).toBe(8); + }); + it("creates root from null tree", () => { + const result = bstInsertIterative(null, 5) as BSTNode; + expect(result.value).toBe(5); + }); + it("inserts left child", () => { + const freshTree = node(10); + const result = bstInsertIterative(freshTree, 5) as BSTNode; + expect(result.left?.value).toBe(5); + }); + it("ignores duplicates", () => { + const freshTree = node(4, node(2), node(6)); + const result = bstInsertIterative(freshTree, 2) as BSTNode; + expect(result.left?.right).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-insert-iterative/educational.ts new file mode 100644 index 00000000..928dd85a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/educational.ts @@ -0,0 +1,31 @@ +import type { EducationalContent } from "@/types"; + +export const bstInsertIterativeEducational: EducationalContent = { + overview: + "**BST Insert (Iterative)** inserts a new value without recursion. It tracks the current node and its parent while walking to the correct leaf, then links the new node directly to the parent.", + + howItWorks: + "1. Create the new node.\n" + + "2. Walk from root: at each node, compare `insertValue` to decide left or right.\n" + + "3. When the child slot is `null`, set it to the new node and stop.\n" + + "4. If the root is `null`, return the new node as the new root.", + + timeAndSpaceComplexity: + "**Time: `O(h)`** — single path to leaf.\n\n**Space: `O(1)`** — no call stack, only pointer variables.", + + bestAndWorstCase: + "**Best case:** Balanced tree — O(log n).\n\n**Worst case:** Degenerate tree — O(n).", + + realWorldUses: [ + "**Real-time systems:** O(1) space insertion is predictable for memory-constrained environments.", + "**Bulk loading:** Iteratively inserting pre-sorted data into a BST (though sorting first and using bst-from-sorted-array is better).", + ], + + strengthsAndLimitations: { + strengths: ["O(1) space — no recursion overhead.", "Straightforward to implement and debug."], + limitations: ["Slightly more boilerplate than the recursive version."], + }, + + whenToUseIt: + "Prefer the iterative insert over recursive when working in stack-constrained environments or when implementing a full BST class.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-insert-iterative/index.ts new file mode 100644 index 00000000..875d55ec --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstInsertIterative } from "./sources/bst-insert-iterative.ts?fn"; +import { generateBstInsertIterativeSteps } from "./step-generator"; +import type { BstInsertIterativeInput } from "./step-generator"; +import { bstInsertIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-insert-iterative.ts?raw"; +import pythonSource from "./sources/bst-insert-iterative.py?raw"; +import javaSource from "./sources/BSTInsertIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstInsertIterative(input: BstInsertIterativeInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstInsertIterative(root, input.insertValue) as BSTNodeShape; + return result.value; +} + +const bstInsertIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_INSERT_ITERATIVE!, + name: "BST Insert (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Iterative BST insertion: track parent pointer while walking to the correct leaf", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", insertValue: 8 }, + }, + execute: executeBstInsertIterative, + generateSteps: generateBstInsertIterativeSteps, + educational: bstInsertIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstInsertIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/BSTInsertIterative.java b/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/BSTInsertIterative.java new file mode 100644 index 00000000..9ae5d33f --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/BSTInsertIterative.java @@ -0,0 +1,40 @@ +// BST Insert (Iterative) — track parent, insert at correct leaf position + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTInsertIterative { + public BSTNode bstInsertIterative(BSTNode root, int insertValue) { + BSTNode newNode = new BSTNode(insertValue); // @step:initialize + + if (root == null) return newNode; // @step:insert-child + + BSTNode current = root; + + while (true) { + if (insertValue < current.value) { + // Go left — if no left child, insert here + if (current.left == null) { + current.left = newNode; // @step:insert-child + break; + } + current = current.left; // @step:search-node + } else if (insertValue > current.value) { + // Go right — if no right child, insert here + if (current.right == null) { + current.right = newNode; // @step:insert-child + break; + } + current = current.right; // @step:search-node + } else { + // Duplicate value — do nothing + break; + } + } + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/bst-insert-iterative.py b/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/bst-insert-iterative.py new file mode 100644 index 00000000..72bb93c7 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/bst-insert-iterative.py @@ -0,0 +1,34 @@ +# BST Insert (Iterative) — track parent, insert at correct leaf position + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_insert_iterative(root, insert_value: int): + new_node = BSTNode(insert_value) # @step:initialize + + if root is None: # @step:insert-child + return new_node + + current = root + + while True: + if insert_value < current.value: + # Go left — if no left child, insert here + if current.left is None: + current.left = new_node # @step:insert-child + break + current = current.left # @step:search-node + elif insert_value > current.value: + # Go right — if no right child, insert here + if current.right is None: + current.right = new_node # @step:insert-child + break + current = current.right # @step:search-node + else: + # Duplicate value — do nothing + break + + return root # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/bst-insert-iterative.ts b/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/bst-insert-iterative.ts new file mode 100644 index 00000000..d3c39b4d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/sources/bst-insert-iterative.ts @@ -0,0 +1,38 @@ +// BST Insert (Iterative) — track parent, insert at correct leaf position + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstInsertIterative(root: BSTNode | null, insertValue: number): BSTNode { + const newNode: BSTNode = { value: insertValue, left: null, right: null }; // @step:initialize + + if (root === null) return newNode; // @step:insert-child + + let current: BSTNode = root; + + while (true) { + if (insertValue < current.value) { + // Go left — if no left child, insert here + if (current.left === null) { + current.left = newNode; // @step:insert-child + break; + } + current = current.left; // @step:search-node + } else if (insertValue > current.value) { + // Go right — if no right child, insert here + if (current.right === null) { + current.right = newNode; // @step:insert-child + break; + } + current = current.right; // @step:search-node + } else { + // Duplicate value — do nothing + break; + } + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-insert-iterative/step-generator.test.ts new file mode 100644 index 00000000..2ee01709 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/step-generator.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstInsertIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstInsertIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstInsertIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + insertValue: 8, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstInsertIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + insertValue: 8, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstInsertIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + insertValue: 8, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstInsertIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + insertValue: 8, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstInsertIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + insertValue: 8, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-insert-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-insert-iterative/step-generator.ts new file mode 100644 index 00000000..5d10707e --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert-iterative/step-generator.ts @@ -0,0 +1,77 @@ +/** Step generator for BST Insert (Iterative) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_INSERT_ITERATIVE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_INSERT_ITERATIVE!); + +export interface BstInsertIterativeInput { + nodes: TreeNode[]; + rootId: string; + insertValue: number; +} + +export function generateBstInsertIterativeSteps(input: BstInsertIterativeInput): ExecutionStep[] { + const { nodes, rootId, insertValue } = input; + + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker(mutableNodes, rootId, BST_INSERT_ITERATIVE_LINE_MAP); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(insertValue, { rootId, insertValue }); + + let currentId: string | null = rootId; + let parentId: string | null = null; + + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + insertValue, + nodeValue: node.value, + }); + + if (insertValue === node.value) break; // Duplicate + + parentId = currentId; + currentId = insertValue < node.value ? node.leftChildId : node.rightChildId; + } + + if (currentId === null && parentId !== null) { + const parentNode = nodeMap.get(parentId); + if (parentNode) { + const newNodeId = `n${insertValue}`; + const newPosition = { + x: parentNode.position.x + (insertValue < parentNode.value ? -50 : 50), + y: parentNode.position.y + 100, + }; + const newNode: TreeNode = { + id: newNodeId, + value: insertValue, + parentId, + leftChildId: null, + rightChildId: null, + state: "current", + position: newPosition, + }; + mutableNodes.push(newNode); + nodeMap.set(newNodeId, newNode); + + if (insertValue < parentNode.value) { + parentNode.leftChildId = newNodeId; + } else { + parentNode.rightChildId = newNodeId; + } + + tracker.insertNode(parentId, newNodeId, { parentNode: parentId, insertValue, newNodeId }); + } + } + + tracker.complete({ insertValue, treeSize: mutableNodes.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-insert/BSTInsertPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-insert/BSTInsertPipeline.stories.tsx new file mode 100644 index 00000000..789b7b81 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/BSTInsertPipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstInsertSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstInsertSteps({ nodes: defaultNodes, rootId: "n4", insertValue: 8 }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Insert (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-insert/bst-insert.test.ts b/src/algorithms/trees/bst-operations/bst-insert/bst-insert.test.ts new file mode 100644 index 00000000..0f42c426 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/bst-insert.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { bstInsert } from "./sources/bst-insert.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstInsert", () => { + it("inserts a value greater than all existing nodes", () => { + const result = bstInsert(tree, 8) as BSTNode; + expect(result.right?.right?.right?.value).toBe(8); + }); + it("inserts a value into the left subtree", () => { + const freshTree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const result = bstInsert(freshTree, 0) as BSTNode; + expect(result.left?.left?.left?.value).toBe(0); + }); + it("creates a new root from null", () => { + const result = bstInsert(null, 10) as BSTNode; + expect(result.value).toBe(10); + }); + it("ignores duplicate values", () => { + const freshTree = node(4, node(2), node(6)); + const result = bstInsert(freshTree, 4) as BSTNode; + expect(result.value).toBe(4); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-insert/educational.ts b/src/algorithms/trees/bst-operations/bst-insert/educational.ts new file mode 100644 index 00000000..01415510 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/educational.ts @@ -0,0 +1,39 @@ +import type { EducationalContent } from "@/types"; + +export const bstInsertEducational: EducationalContent = { + overview: + "**BST Insert (Recursive)** adds a new value to a BST while preserving the BST property: every node's left descendants are smaller and right descendants are larger.\n\nThe algorithm finds the correct `null` slot for the new value by recursively navigating left or right.", + + howItWorks: + "1. **Base case:** If the current node is `null`, create and return a new node with the insert value — this is the correct position.\n" + + "2. If `insertValue < node.value` — recurse left and link the returned subtree back.\n" + + "3. If `insertValue > node.value` — recurse right.\n" + + "4. Duplicates are ignored.\n\n" + + "The recursion naturally 'threads' the new node into the correct leaf position.", + + timeAndSpaceComplexity: + "**Time: `O(h)`** — path from root to insertion point.\n\n**Space: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case:** Balanced tree — O(log n) path to the leaf.\n\n**Worst case:** Inserting sorted data into a standard BST creates a linear chain — O(n) per insert.", + + realWorldUses: [ + "**Dynamic sorted sets:** Insert elements and iterate in order at any time.", + "**Priority scheduling:** Maintain a BST of tasks ordered by deadline.", + "**BST construction:** Building a BST from an unsorted array uses repeated insertion.", + ], + + strengthsAndLimitations: { + strengths: [ + "Clean recursive structure — easy to understand and verify.", + "Preserves BST property automatically.", + ], + limitations: [ + "Recursive stack depth O(h) — may overflow on very deep trees.", + "Does not self-balance — consider AVL or red-black trees for guaranteed O(log n).", + ], + }, + + whenToUseIt: + "Use when you need a simple ordered dynamic structure. Use a self-balancing BST variant when guaranteed O(log n) performance is required.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-insert/index.ts b/src/algorithms/trees/bst-operations/bst-insert/index.ts new file mode 100644 index 00000000..81e3a76e --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/index.ts @@ -0,0 +1,118 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstInsert } from "./sources/bst-insert.ts?fn"; +import { generateBstInsertSteps } from "./step-generator"; +import type { BstInsertInput } from "./step-generator"; +import { bstInsertEducational } from "./educational"; + +import typescriptSource from "./sources/bst-insert.ts?raw"; +import pythonSource from "./sources/bst-insert.py?raw"; +import javaSource from "./sources/BSTInsert.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstInsert(input: BstInsertInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstInsert(root, input.insertValue) as BSTNodeShape; + return result.value; +} + +const bstInsertDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_INSERT!, + name: "BST Insert (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Recursive BST insertion: traverse to the correct leaf position and link the new node", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", insertValue: 8 }, + }, + execute: executeBstInsert, + generateSteps: generateBstInsertSteps, + educational: bstInsertEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstInsertDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-insert/sources/BSTInsert.java b/src/algorithms/trees/bst-operations/bst-insert/sources/BSTInsert.java new file mode 100644 index 00000000..18c8c503 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/sources/BSTInsert.java @@ -0,0 +1,27 @@ +// BST Insert (Recursive) — find correct leaf position and insert new node + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTInsert { + public BSTNode bstInsert(BSTNode root, int insertValue) { + if (root == null) { + // Base case: insert new node at this position + return new BSTNode(insertValue); // @step:insert-child + } + + if (insertValue < root.value) { + // Insert value is smaller — recurse into left subtree + root.left = bstInsert(root.left, insertValue); // @step:search-node + } else if (insertValue > root.value) { + // Insert value is larger — recurse into right subtree + root.right = bstInsert(root.right, insertValue); // @step:search-node + } + // Duplicate values are ignored + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-insert/sources/bst-insert.py b/src/algorithms/trees/bst-operations/bst-insert/sources/bst-insert.py new file mode 100644 index 00000000..79a4b529 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/sources/bst-insert.py @@ -0,0 +1,22 @@ +# BST Insert (Recursive) — find correct leaf position and insert new node + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_insert(root, insert_value: int): + if root is None: + # Base case: insert new node at this position + return BSTNode(insert_value) # @step:insert-child + + if insert_value < root.value: + # Insert value is smaller — recurse into left subtree + root.left = bst_insert(root.left, insert_value) # @step:search-node + elif insert_value > root.value: + # Insert value is larger — recurse into right subtree + root.right = bst_insert(root.right, insert_value) # @step:search-node + # Duplicate values are ignored + + return root # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-insert/sources/bst-insert.ts b/src/algorithms/trees/bst-operations/bst-insert/sources/bst-insert.ts new file mode 100644 index 00000000..4f8608e2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/sources/bst-insert.ts @@ -0,0 +1,25 @@ +// BST Insert (Recursive) — find correct leaf position and insert new node + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstInsert(root: BSTNode | null, insertValue: number): BSTNode { + if (root === null) { + // Base case: insert new node at this position + return { value: insertValue, left: null, right: null }; // @step:insert-child + } + + if (insertValue < root.value) { + // Insert value is smaller — recurse into left subtree + root.left = bstInsert(root.left, insertValue); // @step:search-node + } else if (insertValue > root.value) { + // Insert value is larger — recurse into right subtree + root.right = bstInsert(root.right, insertValue); // @step:search-node + } + // Duplicate values are ignored + + return root; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-insert/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-insert/step-generator.test.ts new file mode 100644 index 00000000..39f30e54 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstInsertSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstInsertSteps", () => { + it("produces steps", () => { + const steps = generateBstInsertSteps({ nodes: defaultNodes, rootId: "n4", insertValue: 8 }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstInsertSteps({ nodes: defaultNodes, rootId: "n4", insertValue: 8 }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstInsertSteps({ nodes: defaultNodes, rootId: "n4", insertValue: 8 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstInsertSteps({ nodes: defaultNodes, rootId: "n4", insertValue: 8 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstInsertSteps({ nodes: defaultNodes, rootId: "n4", insertValue: 8 }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-insert/step-generator.ts b/src/algorithms/trees/bst-operations/bst-insert/step-generator.ts new file mode 100644 index 00000000..6d14cccd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-insert/step-generator.ts @@ -0,0 +1,91 @@ +/** Step generator for BST Insert (Recursive) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_INSERT_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_INSERT!); + +export interface BstInsertInput { + nodes: TreeNode[]; + rootId: string; + insertValue: number; +} + +export function generateBstInsertSteps(input: BstInsertInput): ExecutionStep[] { + const { nodes, rootId, insertValue } = input; + + // Build a mutable copy of nodes for insertion tracking + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker(mutableNodes, rootId, BST_INSERT_LINE_MAP); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(insertValue, { rootId, insertValue }); + + function insert(nodeId: string | null, parentId: string | null): string { + if (!nodeId) { + // Create and insert new node + const newNodeId = `n${insertValue}`; + const parentNode = parentId ? nodeMap.get(parentId) : null; + + // Compute position based on parent + const newPosition = parentNode + ? { + x: parentNode.position.x + (insertValue < parentNode.value ? -50 : 50), + y: parentNode.position.y + 100, + } + : { x: 200, y: 60 }; + + const newNode: TreeNode = { + id: newNodeId, + value: insertValue, + parentId, + leftChildId: null, + rightChildId: null, + state: "current", + position: newPosition, + }; + mutableNodes.push(newNode); + nodeMap.set(newNodeId, newNode); + + // Update parent's child pointer + if (parentNode) { + if (insertValue < parentNode.value) { + parentNode.leftChildId = newNodeId; + } else { + parentNode.rightChildId = newNodeId; + } + } + + tracker.insertNode(parentId, newNodeId, { + parentNode: parentId, + insertValue, + newNodeId, + }); + return newNodeId; + } + + const node = nodeMap.get(nodeId); + if (!node) return nodeId; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + insertValue, + nodeValue: node.value, + }); + + if (insertValue < node.value) { + insert(node.leftChildId, nodeId); + } else if (insertValue > node.value) { + insert(node.rightChildId, nodeId); + } + + return nodeId; + } + + insert(rootId, null); + tracker.complete({ insertValue, treeSize: mutableNodes.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-iterator/BSTIteratorPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-iterator/BSTIteratorPipeline.stories.tsx new file mode 100644 index 00000000..f68aa864 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/BSTIteratorPipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstIteratorSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstIteratorSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Iterator", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-iterator/bst-iterator.test.ts b/src/algorithms/trees/bst-operations/bst-iterator/bst-iterator.test.ts new file mode 100644 index 00000000..e7431fad --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/bst-iterator.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { bstIterator } from "./sources/bst-iterator.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstIterator", () => { + it("iterates all values in sorted ascending order", () => { + expect(bstIterator(tree)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + it("returns empty array for null tree", () => { + expect(bstIterator(null)).toEqual([]); + }); + it("returns single-element array for single node", () => { + expect(bstIterator(node(42))).toEqual([42]); + }); + it("handles right-skewed tree", () => { + const skewed = node(1, null, node(2, null, node(3))); + expect(bstIterator(skewed)).toEqual([1, 2, 3]); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-iterator/educational.ts b/src/algorithms/trees/bst-operations/bst-iterator/educational.ts new file mode 100644 index 00000000..60572497 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/educational.ts @@ -0,0 +1,36 @@ +import type { EducationalContent } from "@/types"; + +export const bstIteratorEducational: EducationalContent = { + overview: + "**BST Iterator** provides an on-demand, sorted iteration interface over a BST using `hasNext()` and `next()` methods. Instead of producing all values upfront, it lazily yields one value at a time by maintaining a controlled in-order traversal state in a stack.\n\nThis is the foundation for Java's `TreeSet` iterator and Python's generator-based BST iteration.", + + howItWorks: + "**Constructor:** Push all left-spine nodes from the root onto the stack.\n\n**next():**\n1. Pop the top node — this is the current smallest unvisited node.\n2. Push the left-spine of the popped node's right child.\n3. Return the node's value.\n\n**hasNext():** Stack is non-empty.\n\nThe key insight: the stack always holds the path to the next in-order node without pre-computing the entire traversal.", + + timeAndSpaceComplexity: + "**Time per `next()`: Amortized `O(1)`** — over all `n` calls, each node is pushed and popped exactly once.\n\n**Space: `O(h)`** — stack holds at most `h` nodes (the leftmost path).", + + bestAndWorstCase: + "**Best case:** Complete iteration of a balanced tree — O(1) amortized per call.\n\n**Worst case:** Right-skewed tree — each `next()` pushes the full right spine, but amortized cost remains O(1).", + + realWorldUses: [ + "**Database cursors:** BST iterators power range scan cursors in database indexes.", + "**Java TreeSet/TreeMap:** `Iterator` is implemented this way.", + "**Lazy evaluation:** Produce only as many values as needed without full traversal.", + ], + + strengthsAndLimitations: { + strengths: [ + "Amortized O(1) per `next()` — efficient for full traversals.", + "O(h) space — much less than O(n) if only a few elements are needed.", + "Supports stopping mid-traversal without wasted work.", + ], + limitations: [ + "More complex state management than a full recursive traversal.", + "Not thread-safe — concurrent modifications require external synchronization.", + ], + }, + + whenToUseIt: + "Use when you need to consume BST values one at a time, stop early, or integrate with a for-each loop interface.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-iterator/index.ts b/src/algorithms/trees/bst-operations/bst-iterator/index.ts new file mode 100644 index 00000000..08383a9a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstIterator } from "./sources/bst-iterator.ts?fn"; +import { generateBstIteratorSteps } from "./step-generator"; +import type { BstIteratorInput } from "./step-generator"; +import { bstIteratorEducational } from "./educational"; + +import typescriptSource from "./sources/bst-iterator.ts?raw"; +import pythonSource from "./sources/bst-iterator.py?raw"; +import javaSource from "./sources/BSTIterator.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstIterator(input: BstIteratorInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstIterator(root) as number[]; +} + +const bstIteratorDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_ITERATOR!, + name: "BST Iterator", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Stack-based BST iterator that yields values in sorted order via hasNext/next interface", + timeComplexity: { best: "O(1)", average: "O(1)", worst: "O(h)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBstIterator, + generateSteps: generateBstIteratorSteps, + educational: bstIteratorEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstIteratorDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-iterator/sources/BSTIterator.java b/src/algorithms/trees/bst-operations/bst-iterator/sources/BSTIterator.java new file mode 100644 index 00000000..4cc47a66 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/sources/BSTIterator.java @@ -0,0 +1,48 @@ +// BST Iterator — stack-based controlled in-order traversal (hasNext/next interface) +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTIteratorImpl { + private Stack stack = new Stack<>(); // @step:initialize + + public BSTIteratorImpl(BSTNode root) { + pushLeft(root); // @step:initialize + } + + private void pushLeft(BSTNode node) { + while (node != null) { + stack.push(node); // @step:search-node + node = node.left; + } + } + + public boolean hasNext() { + return !stack.isEmpty(); // @step:search-node + } + + public int next() { + BSTNode node = stack.pop(); // @step:found + pushLeft(node.right); + return node.value; + } +} + +class BSTIterator { + public List bstIterator(BSTNode root) { + BSTIteratorImpl iterator = new BSTIteratorImpl(root); // @step:initialize + List result = new ArrayList<>(); + + while (iterator.hasNext()) { + result.add(iterator.next()); // @step:found + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-iterator/sources/bst-iterator.py b/src/algorithms/trees/bst-operations/bst-iterator/sources/bst-iterator.py new file mode 100644 index 00000000..25f09cc6 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/sources/bst-iterator.py @@ -0,0 +1,34 @@ +# BST Iterator — stack-based controlled in-order traversal (has_next/next interface) + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +class BSTIteratorClass: + def __init__(self, root): + self.stack = [] # @step:initialize + self._push_left(root) # @step:initialize + + def _push_left(self, node) -> None: + while node is not None: + self.stack.append(node) # @step:search-node + node = node.left + + def has_next(self) -> bool: + return len(self.stack) > 0 # @step:search-node + + def next(self) -> int: + node = self.stack.pop() # @step:found + self._push_left(node.right) + return node.value + +def bst_iterator(root) -> list: + iterator = BSTIteratorClass(root) # @step:initialize + result = [] + + while iterator.has_next(): + result.append(iterator.next()) # @step:found + + return result # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-iterator/sources/bst-iterator.ts b/src/algorithms/trees/bst-operations/bst-iterator/sources/bst-iterator.ts new file mode 100644 index 00000000..42a7161a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/sources/bst-iterator.ts @@ -0,0 +1,44 @@ +// BST Iterator — stack-based controlled in-order traversal (hasNext/next interface) + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +class BSTIterator { + private stack: BSTNode[] = []; // @step:initialize + + constructor(root: BSTNode | null) { + this.pushLeft(root); // @step:initialize + } + + private pushLeft(node: BSTNode | null): void { + while (node !== null) { + this.stack.push(node); // @step:search-node + node = node.left; + } + } + + hasNext(): boolean { + return this.stack.length > 0; // @step:search-node + } + + next(): number { + const node = this.stack.pop()!; // @step:found + this.pushLeft(node.right); + return node.value; + } +} + +// Convenience function to collect all values via iterator +function bstIterator(root: BSTNode | null): number[] { + const iterator = new BSTIterator(root); // @step:initialize + const result: number[] = []; + + while (iterator.hasNext()) { + result.push(iterator.next()); // @step:found + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-iterator/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-iterator/step-generator.test.ts new file mode 100644 index 00000000..dbdd3831 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstIteratorSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstIteratorSteps", () => { + it("produces steps", () => { + const steps = generateBstIteratorSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstIteratorSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstIteratorSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstIteratorSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstIteratorSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-iterator/step-generator.ts b/src/algorithms/trees/bst-operations/bst-iterator/step-generator.ts new file mode 100644 index 00000000..8354da09 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-iterator/step-generator.ts @@ -0,0 +1,56 @@ +/** Step generator for BST Iterator — stack-based controlled in-order traversal. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_ITERATOR_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_ITERATOR!); + +export interface BstIteratorInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstIteratorSteps(input: BstIteratorInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_ITERATOR_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId }); + + const stack: string[] = []; + const result: number[] = []; + + function pushLeft(nodeId: string | null): void { + let currentId = nodeId; + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + stack.push(currentId); + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + pushing: true, + stackSize: stack.length, + }); + currentId = node.leftChildId; + } + } + + pushLeft(rootId); + + while (stack.length > 0) { + const currentId = stack.pop(); + if (!currentId) break; + const node = nodeMap.get(currentId); + if (!node) break; + + result.push(node.value); + tracker.searchFound(currentId, { value: node.value, iterationCount: result.length }); + pushLeft(node.rightChildId); + } + + tracker.complete({ iterationOrder: result.join(", "), count: result.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/BSTKthSmallestIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/BSTKthSmallestIterativePipeline.stories.tsx new file mode 100644 index 00000000..22ade99b --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/BSTKthSmallestIterativePipeline.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstKthSmallestIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstKthSmallestIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Kth Smallest (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/bst-kth-smallest-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/bst-kth-smallest-iterative.test.ts new file mode 100644 index 00000000..8ed31f91 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/bst-kth-smallest-iterative.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { bstKthSmallestIterative } from "./sources/bst-kth-smallest-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstKthSmallestIterative", () => { + it("finds 1st smallest", () => { + expect(bstKthSmallestIterative(tree, 1)).toBe(1); + }); + it("finds 2nd smallest", () => { + expect(bstKthSmallestIterative(tree, 2)).toBe(2); + }); + it("finds 7th smallest", () => { + expect(bstKthSmallestIterative(tree, 7)).toBe(7); + }); + it("returns -1 for out-of-range k", () => { + expect(bstKthSmallestIterative(tree, 99)).toBe(-1); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/educational.ts new file mode 100644 index 00000000..c7242a8d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/educational.ts @@ -0,0 +1,31 @@ +import type { EducationalContent } from "@/types"; + +export const bstKthSmallestIterativeEducational: EducationalContent = { + overview: + "**BST Kth Smallest (Iterative)** finds the kth smallest value using a stack-based in-order traversal instead of recursion. A counter increments with each visited node and the algorithm returns as soon as it reaches `k`.", + + howItWorks: + "Uses an explicit stack simulating in-order traversal:\n1. Push all left nodes onto the stack.\n2. Pop the top — this is the next in-order node. Increment counter.\n3. If counter equals k — return the node's value.\n4. Otherwise, push the right child's leftmost path and continue.", + + timeAndSpaceComplexity: + "**Time: `O(k + h)`**\n\n**Space: `O(h)`** — stack holds at most `h` nodes.", + + bestAndWorstCase: + "**Best case:** k = 1 — pop the leftmost leaf immediately.\n\n**Worst case:** k = n — all nodes visited.", + + realWorldUses: [ + "**In-order streaming:** Lazily produce sorted values from a BST without buffering all of them.", + "**Cursor-based iteration:** BST iterator (next/hasNext) is the direct basis for this approach.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — bounded call stack.", + "Natural extension to a BST iterator pattern.", + ], + limitations: ["O(h) stack space — same asymptotic as recursive version."], + }, + + whenToUseIt: + "Use when you need a BST iterator that can pause after k elements or when recursion is prohibited.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/index.ts new file mode 100644 index 00000000..f82c2e96 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/index.ts @@ -0,0 +1,116 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstKthSmallestIterative } from "./sources/bst-kth-smallest-iterative.ts?fn"; +import { generateBstKthSmallestIterativeSteps } from "./step-generator"; +import type { BstKthSmallestIterativeInput } from "./step-generator"; +import { bstKthSmallestIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-kth-smallest-iterative.ts?raw"; +import pythonSource from "./sources/bst-kth-smallest-iterative.py?raw"; +import javaSource from "./sources/BSTKthSmallestIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstKthSmallestIterative(input: BstKthSmallestIterativeInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstKthSmallestIterative(root, input.kthPosition) as number; +} + +const bstKthSmallestIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_KTH_SMALLEST_ITERATIVE!, + name: "BST Kth Smallest (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Stack-based in-order traversal counting nodes to find the kth smallest value", + timeComplexity: { best: "O(k)", average: "O(k)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", kthPosition: 3 }, + }, + execute: executeBstKthSmallestIterative, + generateSteps: generateBstKthSmallestIterativeSteps, + educational: bstKthSmallestIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstKthSmallestIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/BSTKthSmallestIterative.java b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/BSTKthSmallestIterative.java new file mode 100644 index 00000000..30b43239 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/BSTKthSmallestIterative.java @@ -0,0 +1,37 @@ +// BST Kth Smallest (Iterative) — stack-based in-order with counter +import java.util.Stack; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTKthSmallestIterative { + public int bstKthSmallestIterative(BSTNode root, int kthPosition) { + Stack stack = new Stack<>(); // @step:initialize + int counter = 0; + BSTNode current = root; + + while (current != null || !stack.isEmpty()) { + // Push all left nodes — they have smaller values + while (current != null) { + stack.push(current); // @step:search-node + current = current.left; + } + + // Process next in-order node + current = stack.pop(); + counter++; + + if (counter == kthPosition) { + return current.value; // @step:found + } + + // Move to right subtree + current = current.right; // @step:search-node + } + + return -1; // @step:complete — k exceeds number of nodes + } +} diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/bst-kth-smallest-iterative.py b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/bst-kth-smallest-iterative.py new file mode 100644 index 00000000..85ede625 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/bst-kth-smallest-iterative.py @@ -0,0 +1,30 @@ +# BST Kth Smallest (Iterative) — stack-based in-order with counter + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_kth_smallest_iterative(root, kth_position: int) -> int: + stack = [] # @step:initialize + counter = 0 + current = root + + while current is not None or len(stack) > 0: + # Push all left nodes — they have smaller values + while current is not None: + stack.append(current) # @step:search-node + current = current.left + + # Process next in-order node + current = stack.pop() + counter += 1 + + if counter == kth_position: + return current.value # @step:found + + # Move to right subtree + current = current.right # @step:search-node + + return -1 # @step:complete — k exceeds number of nodes diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/bst-kth-smallest-iterative.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/bst-kth-smallest-iterative.ts new file mode 100644 index 00000000..208d5d44 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/sources/bst-kth-smallest-iterative.ts @@ -0,0 +1,34 @@ +// BST Kth Smallest (Iterative) — stack-based in-order with counter + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstKthSmallestIterative(root: BSTNode | null, kthPosition: number): number { + const stack: BSTNode[] = []; // @step:initialize + let counter = 0; + let current = root; + + while (current !== null || stack.length > 0) { + // Push all left nodes — they have smaller values + while (current !== null) { + stack.push(current); // @step:search-node + current = current.left; + } + + // Process next in-order node + current = stack.pop()!; + counter++; + + if (counter === kthPosition) { + return current.value; // @step:found + } + + // Move to right subtree + current = current.right; // @step:search-node + } + + return -1; // @step:complete — k exceeds number of nodes +} diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/step-generator.test.ts new file mode 100644 index 00000000..2f873afd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/step-generator.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstKthSmallestIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstKthSmallestIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstKthSmallestIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstKthSmallestIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstKthSmallestIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstKthSmallestIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstKthSmallestIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/step-generator.ts new file mode 100644 index 00000000..37da41ef --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest-iterative/step-generator.ts @@ -0,0 +1,61 @@ +/** Step generator for BST Kth Smallest (Iterative) — stack-based in-order with counter. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_KTH_SMALLEST_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.BST_KTH_SMALLEST_ITERATIVE!, +); + +export interface BstKthSmallestIterativeInput { + nodes: TreeNode[]; + rootId: string; + kthPosition: number; +} + +export function generateBstKthSmallestIterativeSteps( + input: BstKthSmallestIterativeInput, +): ExecutionStep[] { + const { nodes, rootId, kthPosition } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_KTH_SMALLEST_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId, kthPosition }); + + const stack: string[] = []; + let counter = 0; + let currentId: string | null = rootId; + let resultValue = -1; + + outer: while (currentId !== null || stack.length > 0) { + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + stack.push(currentId); + tracker.compareNode(currentId, node.value, { currentNode: currentId, pushing: true }); + currentId = node.leftChildId; + } + + const poppedId = stack.pop(); + if (!poppedId) break; + const node = nodeMap.get(poppedId); + if (!node) break; + + counter++; + tracker.compareNode(poppedId, node.value, { currentNode: poppedId, counter, kthPosition }); + + if (counter === kthPosition) { + resultValue = node.value; + tracker.searchFound(poppedId, { kthPosition, result: resultValue }); + break outer; + } + + currentId = node.rightChildId; + } + + tracker.complete({ kthPosition, result: resultValue }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/BSTKthSmallestPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-kth-smallest/BSTKthSmallestPipeline.stories.tsx new file mode 100644 index 00000000..2f33faaf --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/BSTKthSmallestPipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstKthSmallestSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstKthSmallestSteps({ nodes: defaultNodes, rootId: "n4", kthPosition: 3 }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Kth Smallest (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/bst-kth-smallest.test.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest/bst-kth-smallest.test.ts new file mode 100644 index 00000000..45d74f9b --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/bst-kth-smallest.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { bstKthSmallest } from "./sources/bst-kth-smallest.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstKthSmallest", () => { + it("finds 1st smallest", () => { + expect(bstKthSmallest(tree, 1)).toBe(1); + }); + it("finds 3rd smallest", () => { + expect(bstKthSmallest(tree, 3)).toBe(3); + }); + it("finds 7th smallest (largest)", () => { + expect(bstKthSmallest(tree, 7)).toBe(7); + }); + it("finds 4th smallest (root)", () => { + expect(bstKthSmallest(tree, 4)).toBe(4); + }); + it("returns -1 when k exceeds node count", () => { + expect(bstKthSmallest(tree, 10)).toBe(-1); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/educational.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest/educational.ts new file mode 100644 index 00000000..f3dc72a5 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/educational.ts @@ -0,0 +1,39 @@ +import type { EducationalContent } from "@/types"; + +export const bstKthSmallestEducational: EducationalContent = { + overview: + "**BST Kth Smallest (Recursive)** finds the kth smallest value in a BST by performing an in-order traversal (which yields values in ascending order) and stopping as soon as a counter reaches `k`.\n\nNo sorting is needed — the BST property guarantees that in-order traversal naturally visits values from smallest to largest.", + + howItWorks: + "1. Recurse left (smaller values first).\n" + + "2. Increment a counter when visiting each node.\n" + + "3. When `counter === k`, record the current value and stop further recursion.\n" + + "4. Return the recorded value.\n\n" + + "The algorithm stops early once found, avoiding unnecessary traversal of the right subtree.", + + timeAndSpaceComplexity: + "**Time: `O(k + h)`** — visits `k` in-order nodes plus the path to reach the first.\n\n**Space: `O(h)`** — call stack.", + + bestAndWorstCase: + "**Best case:** k = 1 — the leftmost leaf is found after traversing one root-to-leaf path.\n\n**Worst case:** k = n — all nodes are visited.", + + realWorldUses: [ + "**Percentile ranking:** Find the kth element in a dynamically maintained ordered set.", + "**Database ORDER BY with LIMIT:** BST in-order traversal stopping at row k.", + "**Statistical medians:** With k = n/2, find the median element.", + ], + + strengthsAndLimitations: { + strengths: [ + "Leverages the BST property — no explicit sorting needed.", + "Stops early once the kth element is found.", + ], + limitations: [ + "Repeated queries for different k values each require an O(k) traversal.", + "An augmented BST (storing subtree sizes) can answer kth-smallest in O(log n).", + ], + }, + + whenToUseIt: + "Use for occasional rank queries on an existing BST. For frequent rank queries, augment the BST with subtree size counters.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/index.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest/index.ts new file mode 100644 index 00000000..c83d23da --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/index.ts @@ -0,0 +1,116 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstKthSmallest } from "./sources/bst-kth-smallest.ts?fn"; +import { generateBstKthSmallestSteps } from "./step-generator"; +import type { BstKthSmallestInput } from "./step-generator"; +import { bstKthSmallestEducational } from "./educational"; + +import typescriptSource from "./sources/bst-kth-smallest.ts?raw"; +import pythonSource from "./sources/bst-kth-smallest.py?raw"; +import javaSource from "./sources/BSTKthSmallest.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstKthSmallest(input: BstKthSmallestInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstKthSmallest(root, input.kthPosition) as number; +} + +const bstKthSmallestDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_KTH_SMALLEST!, + name: "BST Kth Smallest (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Recursive in-order traversal counting nodes until the kth smallest is reached", + timeComplexity: { best: "O(k)", average: "O(k)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", kthPosition: 3 }, + }, + execute: executeBstKthSmallest, + generateSteps: generateBstKthSmallestSteps, + educational: bstKthSmallestEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstKthSmallestDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/BSTKthSmallest.java b/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/BSTKthSmallest.java new file mode 100644 index 00000000..1f4b41df --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/BSTKthSmallest.java @@ -0,0 +1,36 @@ +// BST Kth Smallest (Recursive) — in-order traversal with counter, stop at k + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTKthSmallest { + private int counter = 0; // @step:initialize + private int result = -1; + + public int bstKthSmallest(BSTNode root, int kthPosition) { + counter = 0; // @step:initialize + result = -1; + inorder(root, kthPosition); + return result; // @step:complete + } + + private void inorder(BSTNode node, int kthPosition) { + if (node == null || counter >= kthPosition) return; // @step:initialize + + // Visit left subtree first (smaller values) + inorder(node.left, kthPosition); // @step:search-node + + // Visit current node — increment counter + counter++; + if (counter == kthPosition) { + result = node.value; // @step:found + return; + } + + // Visit right subtree (larger values) + inorder(node.right, kthPosition); // @step:search-node + } +} diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/bst-kth-smallest.py b/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/bst-kth-smallest.py new file mode 100644 index 00000000..f442079d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/bst-kth-smallest.py @@ -0,0 +1,30 @@ +# BST Kth Smallest (Recursive) — in-order traversal with counter, stop at k + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_kth_smallest(root, kth_position: int) -> int: + counter = [0] # @step:initialize + result = [-1] + + def inorder(node) -> None: + if node is None or counter[0] >= kth_position: # @step:initialize + return + + # Visit left subtree first (smaller values) + inorder(node.left) # @step:search-node + + # Visit current node — increment counter + counter[0] += 1 + if counter[0] == kth_position: + result[0] = node.value # @step:found + return + + # Visit right subtree (larger values) + inorder(node.right) # @step:search-node + + inorder(root) + return result[0] # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/bst-kth-smallest.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/bst-kth-smallest.ts new file mode 100644 index 00000000..c4922526 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/sources/bst-kth-smallest.ts @@ -0,0 +1,32 @@ +// BST Kth Smallest (Recursive) — in-order traversal with counter, stop at k + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstKthSmallest(root: BSTNode | null, kthPosition: number): number { + let counter = 0; // @step:initialize + let result = -1; + + function inorder(node: BSTNode | null): void { + if (node === null || counter >= kthPosition) return; // @step:initialize + + // Visit left subtree first (smaller values) + inorder(node.left); // @step:search-node + + // Visit current node — increment counter + counter++; + if (counter === kthPosition) { + result = node.value; // @step:found + return; + } + + // Visit right subtree (larger values) + inorder(node.right); // @step:search-node + } + + inorder(root); + return result; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest/step-generator.test.ts new file mode 100644 index 00000000..1f0f190c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/step-generator.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstKthSmallestSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstKthSmallestSteps", () => { + it("produces steps", () => { + const steps = generateBstKthSmallestSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstKthSmallestSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstKthSmallestSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstKthSmallestSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstKthSmallestSteps({ + nodes: defaultNodes, + rootId: "n4", + kthPosition: 3, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-kth-smallest/step-generator.ts b/src/algorithms/trees/bst-operations/bst-kth-smallest/step-generator.ts new file mode 100644 index 00000000..0fdcd4de --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-kth-smallest/step-generator.ts @@ -0,0 +1,58 @@ +/** Step generator for BST Kth Smallest (Recursive) — in-order with counter. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_KTH_SMALLEST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_KTH_SMALLEST!); + +export interface BstKthSmallestInput { + nodes: TreeNode[]; + rootId: string; + kthPosition: number; +} + +export function generateBstKthSmallestSteps(input: BstKthSmallestInput): ExecutionStep[] { + const { nodes, rootId, kthPosition } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_KTH_SMALLEST_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId, kthPosition }); + + let counter = 0; + let resultValue = -1; + let found = false; + + function inorder(nodeId: string | null): void { + if (!nodeId || found) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + inorder(node.leftChildId); + + if (!found) { + counter++; + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + counter, + kthPosition, + nodeValue: node.value, + }); + + if (counter === kthPosition) { + resultValue = node.value; + found = true; + tracker.searchFound(nodeId, { kthPosition, result: resultValue }); + return; + } + + inorder(node.rightChildId); + } + } + + inorder(rootId); + tracker.complete({ kthPosition, result: resultValue }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/BSTLowestCommonAncestorIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/BSTLowestCommonAncestorIterativePipeline.stories.tsx new file mode 100644 index 00000000..8f1d6577 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/BSTLowestCommonAncestorIterativePipeline.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstLowestCommonAncestorIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Lowest Common Ancestor (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/bst-lowest-common-ancestor-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/bst-lowest-common-ancestor-iterative.test.ts new file mode 100644 index 00000000..39d4d751 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/bst-lowest-common-ancestor-iterative.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { bstLowestCommonAncestorIterative } from "./sources/bst-lowest-common-ancestor-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstLowestCommonAncestorIterative", () => { + it("finds LCA of 1 and 3", () => { + const result = bstLowestCommonAncestorIterative(tree, 1, 3) as BSTNode | null; + expect(result?.value).toBe(2); + }); + it("finds LCA of 5 and 7", () => { + const result = bstLowestCommonAncestorIterative(tree, 5, 7) as BSTNode | null; + expect(result?.value).toBe(6); + }); + it("finds root as LCA for 1 and 7", () => { + const result = bstLowestCommonAncestorIterative(tree, 1, 7) as BSTNode | null; + expect(result?.value).toBe(4); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/educational.ts new file mode 100644 index 00000000..3343f236 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/educational.ts @@ -0,0 +1,25 @@ +import type { EducationalContent } from "@/types"; + +export const bstLowestCommonAncestorIterativeEducational: EducationalContent = { + overview: + "**BST Lowest Common Ancestor (Iterative)** finds the LCA using a while loop instead of recursion. A single pointer walks the tree until the two values split to different sides of the current node.", + + howItWorks: + "Start at root. At each step:\n- If both target values are less than the current node → move left.\n- If both are greater → move right.\n- Otherwise → the current node is the LCA (values diverge here).", + + timeAndSpaceComplexity: "**Time: `O(h)`**\n\n**Space: `O(1)`** — only a single pointer variable.", + + bestAndWorstCase: "**Best case:** LCA at root — O(1).\n\n**Worst case:** LCA near a leaf — O(h).", + + realWorldUses: [ + "**Constant-space LCA:** Memory-efficient lookup for embedded or real-time systems.", + "**Database join optimization:** Find common ancestor in an index tree.", + ], + + strengthsAndLimitations: { + strengths: ["O(1) space — most memory-efficient BST LCA possible.", "Simple and fast."], + limitations: ["BST-specific — does not generalize to non-ordered trees."], + }, + + whenToUseIt: "Use when O(1) space is required or as the default implementation in a BST class.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/index.ts new file mode 100644 index 00000000..81f56e04 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/index.ts @@ -0,0 +1,124 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstLowestCommonAncestorIterative } from "./sources/bst-lowest-common-ancestor-iterative.ts?fn"; +import { generateBstLowestCommonAncestorIterativeSteps } from "./step-generator"; +import type { BstLowestCommonAncestorIterativeInput } from "./step-generator"; +import { bstLowestCommonAncestorIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-lowest-common-ancestor-iterative.ts?raw"; +import pythonSource from "./sources/bst-lowest-common-ancestor-iterative.py?raw"; +import javaSource from "./sources/BSTLowestCommonAncestorIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstLowestCommonAncestorIterative( + input: BstLowestCommonAncestorIterativeInput, +): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstLowestCommonAncestorIterative( + root, + input.nodeValueA, + input.nodeValueB, + ) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstLowestCommonAncestorIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.BST_LOWEST_COMMON_ANCESTOR_ITERATIVE!, + name: "BST Lowest Common Ancestor (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Iterative LCA: walk left/right until values split across current node", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", nodeValueA: 1, nodeValueB: 3 }, + }, + execute: executeBstLowestCommonAncestorIterative, + generateSteps: generateBstLowestCommonAncestorIterativeSteps, + educational: bstLowestCommonAncestorIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, + }; + +registry.register(bstLowestCommonAncestorIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/BSTLowestCommonAncestorIterative.java b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/BSTLowestCommonAncestorIterative.java new file mode 100644 index 00000000..7aa452e9 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/BSTLowestCommonAncestorIterative.java @@ -0,0 +1,28 @@ +// BST Lowest Common Ancestor (Iterative) — while loop split point search + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTLowestCommonAncestorIterative { + public BSTNode bstLowestCommonAncestorIterative(BSTNode root, int nodeValueA, int nodeValueB) { + BSTNode current = root; // @step:initialize + + while (current != null) { + if (nodeValueA < current.value && nodeValueB < current.value) { + // Both values are smaller — move to left subtree + current = current.left; // @step:search-node + } else if (nodeValueA > current.value && nodeValueB > current.value) { + // Both values are larger — move to right subtree + current = current.right; // @step:search-node + } else { + // Values split across current (or one equals current) — found LCA + return current; // @step:found + } + } + + return null; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/bst-lowest-common-ancestor-iterative.py b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/bst-lowest-common-ancestor-iterative.py new file mode 100644 index 00000000..b6d6100d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/bst-lowest-common-ancestor-iterative.py @@ -0,0 +1,23 @@ +# BST Lowest Common Ancestor (Iterative) — while loop split point search + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_lowest_common_ancestor_iterative(root, node_value_a: int, node_value_b: int): + current = root # @step:initialize + + while current is not None: + if node_value_a < current.value and node_value_b < current.value: + # Both values are smaller — move to left subtree + current = current.left # @step:search-node + elif node_value_a > current.value and node_value_b > current.value: + # Both values are larger — move to right subtree + current = current.right # @step:search-node + else: + # Values split across current (or one equals current) — found LCA + return current # @step:found + + return None # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/bst-lowest-common-ancestor-iterative.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/bst-lowest-common-ancestor-iterative.ts new file mode 100644 index 00000000..2bc67513 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/sources/bst-lowest-common-ancestor-iterative.ts @@ -0,0 +1,30 @@ +// BST Lowest Common Ancestor (Iterative) — while loop split point search + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstLowestCommonAncestorIterative( + root: BSTNode | null, + nodeValueA: number, + nodeValueB: number, +): BSTNode | null { + let current = root; // @step:initialize + + while (current !== null) { + if (nodeValueA < current.value && nodeValueB < current.value) { + // Both values are smaller — move to left subtree + current = current.left; // @step:search-node + } else if (nodeValueA > current.value && nodeValueB > current.value) { + // Both values are larger — move to right subtree + current = current.right; // @step:search-node + } else { + // Values split across current (or one equals current) — found LCA + return current; // @step:found + } + } + + return null; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/step-generator.test.ts new file mode 100644 index 00000000..cb5b8de4 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/step-generator.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstLowestCommonAncestorIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstLowestCommonAncestorIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/step-generator.ts new file mode 100644 index 00000000..ea512fa2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor-iterative/step-generator.ts @@ -0,0 +1,56 @@ +/** Step generator for BST Lowest Common Ancestor (Iterative). */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LCA_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.BST_LOWEST_COMMON_ANCESTOR_ITERATIVE!, +); + +export interface BstLowestCommonAncestorIterativeInput { + nodes: TreeNode[]; + rootId: string; + nodeValueA: number; + nodeValueB: number; +} + +export function generateBstLowestCommonAncestorIterativeSteps( + input: BstLowestCommonAncestorIterativeInput, +): ExecutionStep[] { + const { nodes, rootId, nodeValueA, nodeValueB } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_LCA_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId, nodeValueA, nodeValueB }); + + let currentId: string | null = rootId; + let lcaValue: number | null = null; + + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + nodeValue: node.value, + nodeValueA, + nodeValueB, + }); + + if (nodeValueA < node.value && nodeValueB < node.value) { + currentId = node.leftChildId; + } else if (nodeValueA > node.value && nodeValueB > node.value) { + currentId = node.rightChildId; + } else { + lcaValue = node.value; + tracker.searchFound(currentId, { lcaNode: currentId, lcaValue }); + break; + } + } + + tracker.complete({ lcaValue, nodeValueA, nodeValueB }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/BSTLowestCommonAncestorPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/BSTLowestCommonAncestorPipeline.stories.tsx new file mode 100644 index 00000000..e216f281 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/BSTLowestCommonAncestorPipeline.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstLowestCommonAncestorSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Lowest Common Ancestor", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/bst-lowest-common-ancestor.test.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/bst-lowest-common-ancestor.test.ts new file mode 100644 index 00000000..8992144b --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/bst-lowest-common-ancestor.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { bstLowestCommonAncestor } from "./sources/bst-lowest-common-ancestor.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstLowestCommonAncestor", () => { + it("finds LCA of 1 and 3 (which is 2)", () => { + const result = bstLowestCommonAncestor(tree, 1, 3) as BSTNode | null; + expect(result?.value).toBe(2); + }); + it("finds LCA of 1 and 7 (which is 4 = root)", () => { + const result = bstLowestCommonAncestor(tree, 1, 7) as BSTNode | null; + expect(result?.value).toBe(4); + }); + it("finds LCA of 5 and 7 (which is 6)", () => { + const result = bstLowestCommonAncestor(tree, 5, 7) as BSTNode | null; + expect(result?.value).toBe(6); + }); + it("returns node itself when one value equals LCA", () => { + const result = bstLowestCommonAncestor(tree, 2, 3) as BSTNode | null; + expect(result?.value).toBe(2); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/educational.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/educational.ts new file mode 100644 index 00000000..4418bd00 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/educational.ts @@ -0,0 +1,32 @@ +import type { EducationalContent } from "@/types"; + +export const bstLowestCommonAncestorEducational: EducationalContent = { + overview: + "**BST Lowest Common Ancestor (Recursive)** finds the deepest node that is an ancestor of both given values, exploiting the BST property to navigate directly to the split point without extra data structures.\n\nThe LCA is the first node encountered where the two values no longer fall on the same side.", + + howItWorks: + "At each node:\n1. If both values are smaller — LCA is in the left subtree; recurse left.\n2. If both values are larger — LCA is in the right subtree; recurse right.\n3. Otherwise (values split across current node, or one value equals the current node) — the current node is the LCA.\n\nThis directly exploits the BST ordering invariant — no need to traverse both subtrees.", + + timeAndSpaceComplexity: + "**Time: `O(h)`** — at most one root-to-LCA path.\n\n**Space: `O(h)`** — call stack.", + + bestAndWorstCase: + "**Best case:** LCA is the root — found immediately.\n\n**Worst case:** LCA is a leaf — full height traversal.", + + realWorldUses: [ + "**Organizational hierarchies:** Find the common manager of two employees in a BST of employee IDs.", + "**File system paths:** Find the common ancestor directory of two paths in a sorted BST.", + "**Network routing:** Find the most specific common subnet prefix.", + ], + + strengthsAndLimitations: { + strengths: [ + "Exploits BST property — much simpler than general binary tree LCA algorithms.", + "O(h) time with no auxiliary data structures.", + ], + limitations: ["Only works on BSTs — general trees require DFS with two separate path arrays."], + }, + + whenToUseIt: + "Use when both values are guaranteed to exist in the BST. For general binary trees, use the standard post-order DFS LCA algorithm.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/index.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/index.ts new file mode 100644 index 00000000..70cd4f9b --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/index.ts @@ -0,0 +1,122 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstLowestCommonAncestor } from "./sources/bst-lowest-common-ancestor.ts?fn"; +import { generateBstLowestCommonAncestorSteps } from "./step-generator"; +import type { BstLowestCommonAncestorInput } from "./step-generator"; +import { bstLowestCommonAncestorEducational } from "./educational"; + +import typescriptSource from "./sources/bst-lowest-common-ancestor.ts?raw"; +import pythonSource from "./sources/bst-lowest-common-ancestor.py?raw"; +import javaSource from "./sources/BSTLowestCommonAncestor.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstLowestCommonAncestor(input: BstLowestCommonAncestorInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstLowestCommonAncestor( + root, + input.nodeValueA, + input.nodeValueB, + ) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstLowestCommonAncestorDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_LOWEST_COMMON_ANCESTOR!, + name: "BST Lowest Common Ancestor (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Recursive LCA using BST property: split point where two values diverge left and right", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", nodeValueA: 1, nodeValueB: 3 }, + }, + execute: executeBstLowestCommonAncestor, + generateSteps: generateBstLowestCommonAncestorSteps, + educational: bstLowestCommonAncestorEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstLowestCommonAncestorDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/BSTLowestCommonAncestor.java b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/BSTLowestCommonAncestor.java new file mode 100644 index 00000000..63451f70 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/BSTLowestCommonAncestor.java @@ -0,0 +1,26 @@ +// BST Lowest Common Ancestor (Recursive) — use BST property to find split point + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTLowestCommonAncestor { + public BSTNode bstLowestCommonAncestor(BSTNode root, int nodeValueA, int nodeValueB) { + if (root == null) return null; // @step:initialize + + if (nodeValueA < root.value && nodeValueB < root.value) { + // Both values are smaller — LCA must be in the left subtree + return bstLowestCommonAncestor(root.left, nodeValueA, nodeValueB); // @step:search-node + } + + if (nodeValueA > root.value && nodeValueB > root.value) { + // Both values are larger — LCA must be in the right subtree + return bstLowestCommonAncestor(root.right, nodeValueA, nodeValueB); // @step:search-node + } + + // Values split across root (or one equals root) — current node is the LCA + return root; // @step:found + } +} diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/bst-lowest-common-ancestor.py b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/bst-lowest-common-ancestor.py new file mode 100644 index 00000000..3b545540 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/bst-lowest-common-ancestor.py @@ -0,0 +1,22 @@ +# BST Lowest Common Ancestor (Recursive) — use BST property to find split point + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_lowest_common_ancestor(root, node_value_a: int, node_value_b: int): + if root is None: # @step:initialize + return None + + if node_value_a < root.value and node_value_b < root.value: + # Both values are smaller — LCA must be in the left subtree + return bst_lowest_common_ancestor(root.left, node_value_a, node_value_b) # @step:search-node + + if node_value_a > root.value and node_value_b > root.value: + # Both values are larger — LCA must be in the right subtree + return bst_lowest_common_ancestor(root.right, node_value_a, node_value_b) # @step:search-node + + # Values split across root (or one equals root) — current node is the LCA + return root # @step:found diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/bst-lowest-common-ancestor.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/bst-lowest-common-ancestor.ts new file mode 100644 index 00000000..8bb2a79f --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/sources/bst-lowest-common-ancestor.ts @@ -0,0 +1,28 @@ +// BST Lowest Common Ancestor (Recursive) — use BST property to find split point + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstLowestCommonAncestor( + root: BSTNode | null, + nodeValueA: number, + nodeValueB: number, +): BSTNode | null { + if (root === null) return null; // @step:initialize + + if (nodeValueA < root.value && nodeValueB < root.value) { + // Both values are smaller — LCA must be in the left subtree + return bstLowestCommonAncestor(root.left, nodeValueA, nodeValueB); // @step:search-node + } + + if (nodeValueA > root.value && nodeValueB > root.value) { + // Both values are larger — LCA must be in the right subtree + return bstLowestCommonAncestor(root.right, nodeValueA, nodeValueB); // @step:search-node + } + + // Values split across root (or one equals root) — current node is the LCA + return root; // @step:found +} diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/step-generator.test.ts new file mode 100644 index 00000000..fd9527bd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/step-generator.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstLowestCommonAncestorSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstLowestCommonAncestorSteps", () => { + it("produces steps", () => { + const steps = generateBstLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/step-generator.ts b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/step-generator.ts new file mode 100644 index 00000000..f2302c0e --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-lowest-common-ancestor/step-generator.ts @@ -0,0 +1,54 @@ +/** Step generator for BST Lowest Common Ancestor (Recursive). */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LCA_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_LOWEST_COMMON_ANCESTOR!); + +export interface BstLowestCommonAncestorInput { + nodes: TreeNode[]; + rootId: string; + nodeValueA: number; + nodeValueB: number; +} + +export function generateBstLowestCommonAncestorSteps( + input: BstLowestCommonAncestorInput, +): ExecutionStep[] { + const { nodes, rootId, nodeValueA, nodeValueB } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_LCA_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId, nodeValueA, nodeValueB }); + + function findLca(nodeId: string | null): string | null { + if (!nodeId) return null; + const node = nodeMap.get(nodeId); + if (!node) return null; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + nodeValue: node.value, + nodeValueA, + nodeValueB, + }); + + if (nodeValueA < node.value && nodeValueB < node.value) { + return findLca(node.leftChildId); + } + if (nodeValueA > node.value && nodeValueB > node.value) { + return findLca(node.rightChildId); + } + + tracker.searchFound(nodeId, { lcaNode: nodeId, lcaValue: node.value }); + return nodeId; + } + + const lcaId = findLca(rootId); + const lcaNode = lcaId ? nodeMap.get(lcaId) : null; + tracker.complete({ lcaValue: lcaNode?.value ?? null, nodeValueA, nodeValueB }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/BSTRangeSumIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/BSTRangeSumIterativePipeline.stories.tsx new file mode 100644 index 00000000..6442a33f --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/BSTRangeSumIterativePipeline.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstRangeSumIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstRangeSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Range Sum (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/bst-range-sum-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/bst-range-sum-iterative.test.ts new file mode 100644 index 00000000..b2e5bc10 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/bst-range-sum-iterative.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { bstRangeSumIterative } from "./sources/bst-range-sum-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstRangeSumIterative", () => { + it("sums values in [3, 7]", () => { + expect(bstRangeSumIterative(tree, 3, 7)).toBe(3 + 4 + 5 + 6 + 7); + }); + it("sums all values", () => { + expect(bstRangeSumIterative(tree, 1, 7)).toBe(28); + }); + it("returns 0 for no match", () => { + expect(bstRangeSumIterative(tree, 10, 20)).toBe(0); + }); + it("handles null tree", () => { + expect(bstRangeSumIterative(null, 1, 7)).toBe(0); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/educational.ts new file mode 100644 index 00000000..f12db500 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/educational.ts @@ -0,0 +1,28 @@ +import type { EducationalContent } from "@/types"; + +export const bstRangeSumIterativeEducational: EducationalContent = { + overview: + "**BST Range Sum (Iterative)** uses an explicit stack for DFS traversal, summing values in `[low, high]` and pushing children only when they might contain in-range values.", + + howItWorks: + "Start with root in the stack. At each node:\n1. If the value is in `[low, high]`, add to the running sum.\n2. Push the left child only if `node.value > low` (left subtree could have in-range values).\n3. Push the right child only if `node.value < high`.\n\nThis avoids visiting entire subtrees outside the range.", + + timeAndSpaceComplexity: + "**Time: `O(log n + k)`** — same pruning as recursive version.\n\n**Space: `O(h)`** — explicit stack.", + + bestAndWorstCase: + "**Best case:** Narrow range near a leaf — few nodes visited.\n\n**Worst case:** Range covers all nodes — O(n).", + + realWorldUses: [ + "**Streaming range queries:** Process results as soon as nodes are popped from the stack.", + "**Stack-safe traversal:** No recursion depth concerns.", + ], + + strengthsAndLimitations: { + strengths: ["No recursion.", "Same BST pruning efficiency as recursive version."], + limitations: ["O(h) stack space for the explicit stack."], + }, + + whenToUseIt: + "Use when you need range sum without recursion, or when iterating over a generator-style interface.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/index.ts new file mode 100644 index 00000000..92d94128 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstRangeSumIterative } from "./sources/bst-range-sum-iterative.ts?fn"; +import { generateBstRangeSumIterativeSteps } from "./step-generator"; +import type { BstRangeSumIterativeInput } from "./step-generator"; +import { bstRangeSumIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-range-sum-iterative.ts?raw"; +import pythonSource from "./sources/bst-range-sum-iterative.py?raw"; +import javaSource from "./sources/BSTRangeSumIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstRangeSumIterative(input: BstRangeSumIterativeInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstRangeSumIterative(root, input.lowValue, input.highValue) as number; +} + +const bstRangeSumIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_RANGE_SUM_ITERATIVE!, + name: "BST Range Sum (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Stack-based DFS to sum all BST nodes in [low, high], skipping out-of-range subtrees", + timeComplexity: { best: "O(log n)", average: "O(log n + k)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", lowValue: 2, highValue: 6 }, + }, + execute: executeBstRangeSumIterative, + generateSteps: generateBstRangeSumIterativeSteps, + educational: bstRangeSumIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstRangeSumIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/BSTRangeSumIterative.java b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/BSTRangeSumIterative.java new file mode 100644 index 00000000..78f98a23 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/BSTRangeSumIterative.java @@ -0,0 +1,39 @@ +// BST Range Sum (Iterative) — stack-based DFS summing nodes in [lowValue, highValue] +import java.util.Stack; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTRangeSumIterative { + public int bstRangeSumIterative(BSTNode root, int lowValue, int highValue) { + if (root == null) return 0; // @step:initialize + + Stack stack = new Stack<>(); + stack.push(root); + int totalSum = 0; + + while (!stack.isEmpty()) { + BSTNode node = stack.pop(); + + if (node.value >= lowValue && node.value <= highValue) { + // Node is in range — add to sum + totalSum += node.value; // @step:found + } + + if (node.left != null && node.value > lowValue) { + // Left child exists and may have values in range + stack.push(node.left); // @step:search-node + } + + if (node.right != null && node.value < highValue) { + // Right child exists and may have values in range + stack.push(node.right); // @step:search-node + } + } + + return totalSum; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/bst-range-sum-iterative.py b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/bst-range-sum-iterative.py new file mode 100644 index 00000000..214fbf2f --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/bst-range-sum-iterative.py @@ -0,0 +1,31 @@ +# BST Range Sum (Iterative) — stack-based DFS summing nodes in [low_value, high_value] + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_range_sum_iterative(root, low_value: int, high_value: int) -> int: + if root is None: # @step:initialize + return 0 + + stack = [root] + total_sum = 0 + + while len(stack) > 0: + node = stack.pop() + + if low_value <= node.value <= high_value: + # Node is in range — add to sum + total_sum += node.value # @step:found + + if node.left is not None and node.value > low_value: + # Left child exists and may have values in range + stack.append(node.left) # @step:search-node + + if node.right is not None and node.value < high_value: + # Right child exists and may have values in range + stack.append(node.right) # @step:search-node + + return total_sum # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/bst-range-sum-iterative.ts b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/bst-range-sum-iterative.ts new file mode 100644 index 00000000..96211d83 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/sources/bst-range-sum-iterative.ts @@ -0,0 +1,35 @@ +// BST Range Sum (Iterative) — stack-based DFS summing nodes in [lowValue, highValue] + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstRangeSumIterative(root: BSTNode | null, lowValue: number, highValue: number): number { + if (root === null) return 0; // @step:initialize + + const stack: BSTNode[] = [root]; + let totalSum = 0; + + while (stack.length > 0) { + const node = stack.pop()!; + + if (node.value >= lowValue && node.value <= highValue) { + // Node is in range — add to sum + totalSum += node.value; // @step:found + } + + if (node.left !== null && node.value > lowValue) { + // Left child exists and may have values in range + stack.push(node.left); // @step:search-node + } + + if (node.right !== null && node.value < highValue) { + // Right child exists and may have values in range + stack.push(node.right); // @step:search-node + } + } + + return totalSum; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/step-generator.test.ts new file mode 100644 index 00000000..84fd5611 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/step-generator.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstRangeSumIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstRangeSumIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstRangeSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstRangeSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstRangeSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstRangeSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstRangeSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-range-sum-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/step-generator.ts new file mode 100644 index 00000000..41c57995 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum-iterative/step-generator.ts @@ -0,0 +1,57 @@ +/** Step generator for BST Range Sum (Iterative) — stack-based DFS summing in range. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_RANGE_SUM_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.BST_RANGE_SUM_ITERATIVE!, +); + +export interface BstRangeSumIterativeInput { + nodes: TreeNode[]; + rootId: string; + lowValue: number; + highValue: number; +} + +export function generateBstRangeSumIterativeSteps( + input: BstRangeSumIterativeInput, +): ExecutionStep[] { + const { nodes, rootId, lowValue, highValue } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_RANGE_SUM_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId, lowValue, highValue }); + + const stack: string[] = [rootId]; + let runningSum = 0; + + while (stack.length > 0) { + const currentId = stack.pop(); + if (!currentId) continue; + const node = nodeMap.get(currentId); + if (!node) continue; + + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + nodeValue: node.value, + lowValue, + highValue, + runningSum, + }); + + if (node.value >= lowValue && node.value <= highValue) { + runningSum += node.value; + tracker.searchFound(currentId, { nodeValue: node.value, runningSum }); + } + + if (node.leftChildId && node.value > lowValue) stack.push(node.leftChildId); + if (node.rightChildId && node.value < highValue) stack.push(node.rightChildId); + } + + tracker.complete({ lowValue, highValue, totalSum: runningSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/BSTRangeSumPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-range-sum/BSTRangeSumPipeline.stories.tsx new file mode 100644 index 00000000..1c34bef1 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/BSTRangeSumPipeline.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstRangeSumSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstRangeSumSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Range Sum (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/bst-range-sum.test.ts b/src/algorithms/trees/bst-operations/bst-range-sum/bst-range-sum.test.ts new file mode 100644 index 00000000..6b005673 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/bst-range-sum.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { bstRangeSum } from "./sources/bst-range-sum.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} +const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstRangeSum", () => { + it("sums values in [2, 6]", () => { + expect(bstRangeSum(tree, 2, 6)).toBe(2 + 3 + 4 + 5 + 6); + }); + it("sums all values", () => { + expect(bstRangeSum(tree, 1, 7)).toBe(28); + }); + it("returns 0 when range has no matching nodes", () => { + expect(bstRangeSum(tree, 10, 20)).toBe(0); + }); + it("sums single matching node", () => { + expect(bstRangeSum(tree, 4, 4)).toBe(4); + }); + it("handles null tree", () => { + expect(bstRangeSum(null, 1, 7)).toBe(0); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/educational.ts b/src/algorithms/trees/bst-operations/bst-range-sum/educational.ts new file mode 100644 index 00000000..72d178d9 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/educational.ts @@ -0,0 +1,32 @@ +import type { EducationalContent } from "@/types"; + +export const bstRangeSumEducational: EducationalContent = { + overview: + "**BST Range Sum (Recursive)** computes the sum of all node values within a given range `[low, high]` by exploiting the BST property to skip entire subtrees that cannot contain values in range.", + + howItWorks: + "At each node:\n1. If `node === null`, return 0.\n2. If `node.value` is within `[low, high]`, add it to the sum.\n3. If `node.value > low`, the left subtree may have in-range values — recurse left.\n4. If `node.value < high`, the right subtree may have in-range values — recurse right.\n\nSubtrees where all values are guaranteed to be out of range are pruned entirely.", + + timeAndSpaceComplexity: + "**Time: `O(log n + k)`** for a balanced tree where `k` is the number of in-range nodes (pruning skips out-of-range subtrees).\n\n**Space: `O(h)`** — call stack.", + + bestAndWorstCase: + "**Best case:** Range covers no nodes — only the root-to-boundary path is traversed.\n\n**Worst case:** Range covers all nodes — all `n` nodes are visited.", + + realWorldUses: [ + "**Financial aggregation:** Sum all transactions in a date range stored in a BST.", + "**Database range queries:** Sum indexed column values between two bounds.", + "**Game leaderboards:** Sum scores within a score band.", + ], + + strengthsAndLimitations: { + strengths: [ + "BST pruning skips whole subtrees — much faster than a full scan for narrow ranges.", + "Clean recursive implementation.", + ], + limitations: ["Wide ranges that include most nodes degrade to O(n)."], + }, + + whenToUseIt: + "Use for range aggregation queries on a dynamically maintained BST. For read-heavy workloads, a segment tree or Fenwick tree provides O(log n) range sum.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/index.ts b/src/algorithms/trees/bst-operations/bst-range-sum/index.ts new file mode 100644 index 00000000..6f6c3267 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/index.ts @@ -0,0 +1,116 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstRangeSum } from "./sources/bst-range-sum.ts?fn"; +import { generateBstRangeSumSteps } from "./step-generator"; +import type { BstRangeSumInput } from "./step-generator"; +import { bstRangeSumEducational } from "./educational"; + +import typescriptSource from "./sources/bst-range-sum.ts?raw"; +import pythonSource from "./sources/bst-range-sum.py?raw"; +import javaSource from "./sources/BSTRangeSum.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstRangeSum(input: BstRangeSumInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstRangeSum(root, input.lowValue, input.highValue) as number; +} + +const bstRangeSumDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_RANGE_SUM!, + name: "BST Range Sum (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Recursive sum of all BST nodes with values within [low, high] using BST pruning", + timeComplexity: { best: "O(log n)", average: "O(log n + k)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", lowValue: 2, highValue: 6 }, + }, + execute: executeBstRangeSum, + generateSteps: generateBstRangeSumSteps, + educational: bstRangeSumEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstRangeSumDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/sources/BSTRangeSum.java b/src/algorithms/trees/bst-operations/bst-range-sum/sources/BSTRangeSum.java new file mode 100644 index 00000000..29eea181 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/sources/BSTRangeSum.java @@ -0,0 +1,32 @@ +// BST Range Sum (Recursive) — sum all nodes with values in [lowValue, highValue] + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTRangeSum { + public int bstRangeSum(BSTNode root, int lowValue, int highValue) { + if (root == null) return 0; // @step:initialize + + int sum = 0; + + if (root.value >= lowValue && root.value <= highValue) { + // Current node is in range — add its value + sum += root.value; // @step:found + } + + if (root.value > lowValue) { + // Left subtree may contain values in range + sum += bstRangeSum(root.left, lowValue, highValue); // @step:search-node + } + + if (root.value < highValue) { + // Right subtree may contain values in range + sum += bstRangeSum(root.right, lowValue, highValue); // @step:search-node + } + + return sum; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/sources/bst-range-sum.py b/src/algorithms/trees/bst-operations/bst-range-sum/sources/bst-range-sum.py new file mode 100644 index 00000000..76688958 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/sources/bst-range-sum.py @@ -0,0 +1,27 @@ +# BST Range Sum (Recursive) — sum all nodes with values in [low_value, high_value] + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_range_sum(root, low_value: int, high_value: int) -> int: + if root is None: # @step:initialize + return 0 + + total = 0 + + if low_value <= root.value <= high_value: + # Current node is in range — add its value + total += root.value # @step:found + + if root.value > low_value: + # Left subtree may contain values in range + total += bst_range_sum(root.left, low_value, high_value) # @step:search-node + + if root.value < high_value: + # Right subtree may contain values in range + total += bst_range_sum(root.right, low_value, high_value) # @step:search-node + + return total # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/sources/bst-range-sum.ts b/src/algorithms/trees/bst-operations/bst-range-sum/sources/bst-range-sum.ts new file mode 100644 index 00000000..a6dd9657 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/sources/bst-range-sum.ts @@ -0,0 +1,30 @@ +// BST Range Sum (Recursive) — sum all nodes with values in [lowValue, highValue] + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstRangeSum(root: BSTNode | null, lowValue: number, highValue: number): number { + if (root === null) return 0; // @step:initialize + + let sum = 0; + + if (root.value >= lowValue && root.value <= highValue) { + // Current node is in range — add its value + sum += root.value; // @step:found + } + + if (root.value > lowValue) { + // Left subtree may contain values in range + sum += bstRangeSum(root.left, lowValue, highValue); // @step:search-node + } + + if (root.value < highValue) { + // Right subtree may contain values in range + sum += bstRangeSum(root.right, lowValue, highValue); // @step:search-node + } + + return sum; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-range-sum/step-generator.test.ts new file mode 100644 index 00000000..04953d0d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/step-generator.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstRangeSumSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstRangeSumSteps", () => { + it("produces steps", () => { + const steps = generateBstRangeSumSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstRangeSumSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstRangeSumSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstRangeSumSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstRangeSumSteps({ + nodes: defaultNodes, + rootId: "n4", + lowValue: 2, + highValue: 6, + }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-range-sum/step-generator.ts b/src/algorithms/trees/bst-operations/bst-range-sum/step-generator.ts new file mode 100644 index 00000000..5ae11258 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-range-sum/step-generator.ts @@ -0,0 +1,52 @@ +/** Step generator for BST Range Sum (Recursive) — sum nodes within [low, high]. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_RANGE_SUM_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_RANGE_SUM!); + +export interface BstRangeSumInput { + nodes: TreeNode[]; + rootId: string; + lowValue: number; + highValue: number; +} + +export function generateBstRangeSumSteps(input: BstRangeSumInput): ExecutionStep[] { + const { nodes, rootId, lowValue, highValue } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_RANGE_SUM_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId, lowValue, highValue }); + + let runningSum = 0; + + function rangeSum(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + nodeValue: node.value, + lowValue, + highValue, + runningSum, + }); + + if (node.value >= lowValue && node.value <= highValue) { + runningSum += node.value; + tracker.searchFound(nodeId, { nodeValue: node.value, runningSum }); + } + + if (node.value > lowValue) rangeSum(node.leftChildId); + if (node.value < highValue) rangeSum(node.rightChildId); + } + + rangeSum(rootId); + tracker.complete({ lowValue, highValue, totalSum: runningSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/BSTRecoverSwappedPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-recover-swapped/BSTRecoverSwappedPipeline.stories.tsx new file mode 100644 index 00000000..608913c0 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/BSTRecoverSwappedPipeline.stories.tsx @@ -0,0 +1,100 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstRecoverSwappedSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +/** BST with nodes 3 and 7 swapped — demonstrates the recovery algorithm */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 7, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 3, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstRecoverSwappedSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Recover Swapped Nodes", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/bst-recover-swapped.test.ts b/src/algorithms/trees/bst-operations/bst-recover-swapped/bst-recover-swapped.test.ts new file mode 100644 index 00000000..cf0b2c42 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/bst-recover-swapped.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { bstRecoverSwapped } from "./sources/bst-recover-swapped.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +function collectInorder(root: BSTNode | null): number[] { + if (!root) return []; + return [...collectInorder(root.left), root.value, ...collectInorder(root.right)]; +} + +describe("bstRecoverSwapped", () => { + it("recovers two non-adjacent swapped nodes", () => { + // Swap nodes 3 and 7 in balanced tree to create invalid BST + const invalidTree = node(4, node(2, node(1), node(7)), node(6, node(5), node(3))); + bstRecoverSwapped(invalidTree); + expect(collectInorder(invalidTree)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + it("recovers two adjacent swapped nodes", () => { + // Swap 2 and 3 (adjacent in in-order) + const tree = node(4, node(3, node(1), node(2)), node(6, node(5), node(7))); + bstRecoverSwapped(tree); + expect(collectInorder(tree)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + it("does not modify an already valid BST", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + bstRecoverSwapped(tree); + expect(collectInorder(tree)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/educational.ts b/src/algorithms/trees/bst-operations/bst-recover-swapped/educational.ts new file mode 100644 index 00000000..e8dc610a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/educational.ts @@ -0,0 +1,34 @@ +import type { EducationalContent } from "@/types"; + +export const bstRecoverSwappedEducational: EducationalContent = { + overview: + "**BST Recover Swapped Nodes** fixes a BST where exactly two nodes have been accidentally swapped, violating the BST property.\n\nAn in-order traversal of a valid BST produces strictly ascending values. Two swapped nodes create one or two positions where the sequence dips — the algorithm detects these violations and swaps the node values back.", + + howItWorks: + "Perform a recursive in-order traversal tracking `previousNode`:\n1. At each node, check if `previousNode.value > currentNode.value` (descending — a violation).\n2. The **first violation**: `previousNode` is the first swapped node.\n3. The **second violation** (may be adjacent): `currentNode` is the second swapped node.\n4. After traversal, swap the values of the two detected nodes.\n\n**Two cases:**\n- **Adjacent swap** (e.g., swap nodes 2 and 3 in `[1,2,3,4,5]`): Only one violation found. `firstViolation = 2`, `secondViolation = 3`.\n- **Non-adjacent swap** (e.g., swap 1 and 5): Two violations. `firstViolation` set at first dip, `secondViolation` updated at second dip.", + + timeAndSpaceComplexity: + "**Time: `O(n)`** — full in-order traversal needed.\n\n**Space: `O(h)`** — recursion stack.", + + bestAndWorstCase: + "**Best case:** Swapped nodes are adjacent in in-order — one violation, early detection possible.\n\n**Worst case:** Swapped nodes are at opposite ends (first and last) — full traversal needed.", + + realWorldUses: [ + "**Database tree repair:** Recover a corrupted BST index after a bad write or bit-flip.", + "**Serialization/deserialization bugs:** Detect and fix transposition errors in serialized BSTs.", + ], + + strengthsAndLimitations: { + strengths: [ + "Single-pass O(n) solution — no extra memory for paths or lookups.", + "Handles both adjacent and non-adjacent swap cases correctly.", + ], + limitations: [ + "Assumes exactly two nodes are swapped — cannot recover from arbitrary corruption.", + "Mutates node values in-place.", + ], + }, + + whenToUseIt: + "Use when exactly two BST nodes have been swapped and you need to restore the BST property without rebuilding the entire tree. Morris traversal can reduce space to O(1) for the iterative variant.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/index.ts b/src/algorithms/trees/bst-operations/bst-recover-swapped/index.ts new file mode 100644 index 00000000..1176eaaa --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstRecoverSwapped } from "./sources/bst-recover-swapped.ts?fn"; +import { generateBstRecoverSwappedSteps } from "./step-generator"; +import type { BstRecoverSwappedInput } from "./step-generator"; +import { bstRecoverSwappedEducational } from "./educational"; + +import typescriptSource from "./sources/bst-recover-swapped.ts?raw"; +import pythonSource from "./sources/bst-recover-swapped.py?raw"; +import javaSource from "./sources/BSTRecoverSwapped.java?raw"; + +/** Build the default BST with nodes 3 and 7 swapped (violating BST property) */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 7, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 3, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstRecoverSwapped(input: BstRecoverSwappedInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + if (!root) return null; + bstRecoverSwapped(root); + return root.value; +} + +const bstRecoverSwappedDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_RECOVER_SWAPPED!, + name: "BST Recover Swapped Nodes", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "In-order traversal detects two nodes that violate BST order; swapping their values restores the tree", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBstRecoverSwapped, + generateSteps: generateBstRecoverSwappedSteps, + educational: bstRecoverSwappedEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstRecoverSwappedDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/BSTRecoverSwapped.java b/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/BSTRecoverSwapped.java new file mode 100644 index 00000000..8dd166a5 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/BSTRecoverSwapped.java @@ -0,0 +1,46 @@ +// BST Recover Swapped (Recursive) — in-order detect two swapped nodes and fix + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTRecoverSwapped { + private BSTNode firstViolation = null; // @step:initialize + private BSTNode secondViolation = null; + private BSTNode previousNode = null; + + public void bstRecoverSwapped(BSTNode root) { + firstViolation = null; // @step:initialize + secondViolation = null; + previousNode = null; + inorder(root); + + // Swap the values of the two misplaced nodes to recover the BST + if (firstViolation != null && secondViolation != null) { + int temp = firstViolation.value; + firstViolation.value = secondViolation.value; // @step:complete + secondViolation.value = temp; + } + } + + private void inorder(BSTNode node) { + if (node == null) return; // @step:initialize + + inorder(node.left); // @step:search-node + + // Check if BST property is violated at this position + if (previousNode != null && previousNode.value > node.value) { + if (firstViolation == null) { + // First violation: previous is the first swapped node + firstViolation = previousNode; // @step:found + } + // Second violation: current is always updated to the second swapped node + secondViolation = node; // @step:found + } + + previousNode = node; + inorder(node.right); // @step:search-node + } +} diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/bst-recover-swapped.py b/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/bst-recover-swapped.py new file mode 100644 index 00000000..ac54503c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/bst-recover-swapped.py @@ -0,0 +1,37 @@ +# BST Recover Swapped (Recursive) — in-order detect two swapped nodes and fix + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_recover_swapped(root) -> None: + first_violation = [None] # @step:initialize + second_violation = [None] + previous_node = [None] + + def inorder(node) -> None: + if node is None: # @step:initialize + return + + inorder(node.left) # @step:search-node + + # Check if BST property is violated at this position + if previous_node[0] is not None and previous_node[0].value > node.value: + if first_violation[0] is None: + # First violation: previous is the first swapped node + first_violation[0] = previous_node[0] # @step:found + # Second violation: current is always updated to the second swapped node + second_violation[0] = node # @step:found + + previous_node[0] = node + inorder(node.right) # @step:search-node + + inorder(root) + + # Swap the values of the two misplaced nodes to recover the BST + if first_violation[0] is not None and second_violation[0] is not None: + first_violation[0].value, second_violation[0].value = ( + second_violation[0].value, first_violation[0].value # @step:complete + ) diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/bst-recover-swapped.ts b/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/bst-recover-swapped.ts new file mode 100644 index 00000000..e80d2876 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/sources/bst-recover-swapped.ts @@ -0,0 +1,41 @@ +// BST Recover Swapped (Recursive) — in-order detect two swapped nodes and fix + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstRecoverSwapped(root: BSTNode | null): void { + let firstViolation: BSTNode | null = null; // @step:initialize + let secondViolation: BSTNode | null = null; + let previousNode: BSTNode | null = null; + + function inorder(node: BSTNode | null): void { + if (node === null) return; // @step:initialize + + inorder(node.left); // @step:search-node + + // Check if BST property is violated at this position + if (previousNode !== null && previousNode.value > node.value) { + if (firstViolation === null) { + // First violation: previous is the first swapped node + firstViolation = previousNode; // @step:found + } + // Second violation: current is always updated to the second swapped node + secondViolation = node; // @step:found + } + + previousNode = node; + inorder(node.right); // @step:search-node + } + + inorder(root); + + // Swap the values of the two misplaced nodes to recover the BST + if (firstViolation !== null && secondViolation !== null) { + const temp = firstViolation.value; + firstViolation.value = secondViolation.value; // @step:complete + secondViolation.value = temp; + } +} diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-recover-swapped/step-generator.test.ts new file mode 100644 index 00000000..072a08fb --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstRecoverSwappedSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstRecoverSwappedSteps", () => { + it("produces steps", () => { + const steps = generateBstRecoverSwappedSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstRecoverSwappedSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstRecoverSwappedSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstRecoverSwappedSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstRecoverSwappedSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-recover-swapped/step-generator.ts b/src/algorithms/trees/bst-operations/bst-recover-swapped/step-generator.ts new file mode 100644 index 00000000..057fc647 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-recover-swapped/step-generator.ts @@ -0,0 +1,82 @@ +/** Step generator for BST Recover Swapped — in-order detect and fix two swapped nodes. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_RECOVER_SWAPPED_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_RECOVER_SWAPPED!); + +export interface BstRecoverSwappedInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstRecoverSwappedSteps(input: BstRecoverSwappedInput): ExecutionStep[] { + const { nodes, rootId } = input; + + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker(mutableNodes, rootId, BST_RECOVER_SWAPPED_LINE_MAP); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId }); + + let firstViolationId: string | null = null; + let secondViolationId: string | null = null; + let previousId: string | null = null; + + function inorder(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + inorder(node.leftChildId); + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + nodeValue: node.value, + previousNode: previousId, + }); + + if (previousId !== null) { + const prevNode = nodeMap.get(previousId); + if (prevNode && prevNode.value > node.value) { + if (firstViolationId === null) { + firstViolationId = previousId; + tracker.searchFound(previousId, { + violation: "first", + nodeId: previousId, + value: prevNode.value, + }); + } + secondViolationId = nodeId; + tracker.searchFound(nodeId, { violation: "second", nodeId, value: node.value }); + } + } + + previousId = nodeId; + inorder(node.rightChildId); + } + + inorder(rootId); + + // Swap the two misplaced nodes + if (firstViolationId && secondViolationId) { + const firstNode = nodeMap.get(firstViolationId); + const secondNode = nodeMap.get(secondViolationId); + if (firstNode && secondNode) { + const temp = firstNode.value; + firstNode.value = secondNode.value; + secondNode.value = temp; + tracker.markPath([firstViolationId, secondViolationId], { + swapped: true, + firstNode: firstViolationId, + secondNode: secondViolationId, + }); + } + } + + tracker.complete({ firstViolation: firstViolationId, secondViolation: secondViolationId }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/BSTSearchIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-search-iterative/BSTSearchIterativePipeline.stories.tsx new file mode 100644 index 00000000..4a73cddd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/BSTSearchIterativePipeline.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstSearchIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstSearchIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 5, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Search (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/bst-search-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-search-iterative/bst-search-iterative.test.ts new file mode 100644 index 00000000..8477a93a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/bst-search-iterative.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { bstSearchIterative } from "./sources/bst-search-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +const balancedTree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstSearchIterative", () => { + it("finds a value in the tree", () => { + const result = bstSearchIterative(balancedTree, 6) as BSTNode | null; + expect(result?.value).toBe(6); + }); + + it("returns null for missing value", () => { + expect(bstSearchIterative(balancedTree, 10)).toBeNull(); + }); + + it("finds the root", () => { + const result = bstSearchIterative(balancedTree, 4) as BSTNode | null; + expect(result?.value).toBe(4); + }); + + it("returns null on empty tree", () => { + expect(bstSearchIterative(null, 5)).toBeNull(); + }); + + it("finds a left leaf", () => { + const result = bstSearchIterative(balancedTree, 1) as BSTNode | null; + expect(result?.value).toBe(1); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-search-iterative/educational.ts new file mode 100644 index 00000000..508e2022 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/educational.ts @@ -0,0 +1,41 @@ +import type { EducationalContent } from "@/types"; + +export const bstSearchIterativeEducational: EducationalContent = { + overview: + "**BST Search (Iterative)** performs the same binary search as the recursive variant but uses a `while` loop instead of recursion, eliminating the call stack overhead entirely.\n\nA `current` pointer starts at the root and walks the tree until it finds the target or reaches a `null` leaf.", + + howItWorks: + "1. Set `current = root`.\n" + + "2. While `current !== null`:\n" + + " - If `current.value === target` — return `current`.\n" + + " - If `target < current.value` — move left: `current = current.left`.\n" + + " - Otherwise — move right: `current = current.right`.\n" + + "3. If the loop exits without a match, return `null`.", + + timeAndSpaceComplexity: + "**Time: `O(log n)` average, `O(n)` worst case** — same as the recursive version.\n\n**Space: `O(1)`** — no call stack; only a single pointer variable is maintained.", + + bestAndWorstCase: + "**Best case:** Target equals the root — one comparison, O(1).\n\n**Worst case:** Degenerate tree — traversal visits every node, O(n).", + + realWorldUses: [ + "**Embedded systems:** Where stack space is limited and recursion is avoided.", + "**High-performance lookups:** Loop-based code benefits from better CPU branch prediction than recursive calls.", + "**Iterative BST implementations:** Used as a building block for iterative insert and delete.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) space — no recursive call stack.", + "Faster in practice due to eliminated function-call overhead.", + "Easier to reason about stack safety for very tall trees.", + ], + limitations: [ + "Slightly more verbose code than the elegant recursive version.", + "Still degrades to O(n) on unbalanced trees.", + ], + }, + + whenToUseIt: + "Prefer the iterative variant when memory is constrained or when stack overflow is a concern on potentially tall trees.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-search-iterative/index.ts new file mode 100644 index 00000000..5db48c8a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstSearchIterative } from "./sources/bst-search-iterative.ts?fn"; +import { generateBstSearchIterativeSteps } from "./step-generator"; +import type { BstSearchIterativeInput } from "./step-generator"; +import { bstSearchIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-search-iterative.ts?raw"; +import pythonSource from "./sources/bst-search-iterative.py?raw"; +import javaSource from "./sources/BSTSearchIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstSearchIterative(input: BstSearchIterativeInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstSearchIterative(root, input.targetValue) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstSearchIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_SEARCH_ITERATIVE!, + name: "BST Search (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: "Iterative binary search using a while loop: compare and move left or right", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetValue: 5 }, + }, + execute: executeBstSearchIterative, + generateSteps: generateBstSearchIterativeSteps, + educational: bstSearchIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstSearchIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/sources/BSTSearchIterative.java b/src/algorithms/trees/bst-operations/bst-search-iterative/sources/BSTSearchIterative.java new file mode 100644 index 00000000..f006bfb2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/sources/BSTSearchIterative.java @@ -0,0 +1,27 @@ +// BST Search (Iterative) — while loop binary search, no recursion + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTSearchIterative { + public BSTNode bstSearchIterative(BSTNode root, int target) { + BSTNode current = root; // @step:initialize + + while (current != null) { + if (current.value == target) return current; // @step:found + + if (target < current.value) { + // Target is smaller — move left + current = current.left; // @step:search-node + } else { + // Target is larger — move right + current = current.right; // @step:search-node + } + } + + return null; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/sources/bst-search-iterative.py b/src/algorithms/trees/bst-operations/bst-search-iterative/sources/bst-search-iterative.py new file mode 100644 index 00000000..ebd13349 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/sources/bst-search-iterative.py @@ -0,0 +1,23 @@ +# BST Search (Iterative) — while loop binary search, no recursion + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_search_iterative(root, target: int): + current = root # @step:initialize + + while current is not None: + if current.value == target: # @step:found + return current + + if target < current.value: + # Target is smaller — move left + current = current.left # @step:search-node + else: + # Target is larger — move right + current = current.right # @step:search-node + + return None # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/sources/bst-search-iterative.ts b/src/algorithms/trees/bst-operations/bst-search-iterative/sources/bst-search-iterative.ts new file mode 100644 index 00000000..62b9d8f3 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/sources/bst-search-iterative.ts @@ -0,0 +1,25 @@ +// BST Search (Iterative) — while loop binary search, no recursion + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstSearchIterative(root: BSTNode | null, target: number): BSTNode | null { + let current = root; // @step:initialize + + while (current !== null) { + if (current.value === target) return current; // @step:found + + if (target < current.value) { + // Target is smaller — move left + current = current.left; // @step:search-node + } else { + // Target is larger — move right + current = current.right; // @step:search-node + } + } + + return null; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-search-iterative/step-generator.test.ts new file mode 100644 index 00000000..aa03e7fe --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/step-generator.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstSearchIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstSearchIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstSearchIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 6, + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with initialize", () => { + const steps = generateBstSearchIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 6, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with complete", () => { + const steps = generateBstSearchIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 6, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstSearchIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 6, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has a found step when value exists", () => { + const steps = generateBstSearchIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 6, + }); + expect(steps.some((step) => step.type === "found")).toBe(true); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-search-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-search-iterative/step-generator.ts new file mode 100644 index 00000000..87e36b30 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search-iterative/step-generator.ts @@ -0,0 +1,46 @@ +/** Step generator for BST Search (Iterative) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_SEARCH_ITERATIVE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_SEARCH_ITERATIVE!); + +export interface BstSearchIterativeInput { + nodes: TreeNode[]; + rootId: string; + targetValue: number; +} + +export function generateBstSearchIterativeSteps(input: BstSearchIterativeInput): ExecutionStep[] { + const { nodes, rootId, targetValue } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_SEARCH_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(targetValue, { rootId, targetValue }); + + let currentId: string | null = rootId; + + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + + tracker.compareNode(currentId, node.value, { + currentNode: currentId, + targetValue, + nodeValue: node.value, + }); + + if (node.value === targetValue) { + tracker.searchFound(currentId, { foundAt: currentId, value: targetValue }); + break; + } + + currentId = targetValue < node.value ? node.leftChildId : node.rightChildId; + } + + tracker.complete({ found: currentId !== null, targetValue }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-search/BSTSearchPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-search/BSTSearchPipeline.stories.tsx new file mode 100644 index 00000000..4477ad3c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/BSTSearchPipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstSearchSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 5 }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Search (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-search/bst-search.test.ts b/src/algorithms/trees/bst-operations/bst-search/bst-search.test.ts new file mode 100644 index 00000000..4c206b4a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/bst-search.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { bstSearch } from "./sources/bst-search.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +const balancedTree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + +describe("bstSearch", () => { + it("finds a value that exists in the tree", () => { + const result = bstSearch(balancedTree, 5) as BSTNode | null; + expect(result?.value).toBe(5); + }); + + it("returns null for a value that does not exist", () => { + expect(bstSearch(balancedTree, 9)).toBeNull(); + }); + + it("finds the root value", () => { + const result = bstSearch(balancedTree, 4) as BSTNode | null; + expect(result?.value).toBe(4); + }); + + it("finds a leaf node", () => { + const result = bstSearch(balancedTree, 1) as BSTNode | null; + expect(result?.value).toBe(1); + }); + + it("returns null on an empty tree", () => { + expect(bstSearch(null, 5)).toBeNull(); + }); + + it("finds a value in a single-node tree", () => { + const result = bstSearch(node(42), 42) as BSTNode | null; + expect(result?.value).toBe(42); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-search/educational.ts b/src/algorithms/trees/bst-operations/bst-search/educational.ts new file mode 100644 index 00000000..af07e1e7 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/educational.ts @@ -0,0 +1,40 @@ +import type { EducationalContent } from "@/types"; + +export const bstSearchEducational: EducationalContent = { + overview: + "**BST Search (Recursive)** exploits the BST property — all values in the left subtree are smaller and all in the right subtree are larger — to eliminate half of the remaining candidates at every step.\n\nAt each node, the algorithm compares the target to the current node's value and recursively searches only the relevant subtree.", + + howItWorks: + "1. **Base case:** If the current node is `null`, the target does not exist — return `null`.\n" + + "2. **Match:** If the current node's value equals the target — return the node.\n" + + "3. **Go left:** If target is smaller than the current value — recurse into the left subtree.\n" + + "4. **Go right:** If target is larger — recurse into the right subtree.\n\n" + + "Each comparison halves the search space in a balanced BST, mirroring binary search on a sorted array.", + + timeAndSpaceComplexity: + "**Time: `O(log n)` average, `O(n)` worst case**\n\nFor a balanced BST the height `h = log n`, so the path to any node spans at most `log n` comparisons. A degenerate (linear) BST degrades to `O(n)`.\n\n**Space: `O(h)` call stack**\n\nOne stack frame per level of recursion.", + + bestAndWorstCase: + "**Best case:** Target is the root — returns immediately in `O(1)`.\n\n**Worst case:** Degenerate BST (all nodes inserted in sorted order forming a linked list) — every node must be visited, giving `O(n)`.", + + realWorldUses: [ + "**Database index lookups:** B-tree search follows this same compare-and-branch pattern.", + "**Symbol tables:** Compilers store identifiers in BSTs for efficient lookup during parsing.", + "**Auto-completion:** Dictionary prefix lookups descend a BST (or trie) by character comparison.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple recursive structure that mirrors the mathematical definition of a BST.", + "O(log n) on balanced trees — efficient for large datasets.", + "No auxiliary data structures needed.", + ], + limitations: [ + "Degrades to O(n) on unbalanced trees — an AVL tree or red-black tree maintains balance automatically.", + "Recursive call stack consumes O(h) space — the iterative variant uses O(1) space.", + ], + }, + + whenToUseIt: + "Use BST Search when you need dynamic ordered data with fast lookup. Prefer the iterative version when stack depth could be an issue on very tall trees.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-search/index.ts b/src/algorithms/trees/bst-operations/bst-search/index.ts new file mode 100644 index 00000000..c7468b47 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/index.ts @@ -0,0 +1,118 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstSearch } from "./sources/bst-search.ts?fn"; +import { generateBstSearchSteps } from "./step-generator"; +import type { BstSearchInput } from "./step-generator"; +import { bstSearchEducational } from "./educational"; + +import typescriptSource from "./sources/bst-search.ts?raw"; +import pythonSource from "./sources/bst-search.py?raw"; +import javaSource from "./sources/BSTSearch.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstSearch(input: BstSearchInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstSearch(root, input.targetValue) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstSearchDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_SEARCH!, + name: "BST Search (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Recursive binary search: compare target to current node and recurse left or right", + timeComplexity: { best: "O(log n)", average: "O(log n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetValue: 5 }, + }, + execute: executeBstSearch, + generateSteps: generateBstSearchSteps, + educational: bstSearchEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstSearchDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-search/sources/BSTSearch.java b/src/algorithms/trees/bst-operations/bst-search/sources/BSTSearch.java new file mode 100644 index 00000000..e2215d0b --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/sources/BSTSearch.java @@ -0,0 +1,22 @@ +// BST Search (Recursive) — compare target, recurse left or right + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTSearch { + public BSTNode bstSearch(BSTNode root, int target) { + if (root == null) return null; // @step:initialize + if (root.value == target) return root; // @step:found + + if (target < root.value) { + // Target is smaller — search the left subtree + return bstSearch(root.left, target); // @step:search-node + } else { + // Target is larger — search the right subtree + return bstSearch(root.right, target); // @step:search-node + } + } +} diff --git a/src/algorithms/trees/bst-operations/bst-search/sources/bst-search.py b/src/algorithms/trees/bst-operations/bst-search/sources/bst-search.py new file mode 100644 index 00000000..b182b60a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/sources/bst-search.py @@ -0,0 +1,20 @@ +# BST Search (Recursive) — compare target, recurse left or right + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_search(root, target: int): + if root is None: # @step:initialize + return None + if root.value == target: # @step:found + return root + + if target < root.value: + # Target is smaller — search the left subtree + return bst_search(root.left, target) # @step:search-node + else: + # Target is larger — search the right subtree + return bst_search(root.right, target) # @step:search-node diff --git a/src/algorithms/trees/bst-operations/bst-search/sources/bst-search.ts b/src/algorithms/trees/bst-operations/bst-search/sources/bst-search.ts new file mode 100644 index 00000000..5ec5ec57 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/sources/bst-search.ts @@ -0,0 +1,20 @@ +// BST Search (Recursive) — compare target, recurse left or right + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstSearch(root: BSTNode | null, target: number): BSTNode | null { + if (root === null) return null; // @step:initialize + if (root.value === target) return root; // @step:found + + if (target < root.value) { + // Target is smaller — search the left subtree + return bstSearch(root.left, target); // @step:search-node + } else { + // Target is larger — search the right subtree + return bstSearch(root.right, target); // @step:search-node + } +} diff --git a/src/algorithms/trees/bst-operations/bst-search/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-search/step-generator.test.ts new file mode 100644 index 00000000..1f6b94cd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstSearchSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstSearchSteps", () => { + it("produces steps for a found target", () => { + const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 5 }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 5 }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 5 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 5 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("includes a found step when target exists", () => { + const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 3 }); + const foundStep = steps.find((step) => step.type === "found"); + expect(foundStep).toBeDefined(); + }); + + it("has incrementing step indices", () => { + const steps = generateBstSearchSteps({ nodes: defaultNodes, rootId: "n4", targetValue: 5 }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-search/step-generator.ts b/src/algorithms/trees/bst-operations/bst-search/step-generator.ts new file mode 100644 index 00000000..d2fc81f4 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-search/step-generator.ts @@ -0,0 +1,50 @@ +/** Step generator for BST Search (Recursive) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_SEARCH_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_SEARCH!); + +export interface BstSearchInput { + nodes: TreeNode[]; + rootId: string; + targetValue: number; +} + +export function generateBstSearchSteps(input: BstSearchInput): ExecutionStep[] { + const { nodes, rootId, targetValue } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_SEARCH_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(targetValue, { rootId, targetValue }); + + function search(nodeId: string | null): boolean { + if (!nodeId) return false; + const node = nodeMap.get(nodeId); + if (!node) return false; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + targetValue, + nodeValue: node.value, + }); + + if (node.value === targetValue) { + tracker.searchFound(nodeId, { foundAt: nodeId, value: targetValue }); + return true; + } + + if (targetValue < node.value) { + return search(node.leftChildId); + } else { + return search(node.rightChildId); + } + } + + const found = search(rootId); + tracker.complete({ found, targetValue }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/BSTToGreaterTreeIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/BSTToGreaterTreeIterativePipeline.stories.tsx new file mode 100644 index 00000000..ed10b286 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/BSTToGreaterTreeIterativePipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstToGreaterTreeIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstToGreaterTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST to Greater Tree (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/bst-to-greater-tree-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/bst-to-greater-tree-iterative.test.ts new file mode 100644 index 00000000..e8e4872d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/bst-to-greater-tree-iterative.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { bstToGreaterTreeIterative } from "./sources/bst-to-greater-tree-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstToGreaterTreeIterative", () => { + it("transforms a 3-node BST correctly", () => { + const tree = node(2, node(1), node(3)); + const result = bstToGreaterTreeIterative(tree) as BSTNode | null; + expect(result?.value).toBe(5); + expect(result?.right?.value).toBe(3); + expect(result?.left?.value).toBe(6); + }); + it("handles single node", () => { + const singleNode = node(7); + const result = bstToGreaterTreeIterative(singleNode) as BSTNode | null; + expect(result?.value).toBe(7); + }); + it("handles null tree", () => { + expect(bstToGreaterTreeIterative(null)).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/educational.ts new file mode 100644 index 00000000..c3252065 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/educational.ts @@ -0,0 +1,25 @@ +import type { EducationalContent } from "@/types"; + +export const bstToGreaterTreeIterativeEducational: EducationalContent = { + overview: + "**BST to Greater Tree (Iterative)** performs the same reverse in-order accumulation as the recursive version but uses an explicit stack to traverse right subtrees before visiting root nodes.", + + howItWorks: + "Maintains a stack for reverse in-order (right → root → left):\n1. Push all right-spine nodes first.\n2. Pop a node, accumulate its value into `runningSum`, update the node.\n3. Push the left child's right spine.\n4. Repeat until stack is empty.", + + timeAndSpaceComplexity: "**Time: `O(n)`**\n\n**Space: `O(h)`** — explicit stack.", + + bestAndWorstCase: "Always O(n) — all nodes updated.", + + realWorldUses: [ + "**Iterative greater-tree transformation:** Used in environments where recursion depth is bounded.", + ], + + strengthsAndLimitations: { + strengths: ["No recursion — safe for any tree depth."], + limitations: ["More code than the recursive version for the same result."], + }, + + whenToUseIt: + "Choose the iterative variant when the BST may be very deep or when implementing in a language without tail-call optimization.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/index.ts new file mode 100644 index 00000000..40e4080c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/index.ts @@ -0,0 +1,118 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstToGreaterTreeIterative } from "./sources/bst-to-greater-tree-iterative.ts?fn"; +import { generateBstToGreaterTreeIterativeSteps } from "./step-generator"; +import type { BstToGreaterTreeIterativeInput } from "./step-generator"; +import { bstToGreaterTreeIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-to-greater-tree-iterative.ts?raw"; +import pythonSource from "./sources/bst-to-greater-tree-iterative.py?raw"; +import javaSource from "./sources/BSTToGreaterTreeIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstToGreaterTreeIterative(input: BstToGreaterTreeIterativeInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstToGreaterTreeIterative(root) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstToGreaterTreeIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_TO_GREATER_TREE_ITERATIVE!, + name: "BST to Greater Tree (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Stack-based reverse in-order traversal accumulates running sum to build the greater tree", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBstToGreaterTreeIterative, + generateSteps: generateBstToGreaterTreeIterativeSteps, + educational: bstToGreaterTreeIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstToGreaterTreeIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/BSTToGreaterTreeIterative.java b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/BSTToGreaterTreeIterative.java new file mode 100644 index 00000000..4309edd9 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/BSTToGreaterTreeIterative.java @@ -0,0 +1,36 @@ +// BST to Greater Tree (Iterative) — stack-based reverse in-order accumulation +import java.util.Stack; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTToGreaterTreeIterative { + public BSTNode bstToGreaterTreeIterative(BSTNode root) { + Stack stack = new Stack<>(); // @step:initialize + int runningSum = 0; + BSTNode current = root; + + while (current != null || !stack.isEmpty()) { + // Push all right nodes first (reverse in-order visits right subtree first) + while (current != null) { + stack.push(current); // @step:search-node + current = current.right; + } + + // Process the top node + current = stack.pop(); + + // Accumulate sum and update node value + runningSum += current.value; // @step:found + current.value = runningSum; + + // Move to left subtree + current = current.left; // @step:search-node + } + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/bst-to-greater-tree-iterative.py b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/bst-to-greater-tree-iterative.py new file mode 100644 index 00000000..5bc28496 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/bst-to-greater-tree-iterative.py @@ -0,0 +1,30 @@ +# BST to Greater Tree (Iterative) — stack-based reverse in-order accumulation + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_to_greater_tree_iterative(root): + stack = [] # @step:initialize + running_sum = 0 + current = root + + while current is not None or len(stack) > 0: + # Push all right nodes first (reverse in-order visits right subtree first) + while current is not None: + stack.append(current) # @step:search-node + current = current.right + + # Process the top node + current = stack.pop() + + # Accumulate sum and update node value + running_sum += current.value # @step:found + current.value = running_sum + + # Move to left subtree + current = current.left # @step:search-node + + return root # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/bst-to-greater-tree-iterative.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/bst-to-greater-tree-iterative.ts new file mode 100644 index 00000000..c2da1741 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/sources/bst-to-greater-tree-iterative.ts @@ -0,0 +1,33 @@ +// BST to Greater Tree (Iterative) — stack-based reverse in-order accumulation + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstToGreaterTreeIterative(root: BSTNode | null): BSTNode | null { + const stack: BSTNode[] = []; // @step:initialize + let runningSum = 0; + let current = root; + + while (current !== null || stack.length > 0) { + // Push all right nodes first (reverse in-order visits right subtree first) + while (current !== null) { + stack.push(current); // @step:search-node + current = current.right; + } + + // Process the top node + current = stack.pop()!; + + // Accumulate sum and update node value + runningSum += current.value; // @step:found + current.value = runningSum; + + // Move to left subtree + current = current.left; // @step:search-node + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/step-generator.test.ts new file mode 100644 index 00000000..59a08855 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstToGreaterTreeIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstToGreaterTreeIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstToGreaterTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstToGreaterTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstToGreaterTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstToGreaterTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstToGreaterTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/step-generator.ts new file mode 100644 index 00000000..be19645e --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree-iterative/step-generator.ts @@ -0,0 +1,62 @@ +/** Step generator for BST to Greater Tree (Iterative) — stack-based reverse in-order. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_TO_GREATER_TREE_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.BST_TO_GREATER_TREE_ITERATIVE!, +); + +export interface BstToGreaterTreeIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstToGreaterTreeIterativeSteps( + input: BstToGreaterTreeIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker( + mutableNodes, + rootId, + BST_TO_GREATER_TREE_ITERATIVE_LINE_MAP, + ); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId }); + + const stack: string[] = []; + let runningSum = 0; + let currentId: string | null = rootId; + + while (currentId !== null || stack.length > 0) { + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + stack.push(currentId); + tracker.compareNode(currentId, node.value, { currentNode: currentId, pushing: true }); + currentId = node.rightChildId; + } + + const poppedId = stack.pop(); + if (!poppedId) break; + const node = nodeMap.get(poppedId); + if (!node) break; + + const originalValue = node.value; + runningSum += node.value; + node.value = runningSum; + + tracker.searchFound(poppedId, { originalValue, runningSum, newValue: node.value }); + + currentId = node.leftChildId; + } + + tracker.complete({ runningSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/BSTToGreaterTreePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-to-greater-tree/BSTToGreaterTreePipeline.stories.tsx new file mode 100644 index 00000000..3fdc2482 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/BSTToGreaterTreePipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstToGreaterTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstToGreaterTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST to Greater Tree (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/bst-to-greater-tree.test.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree/bst-to-greater-tree.test.ts new file mode 100644 index 00000000..a52e642d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/bst-to-greater-tree.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { bstToGreaterTree } from "./sources/bst-to-greater-tree.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstToGreaterTree", () => { + it("transforms a 3-node BST correctly", () => { + const tree = node(2, node(1), node(3)); + const result = bstToGreaterTree(tree) as BSTNode | null; + // Node 3 → 3, node 2 → 5, node 1 → 6 + expect(result?.value).toBe(5); + expect(result?.right?.value).toBe(3); + expect(result?.left?.value).toBe(6); + }); + it("handles single node", () => { + const singleNode = node(5); + const result = bstToGreaterTree(singleNode) as BSTNode | null; + expect(result?.value).toBe(5); + }); + it("handles null tree", () => { + expect(bstToGreaterTree(null)).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/educational.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree/educational.ts new file mode 100644 index 00000000..9b77c8a1 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/educational.ts @@ -0,0 +1,30 @@ +import type { EducationalContent } from "@/types"; + +export const bstToGreaterTreeEducational: EducationalContent = { + overview: + "**BST to Greater Tree (Recursive)** transforms each node's value so it equals the sum of all values in the original BST that are **greater than or equal to** the node's value.\n\nThis is achieved by performing a **reverse in-order traversal** (right → root → left) and maintaining a running cumulative sum.", + + howItWorks: + "Reverse in-order visits nodes from largest to smallest:\n1. Recurse into the right subtree (larger values first).\n2. Add the current node's value to the running sum.\n3. Replace the current node's value with the running sum.\n4. Recurse into the left subtree.\n\nFor the default BST `[1,2,3,4,5,6,7]`, node 7 → 7, node 6 → 13, node 5 → 18, node 4 → 22, etc.", + + timeAndSpaceComplexity: + "**Time: `O(n)`** — every node is visited exactly once.\n\n**Space: `O(h)`** — recursion depth.", + + bestAndWorstCase: "Always O(n) — all nodes must be updated.", + + realWorldUses: [ + "**Cumulative score boards:** Transform individual scores into cumulative totals in a sorted BST.", + "**Suffix sums in sorted data:** Greater-sum transformation corresponds to suffix sums on sorted arrays.", + ], + + strengthsAndLimitations: { + strengths: [ + "Elegant reverse in-order traversal pattern.", + "Single pass transforms all values correctly.", + ], + limitations: ["Mutates the tree in-place — original values are lost unless copied beforehand."], + }, + + whenToUseIt: + "Use when you need to enrich a BST with cumulative rank information, or to answer 'what is the total of all values ≥ mine?' for every node.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/index.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree/index.ts new file mode 100644 index 00000000..ab00eed9 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/index.ts @@ -0,0 +1,118 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstToGreaterTree } from "./sources/bst-to-greater-tree.ts?fn"; +import { generateBstToGreaterTreeSteps } from "./step-generator"; +import type { BstToGreaterTreeInput } from "./step-generator"; +import { bstToGreaterTreeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-to-greater-tree.ts?raw"; +import pythonSource from "./sources/bst-to-greater-tree.py?raw"; +import javaSource from "./sources/BSTToGreaterTree.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstToGreaterTree(input: BstToGreaterTreeInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + const result = bstToGreaterTree(root) as BSTNodeShape | null; + return result ? result.value : null; +} + +const bstToGreaterTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_TO_GREATER_TREE!, + name: "BST to Greater Tree (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Reverse in-order traversal accumulates a running sum, replacing each node's value", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBstToGreaterTree, + generateSteps: generateBstToGreaterTreeSteps, + educational: bstToGreaterTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstToGreaterTreeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/BSTToGreaterTree.java b/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/BSTToGreaterTree.java new file mode 100644 index 00000000..c016da4f --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/BSTToGreaterTree.java @@ -0,0 +1,31 @@ +// BST to Greater Tree (Recursive) — reverse in-order: accumulate running sum + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTToGreaterTree { + private int runningSum = 0; // @step:initialize + + public BSTNode bstToGreaterTree(BSTNode root) { + runningSum = 0; // @step:initialize + reverseInorder(root); + return root; // @step:complete + } + + private void reverseInorder(BSTNode node) { + if (node == null) return; // @step:initialize + + // Visit right subtree first (larger values in descending order) + reverseInorder(node.right); // @step:search-node + + // Add current node's value to running sum, then update node + runningSum += node.value; // @step:found + node.value = runningSum; + + // Visit left subtree (smaller values) + reverseInorder(node.left); // @step:search-node + } +} diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/bst-to-greater-tree.py b/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/bst-to-greater-tree.py new file mode 100644 index 00000000..590eb72d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/bst-to-greater-tree.py @@ -0,0 +1,27 @@ +# BST to Greater Tree (Recursive) — reverse in-order: accumulate running sum + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_to_greater_tree(root): + running_sum = [0] # @step:initialize + + def reverse_inorder(node) -> None: + if node is None: # @step:initialize + return + + # Visit right subtree first (larger values in descending order) + reverse_inorder(node.right) # @step:search-node + + # Add current node's value to running sum, then update node + running_sum[0] += node.value # @step:found + node.value = running_sum[0] + + # Visit left subtree (smaller values) + reverse_inorder(node.left) # @step:search-node + + reverse_inorder(root) + return root # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/bst-to-greater-tree.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/bst-to-greater-tree.ts new file mode 100644 index 00000000..4994f300 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/sources/bst-to-greater-tree.ts @@ -0,0 +1,28 @@ +// BST to Greater Tree (Recursive) — reverse in-order: accumulate running sum + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstToGreaterTree(root: BSTNode | null): BSTNode | null { + let runningSum = 0; // @step:initialize + + function reverseInorder(node: BSTNode | null): void { + if (node === null) return; // @step:initialize + + // Visit right subtree first (larger values in descending order) + reverseInorder(node.right); // @step:search-node + + // Add current node's value to running sum, then update node + runningSum += node.value; // @step:found + node.value = runningSum; + + // Visit left subtree (smaller values) + reverseInorder(node.left); // @step:search-node + } + + reverseInorder(root); + return root; // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree/step-generator.test.ts new file mode 100644 index 00000000..1e5e34f1 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstToGreaterTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstToGreaterTreeSteps", () => { + it("produces steps", () => { + const steps = generateBstToGreaterTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstToGreaterTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstToGreaterTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstToGreaterTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstToGreaterTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-to-greater-tree/step-generator.ts b/src/algorithms/trees/bst-operations/bst-to-greater-tree/step-generator.ts new file mode 100644 index 00000000..8bc6e89c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-to-greater-tree/step-generator.ts @@ -0,0 +1,50 @@ +/** Step generator for BST to Greater Tree (Recursive) — reverse in-order sum accumulation. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_TO_GREATER_TREE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_TO_GREATER_TREE!); + +export interface BstToGreaterTreeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstToGreaterTreeSteps(input: BstToGreaterTreeInput): ExecutionStep[] { + const { nodes, rootId } = input; + + const mutableNodes = nodes.map((node) => ({ ...node })); + const tracker = new BSTOperationTracker(mutableNodes, rootId, BST_TO_GREATER_TREE_LINE_MAP); + const nodeMap = new Map(mutableNodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId }); + + let runningSum = 0; + + function reverseInorder(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + reverseInorder(node.rightChildId); + + const originalValue = node.value; + runningSum += node.value; + node.value = runningSum; + + tracker.searchFound(nodeId, { + originalValue, + runningSum, + newValue: node.value, + }); + + reverseInorder(node.leftChildId); + } + + reverseInorder(rootId); + tracker.complete({ runningSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/BSTValidationIterativePipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-validation-iterative/BSTValidationIterativePipeline.stories.tsx new file mode 100644 index 00000000..68a7157d --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/BSTValidationIterativePipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstValidationIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstValidationIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Validation (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/bst-validation-iterative.test.ts b/src/algorithms/trees/bst-operations/bst-validation-iterative/bst-validation-iterative.test.ts new file mode 100644 index 00000000..9e97947a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/bst-validation-iterative.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { bstValidationIterative } from "./sources/bst-validation-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstValidationIterative", () => { + it("validates a correct BST", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstValidationIterative(tree)).toBe(true); + }); + it("rejects an invalid BST", () => { + const invalid = node(5, node(6), node(7)); + expect(bstValidationIterative(invalid)).toBe(false); + }); + it("accepts null", () => { + expect(bstValidationIterative(null)).toBe(true); + }); + it("accepts single node", () => { + expect(bstValidationIterative(node(10))).toBe(true); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/educational.ts b/src/algorithms/trees/bst-operations/bst-validation-iterative/educational.ts new file mode 100644 index 00000000..a87eee65 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/educational.ts @@ -0,0 +1,34 @@ +import type { EducationalContent } from "@/types"; + +export const bstValidationIterativeEducational: EducationalContent = { + overview: + "**BST Validation (Iterative)** validates the BST property by performing a stack-based in-order traversal and checking that each value is strictly greater than the previous one.\n\nA valid BST always produces a strictly ascending sequence during in-order traversal — any deviation is a violation.", + + howItWorks: + "Uses an explicit stack to simulate in-order traversal:\n1. Push all left nodes.\n2. Pop a node, compare its value to the last seen value (`previousValue`).\n3. If `current.value ≤ previousValue` — invalid BST, return `false`.\n4. Update `previousValue`, then move to the right child.\n5. If traversal completes without violations — return `true`.", + + timeAndSpaceComplexity: + "**Time: `O(n)`** — every node processed once.\n\n**Space: `O(h)`** — explicit stack holds at most `h` nodes.", + + bestAndWorstCase: + "**Best case:** Violation found at the first comparison — O(1).\n\n**Worst case:** Valid BST — all `n` nodes processed.", + + realWorldUses: [ + "**Iterative BST correctness testing:** Avoids recursion for stack-constrained environments.", + "**Sorted-order verification:** Confirms that a sequence of BST in-order values is ascending.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursive call stack — bounded by O(h) explicit stack.", + "Intuitive: a valid BST must produce sorted output during in-order traversal.", + ], + limitations: [ + "Still O(h) space for the explicit stack — not O(1).", + "Less directly communicates the BST invariant compared to the min/max bounds approach.", + ], + }, + + whenToUseIt: + "Prefer this approach when you also want to collect the sorted values during validation, or when recursion is not acceptable.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/index.ts b/src/algorithms/trees/bst-operations/bst-validation-iterative/index.ts new file mode 100644 index 00000000..5bb9d926 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstValidationIterative } from "./sources/bst-validation-iterative.ts?fn"; +import { generateBstValidationIterativeSteps } from "./step-generator"; +import type { BstValidationIterativeInput } from "./step-generator"; +import { bstValidationIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-validation-iterative.ts?raw"; +import pythonSource from "./sources/bst-validation-iterative.py?raw"; +import javaSource from "./sources/BSTValidationIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstValidationIterative(input: BstValidationIterativeInput): boolean { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstValidationIterative(root) as boolean; +} + +const bstValidationIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_VALIDATION_ITERATIVE!, + name: "BST Validation (Iterative)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Stack-based in-order traversal BST validation: check that each value is strictly ascending", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBstValidationIterative, + generateSteps: generateBstValidationIterativeSteps, + educational: bstValidationIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstValidationIterativeDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/BSTValidationIterative.java b/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/BSTValidationIterative.java new file mode 100644 index 00000000..0d6c574a --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/BSTValidationIterative.java @@ -0,0 +1,37 @@ +// BST Validation (Iterative) — stack-based in-order traversal checking ascending order +import java.util.Stack; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTValidationIterative { + public boolean bstValidationIterative(BSTNode root) { + Stack stack = new Stack<>(); // @step:initialize + long previousValue = Long.MIN_VALUE; + BSTNode current = root; + + while (current != null || !stack.isEmpty()) { + // Push all left nodes onto the stack + while (current != null) { + stack.push(current); // @step:search-node + current = current.left; + } + + // Process the top of the stack + current = stack.pop(); + + // In-order value must be strictly greater than the previous one + if (current.value <= previousValue) { + return false; // @step:found — BST violation detected + } + + previousValue = current.value; // @step:search-node + current = current.right; + } + + return true; // @step:complete — all values in ascending order + } +} diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/bst-validation-iterative.py b/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/bst-validation-iterative.py new file mode 100644 index 00000000..f27130f9 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/bst-validation-iterative.py @@ -0,0 +1,30 @@ +# BST Validation (Iterative) — stack-based in-order traversal checking ascending order + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_validation_iterative(root) -> bool: + stack = [] # @step:initialize + previous_value = float("-inf") + current = root + + while current is not None or len(stack) > 0: + # Push all left nodes onto the stack + while current is not None: + stack.append(current) # @step:search-node + current = current.left + + # Process the top of the stack + current = stack.pop() + + # In-order value must be strictly greater than the previous one + if current.value <= previous_value: + return False # @step:found — BST violation detected + + previous_value = current.value # @step:search-node + current = current.right + + return True # @step:complete — all values in ascending order diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/bst-validation-iterative.ts b/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/bst-validation-iterative.ts new file mode 100644 index 00000000..42a481b7 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/sources/bst-validation-iterative.ts @@ -0,0 +1,34 @@ +// BST Validation (Iterative) — stack-based in-order traversal checking ascending order + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstValidationIterative(root: BSTNode | null): boolean { + const stack: BSTNode[] = []; // @step:initialize + let previousValue = -Infinity; + let current = root; + + while (current !== null || stack.length > 0) { + // Push all left nodes onto the stack + while (current !== null) { + stack.push(current); // @step:search-node + current = current.left; + } + + // Process the top of the stack + current = stack.pop()!; + + // In-order value must be strictly greater than the previous one + if (current.value <= previousValue) { + return false; // @step:found — BST violation detected + } + + previousValue = current.value; // @step:search-node + current = current.right; + } + + return true; // @step:complete — all values in ascending order +} diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-validation-iterative/step-generator.test.ts new file mode 100644 index 00000000..91808311 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstValidationIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstValidationIterativeSteps", () => { + it("produces steps", () => { + const steps = generateBstValidationIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstValidationIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstValidationIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstValidationIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstValidationIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-validation-iterative/step-generator.ts b/src/algorithms/trees/bst-operations/bst-validation-iterative/step-generator.ts new file mode 100644 index 00000000..16cdafd7 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation-iterative/step-generator.ts @@ -0,0 +1,63 @@ +/** Step generator for BST Validation (Iterative) — stack-based in-order ascending check. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_VALIDATION_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.BST_VALIDATION_ITERATIVE!, +); + +export interface BstValidationIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstValidationIterativeSteps( + input: BstValidationIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_VALIDATION_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId }); + + const stack: string[] = []; + let previousValue = -Infinity; + let currentId: string | null = rootId; + let isValid = true; + + while (currentId !== null || stack.length > 0) { + while (currentId !== null) { + const node = nodeMap.get(currentId); + if (!node) break; + stack.push(currentId); + tracker.compareNode(currentId, node.value, { currentNode: currentId, pushing: true }); + currentId = node.leftChildId; + } + + const poppedId = stack.pop(); + if (!poppedId) break; + const node = nodeMap.get(poppedId); + if (!node) break; + + if (node.value <= previousValue) { + tracker.searchFound(poppedId, { violation: true, nodeValue: node.value, previousValue }); + isValid = false; + break; + } + + previousValue = node.value; + tracker.compareNode(poppedId, node.value, { + currentNode: poppedId, + previousValue, + ascending: true, + }); + currentId = node.rightChildId; + } + + tracker.complete({ isValid }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/bst-operations/bst-validation/BSTValidationPipeline.stories.tsx b/src/algorithms/trees/bst-operations/bst-validation/BSTValidationPipeline.stories.tsx new file mode 100644 index 00000000..c2365ebe --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/BSTValidationPipeline.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstValidationSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstValidationSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Validation (Recursive)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { visualState: steps[0]!.visualState as TreeVisualState }, +}; + +export const MidOperation: Story = { + args: { visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState }, +}; + +export const OperationComplete: Story = { + args: { visualState: steps[steps.length - 1]!.visualState as TreeVisualState }, +}; diff --git a/src/algorithms/trees/bst-operations/bst-validation/bst-validation.test.ts b/src/algorithms/trees/bst-operations/bst-validation/bst-validation.test.ts new file mode 100644 index 00000000..825ea87c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/bst-validation.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { bstValidation } from "./sources/bst-validation.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstValidation", () => { + it("validates a correct BST", () => { + const tree = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstValidation(tree)).toBe(true); + }); + it("rejects an invalid BST", () => { + const invalid = node(5, node(6), node(7)); + expect(bstValidation(invalid)).toBe(false); + }); + it("accepts a null tree", () => { + expect(bstValidation(null)).toBe(true); + }); + it("accepts a single node", () => { + expect(bstValidation(node(42))).toBe(true); + }); + it("rejects a non-local violation", () => { + // Child-only checks would miss this: 3 is in right subtree of 5 but < 5 + const invalid = node(5, null, node(10, node(3), null)); + expect(bstValidation(invalid)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-validation/educational.ts b/src/algorithms/trees/bst-operations/bst-validation/educational.ts new file mode 100644 index 00000000..0c0d1d74 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/educational.ts @@ -0,0 +1,34 @@ +import type { EducationalContent } from "@/types"; + +export const bstValidationEducational: EducationalContent = { + overview: + "**BST Validation (Recursive)** verifies that a binary tree satisfies the BST property at every node: all left-descendant values must be strictly less than the node, and all right-descendant values must be strictly greater.\n\nA naïve approach checks only immediate children — it fails for nodes that are valid locally but violate a global constraint. This algorithm passes **min/max bounds** down the recursion to catch all violations.", + + howItWorks: + "Each node is validated against a window `(minVal, maxVal)`:\n- Root is validated against `(-∞, +∞)`.\n- Left child is validated against `(minVal, node.value)` — must be less than parent.\n- Right child is validated against `(node.value, maxVal)` — must be greater than parent.\n\nAny node that falls outside its window fails validation immediately.", + + timeAndSpaceComplexity: + "**Time: `O(n)`** — every node is visited exactly once.\n\n**Space: `O(h)`** — recursion depth.", + + bestAndWorstCase: + "**Best case:** First node violates BST — returns `false` after one comparison.\n\n**Worst case:** Valid BST — all `n` nodes must be checked.", + + realWorldUses: [ + "**Tree deserialization:** Validate that a deserialized tree is structurally correct before use.", + "**Testing BST implementations:** Correctness check after insertions and deletions.", + "**Database consistency:** Verify B-tree node bounds after a page merge or split.", + ], + + strengthsAndLimitations: { + strengths: [ + "Correctly handles non-local BST violations that naïve child-only checks miss.", + "Simple to understand once the min/max bounds pattern is grasped.", + ], + limitations: [ + "Visits all n nodes even if only validating a small subtree — no early pruning for valid trees.", + ], + }, + + whenToUseIt: + "Use after any structural modification to a BST to assert correctness. The iterative in-order variant offers the same correctness guarantee with O(1) space.", +}; diff --git a/src/algorithms/trees/bst-operations/bst-validation/index.ts b/src/algorithms/trees/bst-operations/bst-validation/index.ts new file mode 100644 index 00000000..1244217e --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/index.ts @@ -0,0 +1,117 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstValidation } from "./sources/bst-validation.ts?fn"; +import { generateBstValidationSteps } from "./step-generator"; +import type { BstValidationInput } from "./step-generator"; +import { bstValidationEducational } from "./educational"; + +import typescriptSource from "./sources/bst-validation.ts?raw"; +import pythonSource from "./sources/bst-validation.py?raw"; +import javaSource from "./sources/BSTValidation.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BSTNodeShape { + value: number; + left: BSTNodeShape | null; + right: BSTNodeShape | null; +} + +function executeBstValidation(input: BstValidationInput): boolean { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBST(id: string | null): BSTNodeShape | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { value: node.value, left: toBST(node.leftChildId), right: toBST(node.rightChildId) }; + } + const root = toBST(input.rootId); + return bstValidation(root) as boolean; +} + +const bstValidationDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_VALIDATION!, + name: "BST Validation (Recursive)", + category: CATEGORY.TREES!, + technique: "bst-operations", + description: + "Recursive BST validation using min/max bounds to check the BST property at every node", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBstValidation, + generateSteps: generateBstValidationSteps, + educational: bstValidationEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(bstValidationDefinition); diff --git a/src/algorithms/trees/bst-operations/bst-validation/sources/BSTValidation.java b/src/algorithms/trees/bst-operations/bst-validation/sources/BSTValidation.java new file mode 100644 index 00000000..1c9d396c --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/sources/BSTValidation.java @@ -0,0 +1,27 @@ +// BST Validation (Recursive) — validate BST property using min/max bounds + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTValidation { + public boolean bstValidation(BSTNode root) { + return validate(root, Long.MIN_VALUE, Long.MAX_VALUE); // @step:complete + } + + private boolean validate(BSTNode node, long minVal, long maxVal) { + if (node == null) return true; // @step:initialize + + if (node.value <= minVal || node.value >= maxVal) { + // Node value violates BST bounds + return false; // @step:found + } + + // Recurse: left subtree values must be less than current node + // Right subtree values must be greater than current node + return validate(node.left, minVal, node.value) && // @step:search-node + validate(node.right, node.value, maxVal); // @step:search-node + } +} diff --git a/src/algorithms/trees/bst-operations/bst-validation/sources/bst-validation.py b/src/algorithms/trees/bst-operations/bst-validation/sources/bst-validation.py new file mode 100644 index 00000000..b6684401 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/sources/bst-validation.py @@ -0,0 +1,25 @@ +# BST Validation (Recursive) — validate BST property using min/max bounds + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_validation(root) -> bool: + def validate(node, min_val, max_val) -> bool: + if node is None: # @step:initialize + return True + + if node.value <= min_val or node.value >= max_val: + # Node value violates BST bounds + return False # @step:found + + # Recurse: left subtree values must be less than current node + # Right subtree values must be greater than current node + return ( + validate(node.left, min_val, node.value) and # @step:search-node + validate(node.right, node.value, max_val) # @step:search-node + ) + + return validate(root, float("-inf"), float("inf")) # @step:complete diff --git a/src/algorithms/trees/bst-operations/bst-validation/sources/bst-validation.ts b/src/algorithms/trees/bst-operations/bst-validation/sources/bst-validation.ts new file mode 100644 index 00000000..f7abdcbd --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/sources/bst-validation.ts @@ -0,0 +1,27 @@ +// BST Validation (Recursive) — validate BST property using min/max bounds + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstValidation(root: BSTNode | null): boolean { + function validate(node: BSTNode | null, minVal: number, maxVal: number): boolean { + if (node === null) return true; // @step:initialize + + if (node.value <= minVal || node.value >= maxVal) { + // Node value violates BST bounds + return false; // @step:found + } + + // Recurse: left subtree values must be less than current node + // Right subtree values must be greater than current node + return ( + validate(node.left, minVal, node.value) && // @step:search-node + validate(node.right, node.value, maxVal) // @step:search-node + ); + } + + return validate(root, -Infinity, Infinity); // @step:complete +} diff --git a/src/algorithms/trees/bst-operations/bst-validation/step-generator.test.ts b/src/algorithms/trees/bst-operations/bst-validation/step-generator.test.ts new file mode 100644 index 00000000..14a0c1c2 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstValidationSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstValidationSteps", () => { + it("produces steps", () => { + const steps = generateBstValidationSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + it("starts with initialize", () => { + const steps = generateBstValidationSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + it("ends with complete", () => { + const steps = generateBstValidationSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + it("produces tree visual states", () => { + const steps = generateBstValidationSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + it("has incrementing indices", () => { + const steps = generateBstValidationSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let idx = 0; idx < steps.length; idx++) { + expect(steps[idx]?.index).toBe(idx); + } + }); +}); diff --git a/src/algorithms/trees/bst-operations/bst-validation/step-generator.ts b/src/algorithms/trees/bst-operations/bst-validation/step-generator.ts new file mode 100644 index 00000000..dbec9391 --- /dev/null +++ b/src/algorithms/trees/bst-operations/bst-validation/step-generator.ts @@ -0,0 +1,56 @@ +/** Step generator for BST Validation (Recursive) — produces ExecutionStep[] using BSTOperationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { BSTOperationTracker } from "@/trackers/bst-operation-tracker"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_VALIDATION_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_VALIDATION!); + +export interface BstValidationInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstValidationSteps(input: BstValidationInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new BSTOperationTracker(nodes, rootId, BST_VALIDATION_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize(null, { rootId }); + + let isValid = true; + + function validate(nodeId: string | null, minVal: number, maxVal: number): boolean { + if (!nodeId) return true; + const node = nodeMap.get(nodeId); + if (!node) return true; + + tracker.compareNode(nodeId, node.value, { + currentNode: nodeId, + nodeValue: node.value, + minBound: minVal, + maxBound: maxVal, + }); + + if (node.value <= minVal || node.value >= maxVal) { + tracker.searchFound(nodeId, { + violation: true, + nodeValue: node.value, + minBound: minVal, + maxBound: maxVal, + }); + return false; + } + + return ( + validate(node.leftChildId, minVal, node.value) && + validate(node.rightChildId, node.value, maxVal) + ); + } + + isValid = validate(rootId, -Infinity, Infinity); + tracker.complete({ isValid }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/construction/build-from-level-order/BuildFromLevelOrderPipeline.stories.tsx b/src/algorithms/trees/construction/build-from-level-order/BuildFromLevelOrderPipeline.stories.tsx new file mode 100644 index 00000000..fbef3b63 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/BuildFromLevelOrderPipeline.stories.tsx @@ -0,0 +1,49 @@ +/** + * Storybook stories for the Build BST from Level-Order pipeline. + * Uses the real step generator with a 7-node balanced BST input, + * rendering the TreeVisualizer at key construction states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBuildFromLevelOrderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBuildFromLevelOrderSteps({ + levelOrder: [4, 2, 6, 1, 3, 5, 7], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Build BST from Level-Order", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty BST before any insertions */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-construction — BST partially built from level-order values */ +export const MidConstruction: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Construction complete — all 7 values inserted into BST */ +export const ConstructionComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/construction/build-from-level-order/build-from-level-order.test.ts b/src/algorithms/trees/construction/build-from-level-order/build-from-level-order.test.ts new file mode 100644 index 00000000..1ecdc961 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/build-from-level-order.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import { buildFromLevelOrder } from "./sources/build-from-level-order.ts?fn"; + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function inorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...inorder(root.left), root.value, ...inorder(root.right)]; +} + +function levelOrder(root: TreeNode | null): number[] { + if (!root) return []; + const result: number[] = []; + const queue: TreeNode[] = [root]; + while (queue.length > 0) { + const node = queue.shift(); + if (!node) break; + result.push(node.value); + if (node.left) queue.push(node.left); + if (node.right) queue.push(node.right); + } + return result; +} + +describe("buildFromLevelOrder", () => { + it("builds a balanced 7-node BST from level-order sequence", () => { + const root = buildFromLevelOrder([4, 2, 6, 1, 3, 5, 7]) as TreeNode; + expect(root.value).toBe(4); + expect(inorder(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("produces sorted inorder traversal", () => { + const root = buildFromLevelOrder([4, 2, 6, 1, 3, 5, 7]) as TreeNode; + expect(inorder(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("restores original level-order when input is valid BST level-order", () => { + const input = [4, 2, 6, 1, 3, 5, 7]; + const root = buildFromLevelOrder(input) as TreeNode; + expect(levelOrder(root)).toEqual(input); + }); + + it("returns null for empty input", () => { + expect(buildFromLevelOrder([])).toBeNull(); + }); + + it("handles a single-node input", () => { + const root = buildFromLevelOrder([42]) as TreeNode; + expect(root.value).toBe(42); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("handles a two-node input with right child", () => { + const root = buildFromLevelOrder([1, 2]) as TreeNode; + expect(root.value).toBe(1); + expect(root.left).toBeNull(); + expect((root.right as TreeNode).value).toBe(2); + }); + + it("handles a three-node balanced input", () => { + const root = buildFromLevelOrder([2, 1, 3]) as TreeNode; + expect(root.value).toBe(2); + expect((root.left as TreeNode).value).toBe(1); + expect((root.right as TreeNode).value).toBe(3); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-level-order/educational.ts b/src/algorithms/trees/construction/build-from-level-order/educational.ts new file mode 100644 index 00000000..edc4e943 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/educational.ts @@ -0,0 +1,69 @@ +import type { EducationalContent } from "@/types"; + +export const buildFromLevelOrderEducational: EducationalContent = { + overview: + "**Build BST from Level-Order** inserts values from a level-order array into a Binary Search Tree one by one, " + + "using standard BST insertion for each value. The result is a BST whose level-order traversal matches the input sequence, " + + "making it useful for reconstructing a BST from its breadth-first representation.", + + howItWorks: + "The algorithm iterates through the level-order array and performs a standard BST insert for each value:\n\n" + + "1. **Initialize** — start with an empty tree (root = null).\n" + + "2. **For each value in level-order:**\n" + + " - Compare the value against the current node.\n" + + " - Traverse left if the value is smaller, right if larger.\n" + + " - When an empty slot is found, create a new node.\n" + + "3. **Return the root** after all insertions.\n\n" + + "### Example\n\n" + + "```\n" + + "levelOrder = [4, 2, 6, 1, 3, 5, 7]\n\n" + + "Insert 4 → root\n" + + "Insert 2 → left of 4\n" + + "Insert 6 → right of 4\n" + + "Insert 1 → left of 2\n" + + "Insert 3 → right of 2\n" + + "Insert 5 → left of 6\n" + + "Insert 7 → right of 6\n" + + "Result: perfectly balanced 7-node BST\n" + + "```\n\n" + + "For this to produce a valid BST level-order, the input must already be in BST-compatible level-order " + + "(i.e., values must honor BST invariants when inserted in sequence).", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n²)` worst case, `O(n log n)` average**\n\n" + + "Each insertion traverses `O(h)` nodes. For `n` insertions on a balanced tree, total time is `O(n log n)`. " + + "For a degenerate (sorted input) tree, each insertion costs `O(n)` → `O(n²)` total.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The tree holds `n` nodes. Recursion depth is `O(h)` — `O(log n)` balanced, `O(n)` worst case.", + + bestAndWorstCase: + "**Best case** is a level-order sequence that produces a balanced BST (e.g., root first, then left/right alternating). " + + "Each insertion is `O(log n)` → total `O(n log n)`.\n\n" + + "**Worst case** is a sorted or reverse-sorted sequence, producing a degenerate tree (linked list). " + + "Each insertion costs `O(n)` → total `O(n²)`.", + + realWorldUses: [ + "**BST reconstruction:** Rebuild a BST from a breadth-first level-order dump for persistence/transmission.", + "**Database index recovery:** Restore a B-tree or BST index from a level-order snapshot.", + "**Balanced tree seeding:** Constructing a BST from a level-order sequence that was generated from a balanced tree ensures reconstruction matches the original shape.", + "**Interview problems:** Classic coding challenge testing understanding of BST insertion and traversal duality.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple, intuitive approach — just repeated BST insertions.", + "Works for any set of distinct integer values.", + "Naturally recovers the original BST structure if input was its own level-order traversal.", + ], + limitations: [ + "O(n²) worst case for sorted or adversarial input.", + "Cannot reconstruct an arbitrary binary tree — only BSTs (values must obey BST ordering).", + "For general binary trees, preorder+inorder or BFS serialization methods are required instead.", + ], + }, + + whenToUseIt: + "Use this algorithm when you have a BST's level-order traversal and need to reconstruct the original tree. " + + "For general binary trees without BST ordering, use the serialize/deserialize approach instead. " + + "Avoid this method with sorted input — use a balanced insertion strategy to prevent degenerate tree growth.", +}; diff --git a/src/algorithms/trees/construction/build-from-level-order/index.ts b/src/algorithms/trees/construction/build-from-level-order/index.ts new file mode 100644 index 00000000..de1212c1 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/index.ts @@ -0,0 +1,48 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { buildFromLevelOrder } from "./sources/build-from-level-order.ts?fn"; +import { generateBuildFromLevelOrderSteps } from "./step-generator"; +import type { BuildFromLevelOrderInput } from "./step-generator"; +import { buildFromLevelOrderEducational } from "./educational"; + +import typescriptSource from "./sources/build-from-level-order.ts?raw"; +import pythonSource from "./sources/build-from-level-order.py?raw"; +import javaSource from "./sources/BuildFromLevelOrder.java?raw"; + +function executeBuildFromLevelOrder(input: BuildFromLevelOrderInput): number | null { + const result = buildFromLevelOrder(input.levelOrder) as { value: number } | null; + return result ? result.value : null; +} + +const buildFromLevelOrderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BUILD_FROM_LEVEL_ORDER!, + name: "Build BST from Level-Order", + category: CATEGORY.TREES!, + technique: "construction", + description: + "Constructs a Binary Search Tree by inserting level-order values one by one using standard BST insertion", + timeComplexity: { + best: "O(n log n)", + average: "O(n log n)", + worst: "O(n²)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + levelOrder: [4, 2, 6, 1, 3, 5, 7], + }, + }, + execute: executeBuildFromLevelOrder, + generateSteps: generateBuildFromLevelOrderSteps, + educational: buildFromLevelOrderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(buildFromLevelOrderDefinition); diff --git a/src/algorithms/trees/construction/build-from-level-order/sources/BuildFromLevelOrder.java b/src/algorithms/trees/construction/build-from-level-order/sources/BuildFromLevelOrder.java new file mode 100644 index 00000000..1546e74f --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/sources/BuildFromLevelOrder.java @@ -0,0 +1,37 @@ +// Build BST from Level-Order Sequence +// Insert each value from the level-order array into a BST using standard BST insertion. +// The resulting tree's level-order traversal will match the input array. + +class TreeNode { + int value; + TreeNode left, right; + TreeNode(int value) { this.value = value; } +} + +class BuildFromLevelOrder { + public TreeNode buildFromLevelOrder(int[] levelOrder) { + if (levelOrder.length == 0) return null; // @step:initialize + + TreeNode root = null; // @step:initialize + + for (int value : levelOrder) { // @step:select-element + root = bstInsert(root, value); // @step:build-node + } + + return root; // @step:complete + } + + private TreeNode bstInsert(TreeNode current, int value) { + if (current == null) { + return new TreeNode(value); // @step:build-node + } + + if (value < current.value) { + current.left = bstInsert(current.left, value); // @step:connect-child + } else if (value > current.value) { + current.right = bstInsert(current.right, value); // @step:connect-child + } + + return current; // @step:visit + } +} diff --git a/src/algorithms/trees/construction/build-from-level-order/sources/build-from-level-order.py b/src/algorithms/trees/construction/build-from-level-order/sources/build-from-level-order.py new file mode 100644 index 00000000..19ef11d3 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/sources/build-from-level-order.py @@ -0,0 +1,30 @@ +# Build BST from Level-Order Sequence +# Insert each value from the level-order array into a BST using standard BST insertion. +# The resulting tree's level-order traversal will match the input array. + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def build_from_level_order(level_order: list): + if not level_order: # @step:initialize + return None + + def bst_insert(current, value): # @step:initialize + if current is None: + return TreeNode(value) # @step:build-node + + if value < current.value: + current.left = bst_insert(current.left, value) # @step:connect-child + elif value > current.value: + current.right = bst_insert(current.right, value) # @step:connect-child + + return current # @step:visit + + root = None # @step:initialize + for value in level_order: # @step:select-element + root = bst_insert(root, value) # @step:build-node + + return root # @step:complete diff --git a/src/algorithms/trees/construction/build-from-level-order/sources/build-from-level-order.ts b/src/algorithms/trees/construction/build-from-level-order/sources/build-from-level-order.ts new file mode 100644 index 00000000..c1d4cc0e --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/sources/build-from-level-order.ts @@ -0,0 +1,37 @@ +// Build BST from Level-Order Sequence +// Insert each value from the level-order array into a BST using standard BST insertion. +// The resulting tree's level-order traversal will match the input array. + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function buildFromLevelOrder(levelOrder: number[]): TreeNode | null { + if (levelOrder.length === 0) return null; // @step:initialize + + let root: TreeNode | null = null; // @step:initialize + + function bstInsert(current: TreeNode | null, value: number): TreeNode { + // @step:initialize + if (current === null) { + return { value, left: null, right: null }; // @step:build-node + } + + if (value < current.value) { + current.left = bstInsert(current.left, value); // @step:connect-child + } else if (value > current.value) { + current.right = bstInsert(current.right, value); // @step:connect-child + } + + return current; // @step:visit + } + + for (const value of levelOrder) { + // @step:select-element + root = bstInsert(root, value); // @step:build-node + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/construction/build-from-level-order/step-generator.test.ts b/src/algorithms/trees/construction/build-from-level-order/step-generator.test.ts new file mode 100644 index 00000000..4fdc9eb1 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/step-generator.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { generateBuildFromLevelOrderSteps } from "./step-generator"; + +const defaultInput = { + levelOrder: [4, 2, 6, 1, 3, 5, 7], +}; + +describe("generateBuildFromLevelOrderSteps", () => { + it("produces steps for a 7-element input", () => { + const steps = generateBuildFromLevelOrderSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBuildFromLevelOrderSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBuildFromLevelOrderSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateBuildFromLevelOrderSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("builds exactly 7 nodes for 7 unique values", () => { + const steps = generateBuildFromLevelOrderSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateBuildFromLevelOrderSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("handles a single-element input", () => { + const steps = generateBuildFromLevelOrderSteps({ levelOrder: [5] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles an empty input", () => { + const steps = generateBuildFromLevelOrderSteps({ levelOrder: [] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-level-order/step-generator.ts b/src/algorithms/trees/construction/build-from-level-order/step-generator.ts new file mode 100644 index 00000000..d2592683 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-level-order/step-generator.ts @@ -0,0 +1,116 @@ +/** Step generator for Build BST from Level-Order — produces ExecutionStep[] using TreeConstructionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { TreeConstructionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BUILD_FROM_LEVEL_ORDER!); + +export interface BuildFromLevelOrderInput { + levelOrder: number[]; +} + +/** Compute a BST node's visual position based on its parent's position and which side it is on. */ +function computeNodePosition( + parentPosition: { x: number; y: number } | null, + side: "left" | "right" | null, + depth: number, +): { x: number; y: number } { + if (!parentPosition || !side) { + return { x: 200, y: 60 }; + } + const levelHeight = 100; + const horizontalOffset = Math.max(60 / depth, 30); + return { + x: side === "left" ? parentPosition.x - horizontalOffset : parentPosition.x + horizontalOffset, + y: parentPosition.y + levelHeight, + }; +} + +export function generateBuildFromLevelOrderSteps(input: BuildFromLevelOrderInput): ExecutionStep[] { + const { levelOrder } = input; + + const tracker = new TreeConstructionTracker([], "", LINE_MAP); + tracker.initialize({ levelOrder: [...levelOrder] }); + + if (levelOrder.length === 0) { + tracker.complete({ totalNodes: 0 }); + return tracker.getSteps(); + } + + // Track inserted nodes for BST traversal + interface InsertedNode { + nodeId: string; + value: number; + position: { x: number; y: number }; + } + + const insertedNodes = new Map(); + const bstLeft = new Map(); + const bstRight = new Map(); + let nodeCounter = 0; + + function insertIntoBST( + rootValue: number | null, + targetValue: number, + parentValue: number | null, + side: "left" | "right" | null, + depth: number, + ): number { + if (rootValue === null) { + nodeCounter++; + const nodeId = `node-${String(nodeCounter)}`; + const parentNode = parentValue !== null ? insertedNodes.get(parentValue) : null; + const position = computeNodePosition(parentNode?.position ?? null, side, Math.max(depth, 1)); + insertedNodes.set(targetValue, { nodeId, value: targetValue, position }); + bstLeft.set(targetValue, null); + bstRight.set(targetValue, null); + + tracker.buildNode(nodeId, targetValue, { nodeId, value: targetValue, depth }); + + if (parentNode && side) { + tracker.connectChild(parentNode.nodeId, nodeId, side, { + parentId: parentNode.nodeId, + childId: nodeId, + side, + }); + if (side === "left") { + bstLeft.set(parentValue!, targetValue); + } else { + bstRight.set(parentValue!, targetValue); + } + } + + tracker.markBuilt(nodeId, { nodeId, value: targetValue }); + return targetValue; + } + + tracker.partitionArray(rootValue, targetValue, rootValue, { + currentValue: rootValue, + targetValue, + direction: targetValue < rootValue ? "left" : "right", + }); + + if (targetValue < rootValue) { + const leftChild = bstLeft.get(rootValue) ?? null; + insertIntoBST(leftChild, targetValue, rootValue, "left", depth + 1); + } else if (targetValue > rootValue) { + const rightChild = bstRight.get(rootValue) ?? null; + insertIntoBST(rightChild, targetValue, rootValue, "right", depth + 1); + } + + return rootValue; + } + + let rootValue: number | null = null; + + for (const value of levelOrder) { + tracker.selectElement(value, { value, levelOrderIndex: levelOrder.indexOf(value) }); + rootValue = insertIntoBST(rootValue, value, null, null, 0); + } + + tracker.complete({ totalNodes: nodeCounter }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/BuildFromPostorderInorderIterativePipeline.stories.tsx b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/BuildFromPostorderInorderIterativePipeline.stories.tsx new file mode 100644 index 00000000..d90d9edf --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/BuildFromPostorderInorderIterativePipeline.stories.tsx @@ -0,0 +1,50 @@ +/** + * Storybook stories for the Build Tree from Postorder + Inorder (Iterative) pipeline. + * Uses the real step generator with a 7-node balanced BST, + * rendering the TreeVisualizer at key construction states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBuildFromPostorderInorderIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBuildFromPostorderInorderIterativeSteps({ + postorder: [1, 3, 2, 5, 7, 6, 4], + inorder: [1, 2, 3, 4, 5, 6, 7], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Build Tree: Postorder + Inorder (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — before iterative postorder construction begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-construction — iterative progress through postorder sequence */ +export const MidConstruction: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Construction complete — all 7 nodes placed */ +export const ConstructionComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/build-from-postorder-inorder-iterative.test.ts b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/build-from-postorder-inorder-iterative.test.ts new file mode 100644 index 00000000..bc107682 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/build-from-postorder-inorder-iterative.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { buildFromPostorderInorderIterative } from "./sources/build-from-postorder-inorder-iterative.ts?fn"; + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function inorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...inorder(root.left), root.value, ...inorder(root.right)]; +} + +function postorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...postorder(root.left), ...postorder(root.right), root.value]; +} + +describe("buildFromPostorderInorderIterative", () => { + it("builds a balanced 7-node BST", () => { + const root = buildFromPostorderInorderIterative( + [1, 3, 2, 5, 7, 6, 4], + [1, 2, 3, 4, 5, 6, 7], + ) as TreeNode; + expect(root.value).toBe(4); + expect(inorder(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("preserves inorder traversal", () => { + const inputInorder = [1, 2, 3, 4, 5, 6, 7]; + const root = buildFromPostorderInorderIterative( + [1, 3, 2, 5, 7, 6, 4], + inputInorder, + ) as TreeNode; + expect(inorder(root)).toEqual(inputInorder); + }); + + it("preserves postorder traversal", () => { + const inputPostorder = [1, 3, 2, 5, 7, 6, 4]; + const root = buildFromPostorderInorderIterative( + inputPostorder, + [1, 2, 3, 4, 5, 6, 7], + ) as TreeNode; + expect(postorder(root)).toEqual(inputPostorder); + }); + + it("returns null for empty input", () => { + expect(buildFromPostorderInorderIterative([], [])).toBeNull(); + }); + + it("handles a single-node tree", () => { + const root = buildFromPostorderInorderIterative([7], [7]) as TreeNode; + expect(root.value).toBe(7); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("builds a two-node tree with left child", () => { + const root = buildFromPostorderInorderIterative([1, 2], [1, 2]) as TreeNode; + expect(root.value).toBe(2); + expect((root.left as TreeNode).value).toBe(1); + expect(root.right).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/educational.ts b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/educational.ts new file mode 100644 index 00000000..e1fbac2f --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/educational.ts @@ -0,0 +1,54 @@ +import type { EducationalContent } from "@/types"; + +export const buildFromPostorderInorderIterativeEducational: EducationalContent = { + overview: + "**Build Tree from Postorder + Inorder (Iterative)** reconstructs a binary tree using a stack, " + + "mirroring the iterative preorder+inorder approach but reading both sequences right-to-left. " + + "The postorder array's last element is the root; subsequent elements are processed from right to left, " + + "with the inorder pointer (also right-to-left) signaling when to shift from right-child to left-child insertion.", + + howItWorks: + "The algorithm works by processing postorder in reverse (right-to-left):\n\n" + + "1. **Initialize** — create the root from `postorder[-1]`, push it on the stack, set inorder pointer to `length - 1`.\n" + + "2. **For each subsequent postorder value (right-to-left):**\n" + + " - Peek at the stack top.\n" + + " - If the stack top **does not** match `inorder[inorderPointer]`, attach new node as **right child** of the top.\n" + + " - Otherwise, **pop** nodes while they match inorder (decrementing pointer); the last popped becomes the **left-child parent**.\n" + + "3. Always **push** the new node onto the stack.\n\n" + + "### Key Insight\n\n" + + "Reading both sequences right-to-left converts the postorder problem into a mirror of the preorder iterative approach.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each node is pushed and popped from the stack at most once; each element in both arrays is processed once.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The explicit stack holds at most `O(h)` nodes where `h` is tree height.", + + bestAndWorstCase: + "**Best case** is a balanced tree — stack depth stays `O(log n)` with frequent left/right alternation.\n\n" + + "**Worst case** is a left-skewed tree — every node is pushed without popping until the entire sequence is consumed, resulting in `O(n)` stack depth.", + + realWorldUses: [ + "**Deep tree safety:** Avoids call-stack overflow for degenerate postorder-inorder inputs.", + "**Streaming reconstruction:** Well-suited for processing traversal sequences as they arrive in a stream.", + "**Embedded environments:** Iterative stack-based construction avoids recursion on memory-limited systems.", + "**Parallel with iterative preorder+inorder:** Demonstrates the duality between the two approaches.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — eliminates call-stack overflow risk for arbitrarily deep trees.", + "O(n) time with no hash map required.", + "Elegant mirror of the preorder iterative approach — understanding one makes the other intuitive.", + ], + limitations: [ + "Requires both postorder and inorder arrays with distinct values.", + "The right-to-left traversal direction can be counterintuitive to debug.", + "Both sequences must be complete and consistent.", + ], + }, + + whenToUseIt: + "Choose this algorithm when handling potentially degenerate trees from postorder+inorder input and call-stack depth is a concern. " + + "For most well-balanced inputs, the recursive variant is clearer and equally performant.", +}; diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/index.ts b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/index.ts new file mode 100644 index 00000000..e97d204e --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/index.ts @@ -0,0 +1,54 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { buildFromPostorderInorderIterative } from "./sources/build-from-postorder-inorder-iterative.ts?fn"; +import { generateBuildFromPostorderInorderIterativeSteps } from "./step-generator"; +import type { BuildFromPostorderInorderIterativeInput } from "./step-generator"; +import { buildFromPostorderInorderIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/build-from-postorder-inorder-iterative.ts?raw"; +import pythonSource from "./sources/build-from-postorder-inorder-iterative.py?raw"; +import javaSource from "./sources/BuildFromPostorderInorderIterative.java?raw"; + +function executeBuildFromPostorderInorderIterative( + input: BuildFromPostorderInorderIterativeInput, +): number | null { + const result = buildFromPostorderInorderIterative(input.postorder, input.inorder) as { + value: number; + } | null; + return result ? result.value : null; +} + +const buildFromPostorderInorderIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.BUILD_FROM_POSTORDER_INORDER_ITERATIVE!, + name: "Build Tree: Postorder + Inorder (Iterative)", + category: CATEGORY.TREES!, + technique: "construction", + description: + "Reconstructs a binary tree from postorder and inorder traversals using an explicit stack, processing both sequences right-to-left", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + postorder: [1, 3, 2, 5, 7, 6, 4], + inorder: [1, 2, 3, 4, 5, 6, 7], + }, + }, + execute: executeBuildFromPostorderInorderIterative, + generateSteps: generateBuildFromPostorderInorderIterativeSteps, + educational: buildFromPostorderInorderIterativeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, + }; + +registry.register(buildFromPostorderInorderIterativeDefinition); diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/BuildFromPostorderInorderIterative.java b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/BuildFromPostorderInorderIterative.java new file mode 100644 index 00000000..d858b23b --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/BuildFromPostorderInorderIterative.java @@ -0,0 +1,43 @@ +// Build Binary Tree from Postorder + Inorder (Iterative with Stack) +// Processes postorder right-to-left; uses inorder (right-to-left) to detect +// when to switch from right-child insertion to left-child insertion. +import java.util.Stack; + +class TreeNode { + int value; + TreeNode left, right; + TreeNode(int value) { this.value = value; } +} + +class BuildFromPostorderInorderIterative { + public TreeNode buildFromPostorderInorderIterative(int[] postorder, int[] inorder) { + if (postorder.length == 0) return null; // @step:initialize + + TreeNode root = new TreeNode(postorder[postorder.length - 1]); // @step:build-node + Stack stack = new Stack<>(); // @step:initialize + stack.push(root); // @step:initialize + int inorderPointer = inorder.length - 1; // @step:initialize + + for (int postorderPointer = postorder.length - 2; postorderPointer >= 0; postorderPointer--) { // @step:select-element + int currentValue = postorder[postorderPointer]; // @step:select-element + TreeNode parentNode = stack.peek(); // @step:search-node + TreeNode newNode = new TreeNode(currentValue); // @step:build-node + + // If stack top does not match inorder, attach as right child + if (parentNode.value != inorder[inorderPointer]) { + parentNode.right = newNode; // @step:connect-child + } else { + // Pop matching nodes to find the left-child parent + while (!stack.isEmpty() && stack.peek().value == inorder[inorderPointer]) { // @step:partition-array + parentNode = stack.pop(); // @step:partition-array + inorderPointer--; // @step:partition-array + } + parentNode.left = newNode; // @step:connect-child + } + + stack.push(newNode); // @step:visit + } + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/build-from-postorder-inorder-iterative.py b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/build-from-postorder-inorder-iterative.py new file mode 100644 index 00000000..0c66f480 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/build-from-postorder-inorder-iterative.py @@ -0,0 +1,36 @@ +# Build Binary Tree from Postorder + Inorder (Iterative with Stack) +# Processes postorder right-to-left; uses inorder (right-to-left) to detect +# when to switch from right-child insertion to left-child insertion. + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def build_from_postorder_inorder_iterative(postorder: list, inorder: list): + if not postorder: # @step:initialize + return None + + root = TreeNode(postorder[-1]) # @step:build-node + stack = [root] # @step:initialize + inorder_pointer = len(inorder) - 1 # @step:initialize + + for postorder_pointer in range(len(postorder) - 2, -1, -1): # @step:select-element + current_value = postorder[postorder_pointer] # @step:select-element + parent_node = stack[-1] # @step:search-node + new_node = TreeNode(current_value) # @step:build-node + + # If top of stack doesn't match inorder, attach as right child + if parent_node.value != inorder[inorder_pointer]: + parent_node.right = new_node # @step:connect-child + else: + # Pop matching nodes to find the left-child parent + while stack and stack[-1].value == inorder[inorder_pointer]: # @step:partition-array + parent_node = stack.pop() # @step:partition-array + inorder_pointer -= 1 # @step:partition-array + parent_node.left = new_node # @step:connect-child + + stack.append(new_node) # @step:visit + + return root # @step:visit diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/build-from-postorder-inorder-iterative.ts b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/build-from-postorder-inorder-iterative.ts new file mode 100644 index 00000000..28ce97c6 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/sources/build-from-postorder-inorder-iterative.ts @@ -0,0 +1,51 @@ +// Build Binary Tree from Postorder + Inorder (Iterative with Stack) +// Processes postorder right-to-left; uses inorder (processed right-to-left too) +// to determine when to switch from right-child insertion to left-child insertion. + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function buildFromPostorderInorderIterative( + postorder: number[], + inorder: number[], +): TreeNode | null { + if (postorder.length === 0) return null; // @step:initialize + + const lastValue = postorder[postorder.length - 1]; // @step:initialize + if (lastValue === undefined) return null; + + const root: TreeNode = { value: lastValue, left: null, right: null }; // @step:build-node + const stack: TreeNode[] = [root]; // @step:initialize + let inorderPointer = inorder.length - 1; // @step:initialize + + for (let postorderPointer = postorder.length - 2; postorderPointer >= 0; postorderPointer--) { + // @step:select-element + const currentValue = postorder[postorderPointer]; // @step:select-element + if (currentValue === undefined) break; + + let parentNode = stack[stack.length - 1]; // @step:search-node + if (parentNode === undefined) break; + + const newNode: TreeNode = { value: currentValue, left: null, right: null }; // @step:build-node + + // If stack top differs from current inorder pointer, insert as right child + if (parentNode.value !== inorder[inorderPointer]) { + parentNode.right = newNode; // @step:connect-child + } else { + // Pop nodes matching inorder (right-to-left) to find left-child parent + while (stack.length > 0 && stack[stack.length - 1]?.value === inorder[inorderPointer]) { + // @step:partition-array + parentNode = stack.pop()!; // @step:partition-array + inorderPointer--; // @step:partition-array + } + parentNode.left = newNode; // @step:connect-child + } + + stack.push(newNode); // @step:visit + } + + return root; // @step:visit +} diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/step-generator.test.ts b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/step-generator.test.ts new file mode 100644 index 00000000..02040a37 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/step-generator.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { generateBuildFromPostorderInorderIterativeSteps } from "./step-generator"; + +const defaultInput = { + postorder: [1, 3, 2, 5, 7, 6, 4], + inorder: [1, 2, 3, 4, 5, 6, 7], +}; + +describe("generateBuildFromPostorderInorderIterativeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("builds exactly 7 nodes", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("handles a single-element input", () => { + const steps = generateBuildFromPostorderInorderIterativeSteps({ postorder: [3], inorder: [3] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/step-generator.ts b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/step-generator.ts new file mode 100644 index 00000000..29912089 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder-iterative/step-generator.ts @@ -0,0 +1,156 @@ +/** Step generator for Build Tree from Postorder + Inorder Iterative — produces ExecutionStep[] using TreeConstructionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { TreeConstructionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BUILD_FROM_POSTORDER_INORDER_ITERATIVE!); + +export interface BuildFromPostorderInorderIterativeInput { + postorder: number[]; + inorder: number[]; +} + +/** Compute visual position given depth and horizontal slot. */ +function computePosition( + depth: number, + nodeIndex: number, + totalAtDepth: number, +): { x: number; y: number } { + const treeWidth = 400; + const levelHeight = 100; + const segmentWidth = treeWidth / Math.max(totalAtDepth, 1); + return { + x: segmentWidth * nodeIndex + segmentWidth / 2, + y: depth * levelHeight + 60, + }; +} + +export function generateBuildFromPostorderInorderIterativeSteps( + input: BuildFromPostorderInorderIterativeInput, +): ExecutionStep[] { + const { postorder, inorder } = input; + + const tracker = new TreeConstructionTracker([], "", LINE_MAP); + tracker.initialize({ postorder: [...postorder], inorder: [...inorder] }); + + if (postorder.length === 0) { + tracker.complete({ totalNodes: 0 }); + return tracker.getSteps(); + } + + const lastValue = postorder[postorder.length - 1]; + if (lastValue === undefined) { + tracker.complete({ totalNodes: 0 }); + return tracker.getSteps(); + } + + let nodeCounter = 0; + const depthNodeCount: number[] = [1]; + const depthCurrentIdx: number[] = [0]; + + // Pre-compute depth for each postorder value using the same iterative logic + const valueToDepth = new Map(); + { + const tempStack: { value: number; depth: number }[] = [{ value: lastValue, depth: 0 }]; + let inorderIdx = inorder.length - 1; + for (let postIdx = postorder.length - 2; postIdx >= 0; postIdx--) { + const currentValue = postorder[postIdx]; + if (currentValue === undefined) break; + const stackTop = tempStack[tempStack.length - 1]; + if (!stackTop) break; + let parentDepth = stackTop.depth; + let newDepth: number; + if (stackTop.value !== inorder[inorderIdx]) { + newDepth = parentDepth + 1; + tempStack.push({ value: currentValue, depth: newDepth }); + } else { + while ( + tempStack.length > 0 && + tempStack[tempStack.length - 1]?.value === inorder[inorderIdx] + ) { + const popped = tempStack.pop(); + if (popped) parentDepth = popped.depth; + inorderIdx--; + } + newDepth = parentDepth + 1; + tempStack.push({ value: currentValue, depth: newDepth }); + } + valueToDepth.set(currentValue, newDepth); + depthNodeCount[newDepth] = (depthNodeCount[newDepth] ?? 0) + 1; + } + } + valueToDepth.set(lastValue, 0); + + function makeNodeId(value: number): string { + nodeCounter++; + const nodeId = `node-${String(nodeCounter)}`; + const depth = valueToDepth.get(value) ?? 0; + while (depthCurrentIdx.length <= depth) depthCurrentIdx.push(0); + const nodeIndex = depthCurrentIdx[depth] ?? 0; + depthCurrentIdx[depth] = nodeIndex + 1; + const position = computePosition(depth, nodeIndex, depthNodeCount[depth] ?? 1); + void position; + return nodeId; + } + + interface StackEntry { + nodeId: string; + value: number; + } + + const rootNodeId = makeNodeId(lastValue); + tracker.selectElement(lastValue, { value: lastValue, postorderPointer: postorder.length - 1 }); + tracker.buildNode(rootNodeId, lastValue, { nodeId: rootNodeId, value: lastValue }); + + const stack: StackEntry[] = [{ nodeId: rootNodeId, value: lastValue }]; + let inorderPointer = inorder.length - 1; + + for (let postorderPointer = postorder.length - 2; postorderPointer >= 0; postorderPointer--) { + const currentValue = postorder[postorderPointer]; + if (currentValue === undefined) break; + + tracker.selectElement(currentValue, { value: currentValue, postorderPointer }); + + const stackTop = stack[stack.length - 1]; + if (!stackTop) break; + + const newNodeId = makeNodeId(currentValue); + tracker.buildNode(newNodeId, currentValue, { nodeId: newNodeId, value: currentValue }); + + if (stackTop.value !== inorder[inorderPointer]) { + tracker.connectChild(stackTop.nodeId, newNodeId, "right", { + parentId: stackTop.nodeId, + childId: newNodeId, + side: "right", + }); + stack.push({ nodeId: newNodeId, value: currentValue }); + } else { + let lastPopped: StackEntry | undefined; + while (stack.length > 0 && stack[stack.length - 1]?.value === inorder[inorderPointer]) { + lastPopped = stack.pop(); + tracker.partitionArray(inorderPointer, inorderPointer, inorderPointer, { + inorderPointer, + poppedValue: lastPopped?.value, + }); + inorderPointer--; + } + if (lastPopped) { + tracker.connectChild(lastPopped.nodeId, newNodeId, "left", { + parentId: lastPopped.nodeId, + childId: newNodeId, + side: "left", + }); + } + stack.push({ nodeId: newNodeId, value: currentValue }); + } + + tracker.markBuilt(newNodeId, { nodeId: newNodeId, value: currentValue }); + } + + tracker.markBuilt(rootNodeId, { nodeId: rootNodeId, value: lastValue }); + tracker.complete({ totalNodes: nodeCounter }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/BuildFromPostorderInorderPipeline.stories.tsx b/src/algorithms/trees/construction/build-from-postorder-inorder/BuildFromPostorderInorderPipeline.stories.tsx new file mode 100644 index 00000000..6f8773e0 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/BuildFromPostorderInorderPipeline.stories.tsx @@ -0,0 +1,50 @@ +/** + * Storybook stories for the Build Tree from Postorder + Inorder algorithm pipeline. + * Uses the real step generator with a 7-node balanced BST, + * rendering the TreeVisualizer at key construction states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBuildFromPostorderInorderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBuildFromPostorderInorderSteps({ + postorder: [1, 3, 2, 5, 7, 6, 4], + inorder: [1, 2, 3, 4, 5, 6, 7], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Build Tree: Postorder + Inorder", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty tree before construction begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-construction — tree partially built from postorder + inorder */ +export const MidConstruction: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Construction complete — all 7 nodes placed */ +export const ConstructionComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/build-from-postorder-inorder.test.ts b/src/algorithms/trees/construction/build-from-postorder-inorder/build-from-postorder-inorder.test.ts new file mode 100644 index 00000000..7aed4403 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/build-from-postorder-inorder.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from "vitest"; +import { buildFromPostorderInorder } from "./sources/build-from-postorder-inorder.ts?fn"; + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function inorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...inorder(root.left), root.value, ...inorder(root.right)]; +} + +function postorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...postorder(root.left), ...postorder(root.right), root.value]; +} + +describe("buildFromPostorderInorder", () => { + it("builds a balanced 7-node BST", () => { + const root = buildFromPostorderInorder( + [1, 3, 2, 5, 7, 6, 4], + [1, 2, 3, 4, 5, 6, 7], + ) as TreeNode; + expect(root.value).toBe(4); + expect(inorder(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("preserves inorder traversal", () => { + const inputInorder = [1, 2, 3, 4, 5, 6, 7]; + const root = buildFromPostorderInorder([1, 3, 2, 5, 7, 6, 4], inputInorder) as TreeNode; + expect(inorder(root)).toEqual(inputInorder); + }); + + it("preserves postorder traversal", () => { + const inputPostorder = [1, 3, 2, 5, 7, 6, 4]; + const root = buildFromPostorderInorder(inputPostorder, [1, 2, 3, 4, 5, 6, 7]) as TreeNode; + expect(postorder(root)).toEqual(inputPostorder); + }); + + it("returns null for empty arrays", () => { + expect(buildFromPostorderInorder([], [])).toBeNull(); + }); + + it("handles a single-node tree", () => { + const root = buildFromPostorderInorder([42], [42]) as TreeNode; + expect(root.value).toBe(42); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("builds a two-node tree correctly", () => { + const root = buildFromPostorderInorder([1, 2], [1, 2]) as TreeNode; + expect(root.value).toBe(2); + expect((root.left as TreeNode).value).toBe(1); + expect(root.right).toBeNull(); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/educational.ts b/src/algorithms/trees/construction/build-from-postorder-inorder/educational.ts new file mode 100644 index 00000000..485a5193 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const buildFromPostorderInorderEducational: EducationalContent = { + overview: + "**Build Tree from Postorder + Inorder** reconstructs a unique binary tree from its postorder and inorder traversal sequences. " + + "Unlike the preorder variant, here the **last** element of the postorder array is the current subtree's root. " + + "The inorder array reveals which elements belong to the left vs right subtree.", + + howItWorks: + "The algorithm recursively identifies the root, splits both arrays, and recurses:\n\n" + + "1. **Identify root** — `postorder[last]` is always the current subtree's root.\n" + + "2. **Locate root in inorder** — everything to its left belongs to the left subtree; everything to its right is the right subtree.\n" + + "3. **Slice both arrays** — use the left subtree size to extract matching postorder and inorder slices for each child.\n" + + "4. **Recurse** — apply the same logic recursively until slices are empty.\n\n" + + "### Example\n\n" + + "```\n" + + "postorder = [1, 3, 2, 5, 7, 6, 4]\n" + + "inorder = [1, 2, 3, 4, 5, 6, 7]\n\n" + + "Root = 4 (postorder[-1])\n" + + "Inorder index of 4 = 3 → left has [1,2,3], right has [5,6,7]\n" + + "Left postorder = [1,3,2], right postorder = [5,7,6]\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n²)` naive, `O(n)` with hash map**\n\n" + + "Searching for the root in the inorder array takes `O(n)` per call without a lookup table. " + + "A precomputed hash map reduces each lookup to `O(1)` and total time to `O(n)`.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "Recursion depth is `O(h)` where `h` is the tree height, plus `O(n)` for the index map.", + + bestAndWorstCase: + "**Best case** is a balanced tree — `O(log n)` recursion depth and even partitioning at each level.\n\n" + + "**Worst case** is a completely left- or right-skewed tree — recursion depth reaches `O(n)` and may overflow the call stack.", + + realWorldUses: [ + "**Tree deserialization:** Reconstruct a tree from its serialized postorder and inorder representations.", + "**Expression tree recovery:** Postorder corresponds to postfix evaluation order; recovering the original expression tree aids code analysis.", + "**Compiler intermediate representations:** Recover the original AST from postorder expression evaluation records.", + "**Algorithm education:** Classic problem for deepening understanding of tree traversal relationships.", + ], + + strengthsAndLimitations: { + strengths: [ + "Uniquely determines the tree given both traversals with distinct node values.", + "Naturally pairs with postorder output from tree evaluation algorithms.", + "Hash map optimization achieves O(n) time.", + ], + limitations: [ + "Requires both postorder and inorder arrays — neither alone is sufficient.", + "All values must be distinct; duplicates break the reconstruction.", + "Deep recursion on skewed trees can overflow the call stack.", + ], + }, + + whenToUseIt: + "Use when you have postorder output (e.g., from expression evaluation or deletion-order logging) and need to recover the tree structure. " + + "For very large trees, convert to the iterative variant or apply the hash map optimization to avoid O(n²) worst case.", +}; diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/index.ts b/src/algorithms/trees/construction/build-from-postorder-inorder/index.ts new file mode 100644 index 00000000..48983cf8 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/index.ts @@ -0,0 +1,51 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { buildFromPostorderInorder } from "./sources/build-from-postorder-inorder.ts?fn"; +import { generateBuildFromPostorderInorderSteps } from "./step-generator"; +import type { BuildFromPostorderInorderInput } from "./step-generator"; +import { buildFromPostorderInorderEducational } from "./educational"; + +import typescriptSource from "./sources/build-from-postorder-inorder.ts?raw"; +import pythonSource from "./sources/build-from-postorder-inorder.py?raw"; +import javaSource from "./sources/BuildFromPostorderInorder.java?raw"; + +function executeBuildFromPostorderInorder(input: BuildFromPostorderInorderInput): number | null { + const result = buildFromPostorderInorder(input.postorder, input.inorder) as { + value: number; + } | null; + return result ? result.value : null; +} + +const buildFromPostorderInorderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BUILD_FROM_POSTORDER_INORDER!, + name: "Build Tree: Postorder + Inorder", + category: CATEGORY.TREES!, + technique: "construction", + description: + "Reconstructs a unique binary tree from its postorder and inorder traversal sequences using recursive divide-and-conquer", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n²)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + postorder: [1, 3, 2, 5, 7, 6, 4], + inorder: [1, 2, 3, 4, 5, 6, 7], + }, + }, + execute: executeBuildFromPostorderInorder, + generateSteps: generateBuildFromPostorderInorderSteps, + educational: buildFromPostorderInorderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(buildFromPostorderInorderDefinition); diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/sources/BuildFromPostorderInorder.java b/src/algorithms/trees/construction/build-from-postorder-inorder/sources/BuildFromPostorderInorder.java new file mode 100644 index 00000000..a21b8578 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/sources/BuildFromPostorderInorder.java @@ -0,0 +1,38 @@ +// Build Binary Tree from Postorder + Inorder Traversal (Recursive) +// Last element of postorder is root; find root in inorder to split left/right subtrees + +class TreeNode { + int value; + TreeNode left, right; + TreeNode(int value) { this.value = value; } +} + +class BuildFromPostorderInorder { + public TreeNode buildFromPostorderInorder(int[] postorder, int[] inorder) { + return build(postorder, 0, postorder.length - 1, inorder, 0, inorder.length - 1); // @step:initialize + } + + private TreeNode build(int[] postorder, int postStart, int postEnd, + int[] inorder, int inStart, int inEnd) { + if (postStart > postEnd || inStart > inEnd) return null; // @step:initialize + + int rootValue = postorder[postEnd]; // @step:select-element + TreeNode root = new TreeNode(rootValue); // @step:build-node + + // Find root's position in inorder array + int inorderRootIndex = inStart; // @step:partition-array + while (inorderRootIndex <= inEnd && inorder[inorderRootIndex] != rootValue) { + inorderRootIndex++; // @step:partition-array + } + + int leftSubtreeSize = inorderRootIndex - inStart; // @step:partition-array + + // Recurse for left and right subtrees + root.left = build(postorder, postStart, postStart + leftSubtreeSize - 1, + inorder, inStart, inorderRootIndex - 1); // @step:connect-child + root.right = build(postorder, postStart + leftSubtreeSize, postEnd - 1, + inorder, inorderRootIndex + 1, inEnd); // @step:connect-child + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/sources/build-from-postorder-inorder.py b/src/algorithms/trees/construction/build-from-postorder-inorder/sources/build-from-postorder-inorder.py new file mode 100644 index 00000000..60a78dc0 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/sources/build-from-postorder-inorder.py @@ -0,0 +1,29 @@ +# Build Binary Tree from Postorder + Inorder Traversal (Recursive) +# Last element of postorder is root; find root in inorder to split left/right subtrees + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def build_from_postorder_inorder(postorder: list, inorder: list): + if not postorder or not inorder: # @step:initialize + return None + + root_value = postorder[-1] # @step:select-element + root = TreeNode(root_value) # @step:build-node + + inorder_root_index = inorder.index(root_value) # @step:partition-array + + # Split inorder into left and right subtree sequences + left_inorder = inorder[:inorder_root_index] # @step:partition-array + right_inorder = inorder[inorder_root_index + 1:] # @step:partition-array + + left_postorder = postorder[:len(left_inorder)] # @step:partition-array + right_postorder = postorder[len(left_inorder):-1] # @step:partition-array + + root.left = build_from_postorder_inorder(left_postorder, left_inorder) # @step:connect-child + root.right = build_from_postorder_inorder(right_postorder, right_inorder) # @step:connect-child + + return root # @step:visit diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/sources/build-from-postorder-inorder.ts b/src/algorithms/trees/construction/build-from-postorder-inorder/sources/build-from-postorder-inorder.ts new file mode 100644 index 00000000..9fda7729 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/sources/build-from-postorder-inorder.ts @@ -0,0 +1,31 @@ +// Build Binary Tree from Postorder + Inorder Traversal (Recursive) +// Last element of postorder is root; find root in inorder to split left/right subtrees + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function buildFromPostorderInorder(postorder: number[], inorder: number[]): TreeNode | null { + if (postorder.length === 0 || inorder.length === 0) return null; // @step:initialize + + const rootValue = postorder[postorder.length - 1]; // @step:select-element + if (rootValue === undefined) return null; + + const root: TreeNode = { value: rootValue, left: null, right: null }; // @step:build-node + + const inorderRootIndex = inorder.indexOf(rootValue); // @step:partition-array + + // Split inorder and postorder into left/right subtrees + const leftInorder = inorder.slice(0, inorderRootIndex); // @step:partition-array + const rightInorder = inorder.slice(inorderRootIndex + 1); // @step:partition-array + + const leftPostorder = postorder.slice(0, leftInorder.length); // @step:partition-array + const rightPostorder = postorder.slice(leftInorder.length, postorder.length - 1); // @step:partition-array + + root.left = buildFromPostorderInorder(leftPostorder, leftInorder); // @step:connect-child + root.right = buildFromPostorderInorder(rightPostorder, rightInorder); // @step:connect-child + + return root; // @step:visit +} diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/step-generator.test.ts b/src/algorithms/trees/construction/build-from-postorder-inorder/step-generator.test.ts new file mode 100644 index 00000000..183ccddf --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/step-generator.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { generateBuildFromPostorderInorderSteps } from "./step-generator"; + +const defaultInput = { + postorder: [1, 3, 2, 5, 7, 6, 4], + inorder: [1, 2, 3, 4, 5, 6, 7], +}; + +describe("generateBuildFromPostorderInorderSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateBuildFromPostorderInorderSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBuildFromPostorderInorderSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBuildFromPostorderInorderSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateBuildFromPostorderInorderSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("builds exactly 7 nodes", () => { + const steps = generateBuildFromPostorderInorderSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateBuildFromPostorderInorderSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("handles a single-element input", () => { + const steps = generateBuildFromPostorderInorderSteps({ postorder: [1], inorder: [1] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-postorder-inorder/step-generator.ts b/src/algorithms/trees/construction/build-from-postorder-inorder/step-generator.ts new file mode 100644 index 00000000..feb2b06d --- /dev/null +++ b/src/algorithms/trees/construction/build-from-postorder-inorder/step-generator.ts @@ -0,0 +1,143 @@ +/** Step generator for Build Tree from Postorder + Inorder — produces ExecutionStep[] using TreeConstructionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { TreeConstructionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BUILD_FROM_POSTORDER_INORDER!); + +export interface BuildFromPostorderInorderInput { + postorder: number[]; + inorder: number[]; +} + +/** Compute visual position for a node at the given depth and horizontal index. */ +function computePosition( + depth: number, + nodeIndex: number, + totalAtDepth: number, +): { x: number; y: number } { + const treeWidth = 400; + const levelHeight = 100; + const segmentWidth = treeWidth / Math.max(totalAtDepth, 1); + return { + x: segmentWidth * nodeIndex + segmentWidth / 2, + y: depth * levelHeight + 60, + }; +} + +export function generateBuildFromPostorderInorderSteps( + input: BuildFromPostorderInorderInput, +): ExecutionStep[] { + const { postorder, inorder } = input; + + const inorderIndexMap = new Map(); + inorder.forEach((value, idx) => inorderIndexMap.set(value, idx)); + + const depthTotals = new Map(); + const depthCounters = new Map(); + + function countNodesAtDepth( + postorderSlice: number[], + inorderSlice: number[], + depth: number, + ): void { + if (postorderSlice.length === 0) return; + const rootValue = postorderSlice[postorderSlice.length - 1]; + if (rootValue === undefined) return; + depthTotals.set(depth, (depthTotals.get(depth) ?? 0) + 1); + + // Find the root's position within the current inorder slice (not the global index) + const localRootIndex = inorderSlice.indexOf(rootValue); + if (localRootIndex === -1) return; + const leftSize = localRootIndex; + countNodesAtDepth( + postorderSlice.slice(0, leftSize), + inorderSlice.slice(0, localRootIndex), + depth + 1, + ); + countNodesAtDepth( + postorderSlice.slice(leftSize, postorderSlice.length - 1), + inorderSlice.slice(localRootIndex + 1), + depth + 1, + ); + } + + countNodesAtDepth(postorder, inorder, 0); + + const tracker = new TreeConstructionTracker([], "", LINE_MAP); + tracker.initialize({ postorder: [...postorder], inorder: [...inorder] }); + + let nodeCounter = 0; + + function buildTree( + postStart: number, + postEnd: number, + inStart: number, + inEnd: number, + depth: number, + parentId: string | null, + side: "left" | "right" | null, + ): string | null { + if (postStart > postEnd || inStart > inEnd) return null; + + const rootValue = postorder[postEnd]; + if (rootValue === undefined) return null; + + tracker.selectElement(rootValue, { rootValue, postStart, postEnd, inStart, inEnd }); + + nodeCounter++; + const nodeId = `node-${String(nodeCounter)}`; + + const depthCount = depthTotals.get(depth) ?? 1; + const depthOffset = depthCounters.get(depth) ?? 0; + depthCounters.set(depth, depthOffset + 1); + const position = computePosition(depth, depthOffset, depthCount); + void position; + + tracker.buildNode(nodeId, rootValue, { nodeId, value: rootValue, depth }); + + const inorderRootIndex = inorderIndexMap.get(rootValue) ?? -1; + const leftSize = inorderRootIndex - inStart; + + tracker.partitionArray(inStart, inEnd, inorderRootIndex, { + inorderRootIndex, + leftSize, + rightSize: inEnd - inorderRootIndex, + }); + + const leftChildId = buildTree( + postStart, + postStart + leftSize - 1, + inStart, + inorderRootIndex - 1, + depth + 1, + nodeId, + "left", + ); + const rightChildId = buildTree( + postStart + leftSize, + postEnd - 1, + inorderRootIndex + 1, + inEnd, + depth + 1, + nodeId, + "right", + ); + + if (parentId && side) { + tracker.connectChild(parentId, nodeId, side, { parentId, childId: nodeId, side }); + } + + tracker.markBuilt(nodeId, { nodeId, value: rootValue, leftChildId, rightChildId }); + + return nodeId; + } + + buildTree(0, postorder.length - 1, 0, inorder.length - 1, 0, null, null); + + tracker.complete({ totalNodes: nodeCounter }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/BuildFromPreorderInorderIterativePipeline.stories.tsx b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/BuildFromPreorderInorderIterativePipeline.stories.tsx new file mode 100644 index 00000000..4997e0c6 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/BuildFromPreorderInorderIterativePipeline.stories.tsx @@ -0,0 +1,50 @@ +/** + * Storybook stories for the Build Tree from Preorder + Inorder (Iterative) pipeline. + * Uses the real step generator with a 7-node balanced BST, + * rendering the TreeVisualizer at key construction states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBuildFromPreorderInorderIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBuildFromPreorderInorderIterativeSteps({ + preorder: [4, 2, 1, 3, 6, 5, 7], + inorder: [1, 2, 3, 4, 5, 6, 7], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Build Tree: Preorder + Inorder (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — before iterative construction begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-construction — stack-based progress through preorder array */ +export const MidConstruction: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Construction complete — all 7 nodes placed */ +export const ConstructionComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/build-from-preorder-inorder-iterative.test.ts b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/build-from-preorder-inorder-iterative.test.ts new file mode 100644 index 00000000..3527e9a5 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/build-from-preorder-inorder-iterative.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; +import { buildFromPreorderInorderIterative } from "./sources/build-from-preorder-inorder-iterative.ts?fn"; + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function inorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...inorder(root.left), root.value, ...inorder(root.right)]; +} + +function preorder(root: TreeNode | null): number[] { + if (!root) return []; + return [root.value, ...preorder(root.left), ...preorder(root.right)]; +} + +describe("buildFromPreorderInorderIterative", () => { + it("builds a balanced 7-node BST", () => { + const root = buildFromPreorderInorderIterative( + [4, 2, 1, 3, 6, 5, 7], + [1, 2, 3, 4, 5, 6, 7], + ) as TreeNode; + expect(root.value).toBe(4); + expect(inorder(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("preserves inorder traversal", () => { + const inputInorder = [1, 2, 3, 4, 5, 6, 7]; + const root = buildFromPreorderInorderIterative([4, 2, 1, 3, 6, 5, 7], inputInorder) as TreeNode; + expect(inorder(root)).toEqual(inputInorder); + }); + + it("preserves preorder traversal", () => { + const inputPreorder = [4, 2, 1, 3, 6, 5, 7]; + const root = buildFromPreorderInorderIterative( + inputPreorder, + [1, 2, 3, 4, 5, 6, 7], + ) as TreeNode; + expect(preorder(root)).toEqual(inputPreorder); + }); + + it("returns null for empty arrays", () => { + expect(buildFromPreorderInorderIterative([], [])).toBeNull(); + }); + + it("handles a single-node tree", () => { + const root = buildFromPreorderInorderIterative([42], [42]) as TreeNode; + expect(root.value).toBe(42); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("matches result of recursive approach for various inputs", () => { + const root1 = buildFromPreorderInorderIterative([1, 2, 3], [2, 1, 3]) as TreeNode; + expect(root1.value).toBe(1); + expect((root1.left as TreeNode).value).toBe(2); + expect((root1.right as TreeNode).value).toBe(3); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/educational.ts b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/educational.ts new file mode 100644 index 00000000..3934c60a --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/educational.ts @@ -0,0 +1,52 @@ +import type { EducationalContent } from "@/types"; + +export const buildFromPreorderInorderIterativeEducational: EducationalContent = { + overview: + "**Build Tree from Preorder + Inorder (Iterative)** reconstructs a binary tree using a stack instead of recursion. " + + "It simulates the call stack of the recursive approach, consuming preorder values one by one and using the inorder sequence to determine when to switch from left-child insertion to right-child insertion.", + + howItWorks: + "The stack-based algorithm processes preorder elements left to right:\n\n" + + "1. **Initialize** — create the root from `preorder[0]` and push it onto the stack.\n" + + "2. **For each subsequent preorder value:**\n" + + " - Peek at the stack top.\n" + + " - If the stack top's value **does not** match the current inorder element, attach the new node as the **left child** of the top.\n" + + " - Otherwise, **pop** nodes from the stack as long as they match the inorder sequence (moving the inorder pointer forward). The last popped node becomes the parent for a **right child**.\n" + + "3. Always **push** the new node onto the stack.\n\n" + + "### Key Insight\n\n" + + "The inorder pointer acts as a boundary detector. When the stack top equals `inorder[pointer]`, we've finished the left subtree of that node and must start building its right subtree.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each of the `n` nodes is pushed and popped from the stack at most once, and each preorder element is processed exactly once.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The stack holds at most `O(h)` nodes where `h` is the tree height. In the worst case (skewed tree), `h = O(n)`.", + + bestAndWorstCase: + "**Best case** is a balanced tree — the stack depth stays at `O(log n)` and alternation between left and right insertions is frequent.\n\n" + + "**Worst case** is a right-skewed tree — every node is pushed without being popped until the very end, giving `O(n)` stack depth.", + + realWorldUses: [ + "**Stack-overflow prevention:** Converts the recursive approach to an iterative one that is safe for very deep trees.", + "**Streaming tree reconstruction:** Suitable for processing traversal sequences as a data stream.", + "**Embedded systems:** Iterative tree construction avoids call-stack limitations on memory-constrained platforms.", + "**Compiler backends:** Reconstructing parse trees from serialized traversal sequences without deep recursion.", + ], + + strengthsAndLimitations: { + strengths: [ + "Eliminates recursion — no risk of call-stack overflow even for deeply skewed trees.", + "O(n) time and O(n) space with no hash map needed for the core algorithm.", + "Mirrors the recursive logic closely — straightforward to derive from the recursive version.", + ], + limitations: [ + "Requires both preorder and inorder arrays with all distinct values.", + "The stack management logic is less intuitive than the recursive version.", + "Both sequences must be complete — partial or corrupted sequences yield incorrect results.", + ], + }, + + whenToUseIt: + "Prefer the iterative version over the recursive one when input trees may be very deep (e.g., degenerate linked-list-shaped trees) " + + "and call-stack depth is a concern. In most practical cases, the recursive version with a hash map is simpler and equally fast.", +}; diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/index.ts b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/index.ts new file mode 100644 index 00000000..879eab1a --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/index.ts @@ -0,0 +1,54 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { buildFromPreorderInorderIterative } from "./sources/build-from-preorder-inorder-iterative.ts?fn"; +import { generateBuildFromPreorderInorderIterativeSteps } from "./step-generator"; +import type { BuildFromPreorderInorderIterativeInput } from "./step-generator"; +import { buildFromPreorderInorderIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/build-from-preorder-inorder-iterative.ts?raw"; +import pythonSource from "./sources/build-from-preorder-inorder-iterative.py?raw"; +import javaSource from "./sources/BuildFromPreorderInorderIterative.java?raw"; + +function executeBuildFromPreorderInorderIterative( + input: BuildFromPreorderInorderIterativeInput, +): number | null { + const result = buildFromPreorderInorderIterative(input.preorder, input.inorder) as { + value: number; + } | null; + return result ? result.value : null; +} + +const buildFromPreorderInorderIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.BUILD_FROM_PREORDER_INORDER_ITERATIVE!, + name: "Build Tree: Preorder + Inorder (Iterative)", + category: CATEGORY.TREES!, + technique: "construction", + description: + "Reconstructs a binary tree from preorder and inorder traversals using an explicit stack instead of recursion", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + preorder: [4, 2, 1, 3, 6, 5, 7], + inorder: [1, 2, 3, 4, 5, 6, 7], + }, + }, + execute: executeBuildFromPreorderInorderIterative, + generateSteps: generateBuildFromPreorderInorderIterativeSteps, + educational: buildFromPreorderInorderIterativeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, + }; + +registry.register(buildFromPreorderInorderIterativeDefinition); diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/BuildFromPreorderInorderIterative.java b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/BuildFromPreorderInorderIterative.java new file mode 100644 index 00000000..7227b9ab --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/BuildFromPreorderInorderIterative.java @@ -0,0 +1,43 @@ +// Build Binary Tree from Preorder + Inorder (Iterative with Stack) +// Uses a stack to simulate recursion — push nodes as we consume preorder values, +// pop when we detect a boundary via the inorder pointer. +import java.util.Stack; + +class TreeNode { + int value; + TreeNode left, right; + TreeNode(int value) { this.value = value; } +} + +class BuildFromPreorderInorderIterative { + public TreeNode buildFromPreorderInorderIterative(int[] preorder, int[] inorder) { + if (preorder.length == 0) return null; // @step:initialize + + TreeNode root = new TreeNode(preorder[0]); // @step:build-node + Stack stack = new Stack<>(); // @step:initialize + stack.push(root); // @step:initialize + int inorderPointer = 0; // @step:initialize + + for (int preorderPointer = 1; preorderPointer < preorder.length; preorderPointer++) { // @step:select-element + int currentValue = preorder[preorderPointer]; // @step:select-element + TreeNode parentNode = stack.peek(); // @step:search-node + TreeNode newNode = new TreeNode(currentValue); // @step:build-node + + // If top of stack does not match inorder, attach as left child + if (parentNode.value != inorder[inorderPointer]) { + parentNode.left = newNode; // @step:connect-child + } else { + // Pop matching nodes to find the parent for right child + while (!stack.isEmpty() && stack.peek().value == inorder[inorderPointer]) { // @step:partition-array + parentNode = stack.pop(); // @step:partition-array + inorderPointer++; // @step:partition-array + } + parentNode.right = newNode; // @step:connect-child + } + + stack.push(newNode); // @step:visit + } + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/build-from-preorder-inorder-iterative.py b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/build-from-preorder-inorder-iterative.py new file mode 100644 index 00000000..39257cc5 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/build-from-preorder-inorder-iterative.py @@ -0,0 +1,36 @@ +# Build Binary Tree from Preorder + Inorder (Iterative with Stack) +# Uses a stack to simulate recursion — push nodes as we consume preorder values, +# pop when we detect a boundary via the inorder pointer. + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def build_from_preorder_inorder_iterative(preorder: list, inorder: list): + if not preorder: # @step:initialize + return None + + root = TreeNode(preorder[0]) # @step:build-node + stack = [root] # @step:initialize + inorder_pointer = 0 # @step:initialize + + for preorder_pointer in range(1, len(preorder)): # @step:select-element + current_value = preorder[preorder_pointer] # @step:select-element + parent_node = stack[-1] # @step:search-node + new_node = TreeNode(current_value) # @step:build-node + + # If top of stack doesn't match inorder, attach as left child + if parent_node.value != inorder[inorder_pointer]: + parent_node.left = new_node # @step:connect-child + else: + # Pop matching nodes to find the right-child parent + while stack and stack[-1].value == inorder[inorder_pointer]: # @step:partition-array + parent_node = stack.pop() # @step:partition-array + inorder_pointer += 1 # @step:partition-array + parent_node.right = new_node # @step:connect-child + + stack.append(new_node) # @step:visit + + return root # @step:visit diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/build-from-preorder-inorder-iterative.ts b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/build-from-preorder-inorder-iterative.ts new file mode 100644 index 00000000..fdab183f --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/sources/build-from-preorder-inorder-iterative.ts @@ -0,0 +1,48 @@ +// Build Binary Tree from Preorder + Inorder (Iterative with Stack) +// Uses a stack to simulate recursion — push nodes as we consume preorder values, +// pop when we detect a boundary via the inorder pointer. + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function buildFromPreorderInorderIterative(preorder: number[], inorder: number[]): TreeNode | null { + if (preorder.length === 0) return null; // @step:initialize + + const firstValue = preorder[0]; // @step:initialize + if (firstValue === undefined) return null; + + const root: TreeNode = { value: firstValue, left: null, right: null }; // @step:build-node + const stack: TreeNode[] = [root]; // @step:initialize + let inorderPointer = 0; // @step:initialize + + for (let preorderPointer = 1; preorderPointer < preorder.length; preorderPointer++) { + // @step:select-element + const currentValue = preorder[preorderPointer]; // @step:select-element + if (currentValue === undefined) break; + + let parentNode = stack[stack.length - 1]; // @step:search-node + if (parentNode === undefined) break; + + const newNode: TreeNode = { value: currentValue, left: null, right: null }; // @step:build-node + + // If stack top differs from current inorder value, go left + if (parentNode.value !== inorder[inorderPointer]) { + parentNode.left = newNode; // @step:connect-child + } else { + // Pop nodes that match inorder to find the parent for right insertion + while (stack.length > 0 && stack[stack.length - 1]?.value === inorder[inorderPointer]) { + // @step:partition-array + parentNode = stack.pop()!; // @step:partition-array + inorderPointer++; // @step:partition-array + } + parentNode.right = newNode; // @step:connect-child + } + + stack.push(newNode); // @step:visit + } + + return root; // @step:visit +} diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/step-generator.test.ts b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/step-generator.test.ts new file mode 100644 index 00000000..df8074fe --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/step-generator.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { generateBuildFromPreorderInorderIterativeSteps } from "./step-generator"; + +const defaultInput = { + preorder: [4, 2, 1, 3, 6, 5, 7], + inorder: [1, 2, 3, 4, 5, 6, 7], +}; + +describe("generateBuildFromPreorderInorderIterativeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("builds exactly 7 nodes", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("handles a single-element input", () => { + const steps = generateBuildFromPreorderInorderIterativeSteps({ preorder: [5], inorder: [5] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/step-generator.ts b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/step-generator.ts new file mode 100644 index 00000000..9e28caa9 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder-iterative/step-generator.ts @@ -0,0 +1,178 @@ +/** Step generator for Build Tree from Preorder + Inorder Iterative — produces ExecutionStep[] using TreeConstructionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { TreeConstructionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BUILD_FROM_PREORDER_INORDER_ITERATIVE!); + +export interface BuildFromPreorderInorderIterativeInput { + preorder: number[]; + inorder: number[]; +} + +/** Compute visual position for a node given depth and horizontal index. */ +function computePosition( + depth: number, + nodeIndex: number, + totalAtDepth: number, +): { x: number; y: number } { + const treeWidth = 400; + const levelHeight = 100; + const segmentWidth = treeWidth / Math.max(totalAtDepth, 1); + return { + x: segmentWidth * nodeIndex + segmentWidth / 2, + y: depth * levelHeight + 60, + }; +} + +export function generateBuildFromPreorderInorderIterativeSteps( + input: BuildFromPreorderInorderIterativeInput, +): ExecutionStep[] { + const { preorder, inorder } = input; + + interface StackEntry { + nodeId: string; + value: number; + depth: number; + } + + const tracker = new TreeConstructionTracker([], "", LINE_MAP); + tracker.initialize({ preorder: [...preorder], inorder: [...inorder] }); + + if (preorder.length === 0) { + tracker.complete({ totalNodes: 0 }); + return tracker.getSteps(); + } + + const firstValueRaw = preorder[0]; + if (firstValueRaw === undefined) { + tracker.complete({ totalNodes: 0 }); + return tracker.getSteps(); + } + const firstValue: number = firstValueRaw; + + let nodeCounter = 0; + const depthNodeCount: number[] = []; + + // Pre-compute how many nodes land at each depth for positioning + const nodeIdMap = new Map(); // value -> nodeId + const nodeDepthMap = new Map(); // nodeId -> depth + + function assignDepths(): void { + const tempStack: { value: number; depth: number }[] = []; + tempStack.push({ value: firstValue, depth: 0 }); + let inorderIdx = 0; + + for (let preIdx = 1; preIdx < preorder.length; preIdx++) { + const currentValue = preorder[preIdx]; + if (currentValue === undefined) break; + + const stackTop = tempStack[tempStack.length - 1]; + if (!stackTop) break; + + let parentDepth = stackTop.depth; + let newDepth: number; + + if (stackTop.value !== inorder[inorderIdx]) { + newDepth = parentDepth + 1; + tempStack.push({ value: currentValue, depth: newDepth }); + } else { + while ( + tempStack.length > 0 && + tempStack[tempStack.length - 1]?.value === inorder[inorderIdx] + ) { + const popped = tempStack.pop(); + if (popped) parentDepth = popped.depth; + inorderIdx++; + } + newDepth = parentDepth + 1; + tempStack.push({ value: currentValue, depth: newDepth }); + } + + depthNodeCount[newDepth] = (depthNodeCount[newDepth] ?? 0) + 1; + } + } + + assignDepths(); + depthNodeCount[0] = 1; + + const depthCurrentIdx: number[] = new Array(depthNodeCount.length).fill(0); + + function makeNodeId(value: number, depth: number): string { + nodeCounter++; + const nodeId = `node-${String(nodeCounter)}`; + nodeIdMap.set(value, nodeId); + nodeDepthMap.set(nodeId, depth); + const nodeIndex = depthCurrentIdx[depth] ?? 0; + depthCurrentIdx[depth] = nodeIndex + 1; + const position = computePosition(depth, nodeIndex, depthNodeCount[depth] ?? 1); + // The tracker buildNode will add the node, but we need to set position afterwards + void position; + return nodeId; + } + + const rootNodeId = makeNodeId(firstValue, 0); + tracker.selectElement(firstValue, { value: firstValue, preorderPointer: 0 }); + tracker.buildNode(rootNodeId, firstValue, { nodeId: rootNodeId, value: firstValue }); + + const stack: StackEntry[] = [{ nodeId: rootNodeId, value: firstValue, depth: 0 }]; + let inorderPointer = 0; + const parentMap = new Map(); // childId -> parentId + const sideMap = new Map(); // childId -> side + + for (let preorderPointer = 1; preorderPointer < preorder.length; preorderPointer++) { + const currentValue = preorder[preorderPointer]; + if (currentValue === undefined) break; + + tracker.selectElement(currentValue, { value: currentValue, preorderPointer }); + + const stackTop = stack[stack.length - 1]; + if (!stackTop) break; + + const newDepth = stackTop.depth + 1; + const newNodeId = makeNodeId(currentValue, newDepth); + tracker.buildNode(newNodeId, currentValue, { nodeId: newNodeId, value: currentValue }); + + if (stackTop.value !== inorder[inorderPointer]) { + // Left child + parentMap.set(newNodeId, stackTop.nodeId); + sideMap.set(newNodeId, "left"); + tracker.connectChild(stackTop.nodeId, newNodeId, "left", { + parentId: stackTop.nodeId, + childId: newNodeId, + side: "left", + }); + stack.push({ nodeId: newNodeId, value: currentValue, depth: newDepth }); + } else { + // Pop stack to find right-child parent + let lastPopped: StackEntry | undefined; + while (stack.length > 0 && stack[stack.length - 1]?.value === inorder[inorderPointer]) { + lastPopped = stack.pop(); + tracker.partitionArray(inorderPointer, inorderPointer, inorderPointer, { + inorderPointer, + poppedValue: lastPopped?.value, + }); + inorderPointer++; + } + if (lastPopped) { + parentMap.set(newNodeId, lastPopped.nodeId); + sideMap.set(newNodeId, "right"); + tracker.connectChild(lastPopped.nodeId, newNodeId, "right", { + parentId: lastPopped.nodeId, + childId: newNodeId, + side: "right", + }); + } + stack.push({ nodeId: newNodeId, value: currentValue, depth: newDepth }); + } + + tracker.markBuilt(newNodeId, { nodeId: newNodeId, value: currentValue }); + } + + tracker.markBuilt(rootNodeId, { nodeId: rootNodeId, value: firstValue }); + tracker.complete({ totalNodes: nodeCounter }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/BuildFromPreorderInorderPipeline.stories.tsx b/src/algorithms/trees/construction/build-from-preorder-inorder/BuildFromPreorderInorderPipeline.stories.tsx new file mode 100644 index 00000000..3ee13a75 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/BuildFromPreorderInorderPipeline.stories.tsx @@ -0,0 +1,50 @@ +/** + * Storybook stories for the Build Tree from Preorder + Inorder algorithm pipeline. + * Uses the real step generator with a 7-node balanced BST, + * rendering the TreeVisualizer at key construction states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState } from "@/types"; +import { generateBuildFromPreorderInorderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const steps = generateBuildFromPreorderInorderSteps({ + preorder: [4, 2, 1, 3, 6, 5, 7], + inorder: [1, 2, 3, 4, 5, 6, 7], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Build Tree: Preorder + Inorder", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty tree before construction begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-construction with some nodes built */ +export const MidConstruction: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Construction complete — all 7 nodes placed */ +export const ConstructionComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/build-from-preorder-inorder.test.ts b/src/algorithms/trees/construction/build-from-preorder-inorder/build-from-preorder-inorder.test.ts new file mode 100644 index 00000000..3be67ee3 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/build-from-preorder-inorder.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "vitest"; +import { buildFromPreorderInorder } from "./sources/build-from-preorder-inorder.ts?fn"; + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function inorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...inorder(root.left), root.value, ...inorder(root.right)]; +} + +function preorder(root: TreeNode | null): number[] { + if (!root) return []; + return [root.value, ...preorder(root.left), ...preorder(root.right)]; +} + +describe("buildFromPreorderInorder", () => { + it("builds a balanced 7-node BST", () => { + const root = buildFromPreorderInorder([4, 2, 1, 3, 6, 5, 7], [1, 2, 3, 4, 5, 6, 7]) as TreeNode; + expect(root.value).toBe(4); + expect(inorder(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("preserves original inorder traversal", () => { + const inputInorder = [1, 2, 3, 4, 5, 6, 7]; + const root = buildFromPreorderInorder([4, 2, 1, 3, 6, 5, 7], inputInorder) as TreeNode; + expect(inorder(root)).toEqual(inputInorder); + }); + + it("preserves original preorder traversal", () => { + const inputPreorder = [4, 2, 1, 3, 6, 5, 7]; + const root = buildFromPreorderInorder(inputPreorder, [1, 2, 3, 4, 5, 6, 7]) as TreeNode; + expect(preorder(root)).toEqual(inputPreorder); + }); + + it("returns null for empty arrays", () => { + expect(buildFromPreorderInorder([], [])).toBeNull(); + }); + + it("handles a single-node tree", () => { + const root = buildFromPreorderInorder([42], [42]) as TreeNode; + expect(root.value).toBe(42); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("builds a right-skewed tree", () => { + const root = buildFromPreorderInorder([1, 2, 3], [1, 2, 3]) as TreeNode; + expect(root.value).toBe(1); + expect(root.left).toBeNull(); + expect((root.right as TreeNode).value).toBe(2); + }); + + it("builds a left-skewed tree", () => { + const root = buildFromPreorderInorder([3, 2, 1], [1, 2, 3]) as TreeNode; + expect(root.value).toBe(3); + expect(root.right).toBeNull(); + expect((root.left as TreeNode).value).toBe(2); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/educational.ts b/src/algorithms/trees/construction/build-from-preorder-inorder/educational.ts new file mode 100644 index 00000000..276b3df8 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/educational.ts @@ -0,0 +1,60 @@ +import type { EducationalContent } from "@/types"; + +export const buildFromPreorderInorderEducational: EducationalContent = { + overview: + "**Build Tree from Preorder + Inorder** reconstructs a unique binary tree from two traversal sequences. " + + "The **preorder** array always starts with the current subtree's root. The **inorder** array reveals which elements belong to the left vs right subtree by locating that root value.", + + howItWorks: + "The algorithm is recursive and follows three steps per call:\n\n" + + "1. **Identify root** — the first element of the current preorder slice is the subtree root.\n" + + "2. **Partition inorder** — find the root's position in the inorder array. Everything to its left belongs to the left subtree; everything to its right to the right subtree.\n" + + "3. **Recurse** — apply the same logic to the left and right slices, progressively building child nodes.\n\n" + + "### Example\n\n" + + "```\n" + + "preorder = [4, 2, 1, 3, 6, 5, 7]\n" + + "inorder = [1, 2, 3, 4, 5, 6, 7]\n\n" + + "Root = 4 (preorder[0])\n" + + "Inorder index of 4 = 3 → left subtree has 3 nodes [1,2,3], right has 3 [5,6,7]\n" + + "Recurse left: preorder=[2,1,3], inorder=[1,2,3] → root=2\n" + + "Recurse right: preorder=[6,5,7], inorder=[5,6,7] → root=6\n" + + "```\n\n" + + "Base case: an empty preorder or inorder slice returns `null`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n²)` naive, `O(n)` with hash map**\n\n" + + "Without a lookup table, finding the root in the inorder array takes `O(n)` per call, leading to `O(n²)` overall. " + + "With an index map precomputed in `O(n)`, each lookup is `O(1)` and total time is `O(n)`.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The recursion stack reaches depth `O(h)` where `h` is the tree height. The index map uses `O(n)`. " + + "For a balanced tree `h = O(log n)`; for a degenerate tree `h = O(n)`.", + + bestAndWorstCase: + "**Best case** is a perfectly balanced tree — recursion depth is `O(log n)` and the tree is evenly divided at each step.\n\n" + + "**Worst case** is a completely skewed tree (e.g., all left children) — recursion depth reaches `O(n)` and the stack may overflow for very large inputs.", + + realWorldUses: [ + "**Serialization/deserialization:** Reconstruct a tree sent over a network or stored as traversal sequences.", + "**Compiler parse trees:** Recover abstract syntax trees from pre-computed traversal representations.", + "**Competitive programming:** A classic reconstruction problem that validates understanding of tree invariants.", + "**Database checkpointing:** Restore balanced index trees from logged traversal snapshots.", + ], + + strengthsAndLimitations: { + strengths: [ + "Uniquely reconstructs the original tree — given preorder + inorder, only one tree is possible.", + "Elegant divide-and-conquer structure mirrors the tree's natural recursive shape.", + "Hash map optimization brings time complexity down to O(n).", + ], + limitations: [ + "Requires both traversal sequences — neither alone is sufficient for unique reconstruction.", + "All node values must be distinct; duplicate values make reconstruction ambiguous.", + "Deep recursion on skewed trees risks stack overflow without an iterative conversion.", + ], + }, + + whenToUseIt: + "Use this algorithm whenever you have both a preorder and inorder traversal of a tree and need to recover its structure. " + + "If you only have one traversal, the tree cannot be uniquely determined. " + + "For large trees, apply the hash map optimization to achieve O(n) time.", +}; diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/index.ts b/src/algorithms/trees/construction/build-from-preorder-inorder/index.ts new file mode 100644 index 00000000..a8b0aa9b --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/index.ts @@ -0,0 +1,54 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { buildFromPreorderInorder } from "./sources/build-from-preorder-inorder.ts?fn"; +import { generateBuildFromPreorderInorderSteps } from "./step-generator"; +import type { BuildFromPreorderInorderInput } from "./step-generator"; +import { buildFromPreorderInorderEducational } from "./educational"; + +import typescriptSource from "./sources/build-from-preorder-inorder.ts?raw"; +import pythonSource from "./sources/build-from-preorder-inorder.py?raw"; +import javaSource from "./sources/BuildFromPreorderInorder.java?raw"; + +/** Execute the pure algorithm and return the serialized root value or null */ +function executeBuildFromPreorderInorder(input: BuildFromPreorderInorderInput): number | null { + const result = buildFromPreorderInorder(input.preorder, input.inorder) as { + value: number; + left: unknown; + right: unknown; + } | null; + return result ? result.value : null; +} + +const buildFromPreorderInorderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BUILD_FROM_PREORDER_INORDER!, + name: "Build Tree: Preorder + Inorder", + category: CATEGORY.TREES!, + technique: "construction", + description: + "Reconstructs a unique binary tree from its preorder and inorder traversal sequences using recursive divide-and-conquer", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n²)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + preorder: [4, 2, 1, 3, 6, 5, 7], + inorder: [1, 2, 3, 4, 5, 6, 7], + }, + }, + execute: executeBuildFromPreorderInorder, + generateSteps: generateBuildFromPreorderInorderSteps, + educational: buildFromPreorderInorderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(buildFromPreorderInorderDefinition); diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/sources/BuildFromPreorderInorder.java b/src/algorithms/trees/construction/build-from-preorder-inorder/sources/BuildFromPreorderInorder.java new file mode 100644 index 00000000..bf368f67 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/sources/BuildFromPreorderInorder.java @@ -0,0 +1,38 @@ +// Build Binary Tree from Preorder + Inorder Traversal (Recursive) +// First element of preorder is root; find root in inorder to split left/right subtrees + +class TreeNode { + int value; + TreeNode left, right; + TreeNode(int value) { this.value = value; } +} + +class BuildFromPreorderInorder { + public TreeNode buildFromPreorderInorder(int[] preorder, int[] inorder) { + return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1); // @step:initialize + } + + private TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + if (preStart > preEnd || inStart > inEnd) return null; // @step:initialize + + int rootValue = preorder[preStart]; // @step:select-element + TreeNode root = new TreeNode(rootValue); // @step:build-node + + // Find root position in inorder array + int inorderRootIndex = inStart; // @step:partition-array + while (inorderRootIndex <= inEnd && inorder[inorderRootIndex] != rootValue) { + inorderRootIndex++; // @step:partition-array + } + + int leftSubtreeSize = inorderRootIndex - inStart; // @step:partition-array + + // Recurse for left and right subtrees + root.left = build(preorder, preStart + 1, preStart + leftSubtreeSize, + inorder, inStart, inorderRootIndex - 1); // @step:connect-child + root.right = build(preorder, preStart + leftSubtreeSize + 1, preEnd, + inorder, inorderRootIndex + 1, inEnd); // @step:connect-child + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/sources/build-from-preorder-inorder.py b/src/algorithms/trees/construction/build-from-preorder-inorder/sources/build-from-preorder-inorder.py new file mode 100644 index 00000000..fe38fa53 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/sources/build-from-preorder-inorder.py @@ -0,0 +1,29 @@ +# Build Binary Tree from Preorder + Inorder Traversal (Recursive) +# First element of preorder is root; find root in inorder to split left/right subtrees + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def build_from_preorder_inorder(preorder: list, inorder: list): + if not preorder or not inorder: # @step:initialize + return None + + root_value = preorder[0] # @step:select-element + root = TreeNode(root_value) # @step:build-node + + inorder_root_index = inorder.index(root_value) # @step:partition-array + + # Split inorder and preorder into left/right sub-arrays + left_inorder = inorder[:inorder_root_index] # @step:partition-array + left_preorder = preorder[1:1 + len(left_inorder)] # @step:partition-array + + right_inorder = inorder[inorder_root_index + 1:] # @step:partition-array + right_preorder = preorder[1 + len(left_inorder):] # @step:partition-array + + root.left = build_from_preorder_inorder(left_preorder, left_inorder) # @step:connect-child + root.right = build_from_preorder_inorder(right_preorder, right_inorder) # @step:connect-child + + return root # @step:visit diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/sources/build-from-preorder-inorder.ts b/src/algorithms/trees/construction/build-from-preorder-inorder/sources/build-from-preorder-inorder.ts new file mode 100644 index 00000000..52836e51 --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/sources/build-from-preorder-inorder.ts @@ -0,0 +1,32 @@ +// Build Binary Tree from Preorder + Inorder Traversal (Recursive) +// First element of preorder is root; find root in inorder to split left/right subtrees + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function buildFromPreorderInorder(preorder: number[], inorder: number[]): TreeNode | null { + if (preorder.length === 0 || inorder.length === 0) return null; // @step:initialize + + const rootValue = preorder[0]; // @step:select-element + if (rootValue === undefined) return null; + + const root: TreeNode = { value: rootValue, left: null, right: null }; // @step:build-node + + const inorderRootIndex = inorder.indexOf(rootValue); // @step:partition-array + + // Left subtree uses inorder[0..inorderRootIndex-1] and corresponding preorder slice + const leftInorder = inorder.slice(0, inorderRootIndex); // @step:partition-array + const leftPreorder = preorder.slice(1, 1 + leftInorder.length); // @step:partition-array + + // Right subtree uses inorder[inorderRootIndex+1..] and the remaining preorder elements + const rightInorder = inorder.slice(inorderRootIndex + 1); // @step:partition-array + const rightPreorder = preorder.slice(1 + leftInorder.length); // @step:partition-array + + root.left = buildFromPreorderInorder(leftPreorder, leftInorder); // @step:connect-child + root.right = buildFromPreorderInorder(rightPreorder, rightInorder); // @step:connect-child + + return root; // @step:visit +} diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/step-generator.test.ts b/src/algorithms/trees/construction/build-from-preorder-inorder/step-generator.test.ts new file mode 100644 index 00000000..98ad782e --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/step-generator.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { generateBuildFromPreorderInorderSteps } from "./step-generator"; + +const defaultInput = { + preorder: [4, 2, 1, 3, 6, 5, 7], + inorder: [1, 2, 3, 4, 5, 6, 7], +}; + +describe("generateBuildFromPreorderInorderSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateBuildFromPreorderInorderSteps(defaultInput); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBuildFromPreorderInorderSteps(defaultInput); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBuildFromPreorderInorderSteps(defaultInput); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateBuildFromPreorderInorderSteps(defaultInput); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("builds exactly 7 nodes", () => { + const steps = generateBuildFromPreorderInorderSteps(defaultInput); + const buildSteps = steps.filter((step) => step.type === "build-node"); + expect(buildSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateBuildFromPreorderInorderSteps(defaultInput); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("handles a single-element input", () => { + const steps = generateBuildFromPreorderInorderSteps({ preorder: [1], inorder: [1] }); + expect(steps.length).toBeGreaterThan(0); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/trees/construction/build-from-preorder-inorder/step-generator.ts b/src/algorithms/trees/construction/build-from-preorder-inorder/step-generator.ts new file mode 100644 index 00000000..9b88776f --- /dev/null +++ b/src/algorithms/trees/construction/build-from-preorder-inorder/step-generator.ts @@ -0,0 +1,99 @@ +/** Step generator for Build Tree from Preorder + Inorder — produces ExecutionStep[] using TreeConstructionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { TreeConstructionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BUILD_FROM_PREORDER_INORDER!); + +export interface BuildFromPreorderInorderInput { + preorder: number[]; + inorder: number[]; +} + +export function generateBuildFromPreorderInorderSteps( + input: BuildFromPreorderInorderInput, +): ExecutionStep[] { + const { preorder, inorder } = input; + + // Pre-compute inorder index map for O(1) lookup + const inorderIndexMap = new Map(); + inorder.forEach((value, idx) => inorderIndexMap.set(value, idx)); + + const tracker = new TreeConstructionTracker([], "", LINE_MAP); + + tracker.initialize({ preorder: [...preorder], inorder: [...inorder] }); + + let nodeCounter = 0; + + function buildTree( + preStart: number, + preEnd: number, + inStart: number, + inEnd: number, + depth: number, + parentId: string | null, + side: "left" | "right" | null, + ): string | null { + if (preStart > preEnd || inStart > inEnd) return null; + + const rootValue = preorder[preStart]; + if (rootValue === undefined) return null; + + tracker.selectElement(rootValue, { + rootValue, + preStart, + preEnd, + inStart, + inEnd, + }); + + nodeCounter++; + const nodeId = `node-${String(nodeCounter)}`; + + tracker.buildNode(nodeId, rootValue, { nodeId, value: rootValue, depth }); + + const inorderRootIndex = inorderIndexMap.get(rootValue) ?? -1; + const leftSize = inorderRootIndex - inStart; + + tracker.partitionArray(inStart, inEnd, inorderRootIndex, { + inorderRootIndex, + leftSize, + rightSize: inEnd - inorderRootIndex, + }); + + const leftChildId = buildTree( + preStart + 1, + preStart + leftSize, + inStart, + inorderRootIndex - 1, + depth + 1, + nodeId, + "left", + ); + const rightChildId = buildTree( + preStart + leftSize + 1, + preEnd, + inorderRootIndex + 1, + inEnd, + depth + 1, + nodeId, + "right", + ); + + if (parentId && side) { + tracker.connectChild(parentId, nodeId, side, { parentId, childId: nodeId, side }); + } + + tracker.markBuilt(nodeId, { nodeId, value: rootValue, leftChildId, rightChildId }); + + return nodeId; + } + + buildTree(0, preorder.length - 1, 0, inorder.length - 1, 0, null, null); + + tracker.complete({ totalNodes: nodeCounter }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/SerializeDeserializeTreePipeline.stories.tsx b/src/algorithms/trees/construction/serialize-deserialize-tree/SerializeDeserializeTreePipeline.stories.tsx new file mode 100644 index 00000000..69aea2d1 --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/SerializeDeserializeTreePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Serialize & Deserialize Binary Tree pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key states (serialize then deserialize phases). + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSerializeDeserializeTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Serialize & Deserialize Binary Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before serialization begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-construction — during deserialization phase */ +export const MidConstruction: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Construction complete — tree fully deserialized */ +export const ConstructionComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/educational.ts b/src/algorithms/trees/construction/serialize-deserialize-tree/educational.ts new file mode 100644 index 00000000..4c104297 --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/educational.ts @@ -0,0 +1,63 @@ +import type { EducationalContent } from "@/types"; + +export const serializeDeserializeTreeEducational: EducationalContent = { + overview: + "**Serialize and Deserialize a Binary Tree** converts a binary tree into a compact string representation (serialization) " + + "and reconstructs the exact same tree from that string (deserialization). " + + "This BFS-based approach uses level-order traversal, encoding `null` pointers explicitly so the structure is fully recoverable.", + + howItWorks: + "**Serialization** (BFS):\n\n" + + "1. Push the root onto a queue.\n" + + "2. Dequeue each node: if non-null, record its value and enqueue its left and right children (even if null).\n" + + '3. If null, record the string `"null"`.\n' + + "4. Join all recorded values with commas to form the serialized string.\n\n" + + "**Deserialization**:\n\n" + + "1. Split the string on commas.\n" + + "2. Create the root from the first value, push it on a queue.\n" + + "3. For each dequeued node, consume the next two values from the list as left and right children, " + + "creating nodes for non-null values and pushing them on the queue.\n\n" + + "### Example\n\n" + + "```\n" + + 'Serialize: [4, 2, 6, 1, 3, 5, 7] → "4,2,6,1,3,5,7"\n' + + "(null children omitted at leaf level for brevity)\n" + + 'Deserialize: "4,2,6,1,3,5,7" → balanced 7-node BST\n' + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)` for both serialization and deserialization**\n\n" + + "Every node is visited exactly once during BFS. Splitting and joining the string is also `O(n)`.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The BFS queue holds at most `O(n)` entries for a complete tree (one full level). " + + "The serialized string itself is `O(n)` characters.", + + bestAndWorstCase: + "**Best case** is a compact, complete binary tree — no trailing null tokens needed at the leaf level, minimizing string length.\n\n" + + "**Worst case** is a sparse tree or a skewed tree where every level has only one real node — " + + "the serialized string can contain up to `O(2^h)` null tokens for a tree of height `h`.", + + realWorldUses: [ + "**Network transmission:** Send an entire tree structure across a network as a flat string with lossless reconstruction.", + "**Persistent storage:** Store trees in databases or files that do not support hierarchical data natively.", + "**Deep copying:** Serialize then deserialize as a simple way to deep-clone a tree.", + "**Distributed systems:** Pass tree configurations between microservices as portable serialized strings.", + ], + + strengthsAndLimitations: { + strengths: [ + "Lossless round-trip — serialize → deserialize always reconstructs the original tree exactly.", + "Handles arbitrary binary trees including unbalanced and sparse trees.", + "BFS order makes the serialized string human-readable and easy to debug.", + ], + limitations: [ + "Sparse trees produce long strings with many null tokens, increasing storage cost.", + "Not space-optimal compared to preorder-based serialization that omits null tokens.", + "Integer overflow possible for very large node values without size guards.", + ], + }, + + whenToUseIt: + "Use BFS serialization when you need a self-contained, portable representation of any binary tree. " + + "For space-constrained scenarios with complete or nearly complete trees, consider preorder serialization with explicit null markers. " + + "For trees with duplicate values, include node IDs in the serialized format.", +}; diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/index.ts b/src/algorithms/trees/construction/serialize-deserialize-tree/index.ts new file mode 100644 index 00000000..7b9869ab --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/index.ts @@ -0,0 +1,136 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { serializeTree } from "./sources/serialize-deserialize-tree.ts?fn"; +import { generateSerializeDeserializeTreeSteps } from "./step-generator"; +import type { SerializeDeserializeTreeInput } from "./step-generator"; +import { serializeDeserializeTreeEducational } from "./educational"; + +import typescriptSource from "./sources/serialize-deserialize-tree.ts?raw"; +import pythonSource from "./sources/serialize-deserialize-tree.py?raw"; +import javaSource from "./sources/SerializeDeserializeTree.java?raw"; + +/** Build a balanced 7-node BST for default input */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Convert our TreeNode[] + rootId into the linked node structure expected by the pure function */ +function executeSerializeDeserializeTree(input: SerializeDeserializeTreeInput): string { + interface PureNode { + value: number; + left: PureNode | null; + right: PureNode | null; + } + + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + + function toPureNode(id: string | null): PureNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toPureNode(node.leftChildId), + right: toPureNode(node.rightChildId), + }; + } + + const root = toPureNode(input.rootId); + return serializeTree(root) as string; +} + +const serializeDeserializeTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SERIALIZE_DESERIALIZE_TREE!, + name: "Serialize & Deserialize Binary Tree", + category: CATEGORY.TREES!, + technique: "construction", + description: + "Converts a binary tree to a level-order string representation and reconstructs the identical tree from that string", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeSerializeDeserializeTree, + generateSteps: generateSerializeDeserializeTreeSteps, + educational: serializeDeserializeTreeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(serializeDeserializeTreeDefinition); diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/serialize-deserialize-tree.test.ts b/src/algorithms/trees/construction/serialize-deserialize-tree/serialize-deserialize-tree.test.ts new file mode 100644 index 00000000..0f2f63ec --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/serialize-deserialize-tree.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from "vitest"; +import { serializeTree, deserializeTree } from "./sources/serialize-deserialize-tree.ts?fn"; + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function node( + value: number, + left: TreeNode | null = null, + right: TreeNode | null = null, +): TreeNode { + return { value, left, right }; +} + +function inorder(root: TreeNode | null): number[] { + if (!root) return []; + return [...inorder(root.left), root.value, ...inorder(root.right)]; +} + +describe("serializeTree", () => { + it("serializes a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const result = serializeTree(root) as string; + expect(result).toContain("4"); + expect(result).toContain("2"); + expect(result).toContain("6"); + }); + + it("serializes null as 'null'", () => { + expect(serializeTree(null)).toBe("null"); + }); + + it("serializes a single-node tree", () => { + const result = serializeTree(node(42)) as string; + expect(result).toContain("42"); + }); +}); + +describe("deserializeTree", () => { + it("deserializes null string to null", () => { + expect(deserializeTree("null")).toBeNull(); + }); + + it("round-trips a balanced 7-node BST", () => { + const original = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const serialized = serializeTree(original) as string; + const reconstructed = deserializeTree(serialized) as TreeNode; + expect(inorder(reconstructed)).toEqual([1, 2, 3, 4, 5, 6, 7]); + expect(reconstructed.value).toBe(4); + }); + + it("round-trips a single-node tree", () => { + const original = node(99); + const serialized = serializeTree(original) as string; + const reconstructed = deserializeTree(serialized) as TreeNode; + expect(reconstructed.value).toBe(99); + expect(reconstructed.left).toBeNull(); + expect(reconstructed.right).toBeNull(); + }); + + it("round-trips a right-skewed tree", () => { + const original = node(1, null, node(2, null, node(3))); + const serialized = serializeTree(original) as string; + const reconstructed = deserializeTree(serialized) as TreeNode; + expect(inorder(reconstructed)).toEqual([1, 2, 3]); + }); + + it("round-trips a left-skewed tree", () => { + const original = node(3, node(2, node(1))); + const serialized = serializeTree(original) as string; + const reconstructed = deserializeTree(serialized) as TreeNode; + expect(inorder(reconstructed)).toEqual([1, 2, 3]); + }); +}); diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/sources/SerializeDeserializeTree.java b/src/algorithms/trees/construction/serialize-deserialize-tree/sources/SerializeDeserializeTree.java new file mode 100644 index 00000000..a1b92afe --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/sources/SerializeDeserializeTree.java @@ -0,0 +1,67 @@ +// Serialize and Deserialize Binary Tree (BFS / Level-Order) +// Serialization: BFS level-by-level, null nodes represented as "null" +// Deserialization: parse the string back into a tree using a queue +import java.util.LinkedList; +import java.util.Queue; + +class TreeNode { + int value; + TreeNode left, right; + TreeNode(int value) { this.value = value; } +} + +class SerializeDeserializeTree { + public String serializeTree(TreeNode root) { + if (root == null) return "null"; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.add(root); // @step:initialize + StringBuilder result = new StringBuilder(); // @step:initialize + + while (!queue.isEmpty()) { // @step:search-node + TreeNode node = queue.poll(); // @step:search-node + + if (node == null) { + result.append("null,"); // @step:visit + } else { + result.append(node.value).append(","); // @step:visit + queue.add(node.left); // @step:build-node + queue.add(node.right); // @step:build-node + } + } + + return result.toString(); // @step:complete + } + + public TreeNode deserializeTree(String data) { + if (data.equals("null") || data.isEmpty()) return null; // @step:initialize + + String[] parts = data.split(","); // @step:initialize + TreeNode root = new TreeNode(Integer.parseInt(parts[0])); // @step:build-node + Queue queue = new LinkedList<>(); // @step:initialize + queue.add(root); // @step:initialize + int partIndex = 1; // @step:initialize + + while (!queue.isEmpty() && partIndex < parts.length) { // @step:search-node + TreeNode currentNode = queue.poll(); // @step:search-node + + String leftValue = parts[partIndex++]; // @step:select-element + if (!leftValue.equals("null")) { + TreeNode leftNode = new TreeNode(Integer.parseInt(leftValue)); // @step:build-node + currentNode.left = leftNode; // @step:connect-child + queue.add(leftNode); // @step:visit + } + + if (partIndex < parts.length) { + String rightValue = parts[partIndex++]; // @step:select-element + if (!rightValue.equals("null")) { + TreeNode rightNode = new TreeNode(Integer.parseInt(rightValue)); // @step:build-node + currentNode.right = rightNode; // @step:connect-child + queue.add(rightNode); // @step:visit + } + } + } + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/sources/serialize-deserialize-tree.py b/src/algorithms/trees/construction/serialize-deserialize-tree/sources/serialize-deserialize-tree.py new file mode 100644 index 00000000..99a1dfd5 --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/sources/serialize-deserialize-tree.py @@ -0,0 +1,61 @@ +# Serialize and Deserialize Binary Tree (BFS / Level-Order) +# Serialization: BFS level-by-level, null nodes represented as "null" +# Deserialization: parse the string back into a tree using a queue + +from collections import deque + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def serialize_tree(root) -> str: + if root is None: # @step:initialize + return "null" + + queue = deque([root]) # @step:initialize + parts = [] # @step:initialize + + while queue: # @step:search-node + node = queue.popleft() # @step:search-node + + if node is None: + parts.append("null") # @step:visit + else: + parts.append(str(node.value)) # @step:visit + queue.append(node.left) # @step:build-node + queue.append(node.right) # @step:build-node + + return ",".join(parts) # @step:complete + +def deserialize_tree(data: str): + if data == "null" or data == "": # @step:initialize + return None + + parts = data.split(",") # @step:initialize + root = TreeNode(int(parts[0])) # @step:build-node + queue = deque([root]) # @step:initialize + part_index = 1 # @step:initialize + + while queue and part_index < len(parts): # @step:search-node + current_node = queue.popleft() # @step:search-node + + left_value = parts[part_index] # @step:select-element + part_index += 1 # @step:select-element + + if left_value != "null": + left_node = TreeNode(int(left_value)) # @step:build-node + current_node.left = left_node # @step:connect-child + queue.append(left_node) # @step:visit + + if part_index < len(parts): + right_value = parts[part_index] # @step:select-element + part_index += 1 # @step:select-element + + if right_value != "null": + right_node = TreeNode(int(right_value)) # @step:build-node + current_node.right = right_node # @step:connect-child + queue.append(right_node) # @step:visit + + return root # @step:complete diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/sources/serialize-deserialize-tree.ts b/src/algorithms/trees/construction/serialize-deserialize-tree/sources/serialize-deserialize-tree.ts new file mode 100644 index 00000000..877e1100 --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/sources/serialize-deserialize-tree.ts @@ -0,0 +1,68 @@ +// Serialize and Deserialize Binary Tree (BFS / Level-Order) +// Serialization: BFS level-by-level, null nodes represented as "null" +// Deserialization: parse the string back into a tree using a queue + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function serializeTree(root: TreeNode | null): string { + if (root === null) return "null"; // @step:initialize + + const queue: (TreeNode | null)[] = [root]; // @step:initialize + const parts: string[] = []; // @step:initialize + + while (queue.length > 0) { + // @step:search-node + const node = queue.shift()!; // @step:search-node + + if (node === null) { + parts.push("null"); // @step:visit + } else { + parts.push(String(node.value)); // @step:visit + queue.push(node.left); // @step:build-node + queue.push(node.right); // @step:build-node + } + } + + return parts.join(","); // @step:complete +} + +function deserializeTree(data: string): TreeNode | null { + if (data === "null" || data === "") return null; // @step:initialize + + const parts = data.split(","); // @step:initialize + const firstValue = parts[0]; // @step:select-element + if (firstValue === undefined || firstValue === "null") return null; + + const root: TreeNode = { value: parseInt(firstValue, 10), left: null, right: null }; // @step:build-node + const queue: TreeNode[] = [root]; // @step:initialize + let partIndex = 1; // @step:initialize + + while (queue.length > 0 && partIndex < parts.length) { + // @step:search-node + const currentNode = queue.shift()!; // @step:search-node + + const leftValue = parts[partIndex]; // @step:select-element + partIndex++; // @step:select-element + + if (leftValue !== undefined && leftValue !== "null") { + const leftNode: TreeNode = { value: parseInt(leftValue, 10), left: null, right: null }; // @step:build-node + currentNode.left = leftNode; // @step:connect-child + queue.push(leftNode); // @step:visit + } + + const rightValue = parts[partIndex]; // @step:select-element + partIndex++; // @step:select-element + + if (rightValue !== undefined && rightValue !== "null") { + const rightNode: TreeNode = { value: parseInt(rightValue, 10), left: null, right: null }; // @step:build-node + currentNode.right = rightNode; // @step:connect-child + queue.push(rightNode); // @step:visit + } + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/step-generator.test.ts b/src/algorithms/trees/construction/serialize-deserialize-tree/step-generator.test.ts new file mode 100644 index 00000000..9ac67a38 --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/step-generator.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSerializeDeserializeTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateSerializeDeserializeTreeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states throughout", () => { + const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("includes both serialize and deserialize phases", () => { + const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + // Should have 2 complete steps (one for serialize, one for deserialize) + const completeSteps = steps.filter((step) => step.type === "complete"); + expect(completeSteps.length).toBeGreaterThanOrEqual(2); + }); + + it("has incrementing step indices", () => { + const steps = generateSerializeDeserializeTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/construction/serialize-deserialize-tree/step-generator.ts b/src/algorithms/trees/construction/serialize-deserialize-tree/step-generator.ts new file mode 100644 index 00000000..4c547255 --- /dev/null +++ b/src/algorithms/trees/construction/serialize-deserialize-tree/step-generator.ts @@ -0,0 +1,150 @@ +/** Step generator for Serialize/Deserialize Binary Tree — produces ExecutionStep[] using TreeConstructionTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeConstructionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SERIALIZE_DESERIALIZE_TREE!); + +export interface SerializeDeserializeTreeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateSerializeDeserializeTreeSteps( + input: SerializeDeserializeTreeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + // Phase 1: Serialize via BFS + const serializeTracker = new TreeConstructionTracker([...nodes], rootId, LINE_MAP); + serializeTracker.initialize({ rootId, phase: "serialize" }); + + const serializedParts: string[] = []; + const bfsQueue: (string | null)[] = [rootId]; + + while (bfsQueue.length > 0) { + const nodeId = bfsQueue.shift(); + if (nodeId === null || nodeId === undefined) { + serializedParts.push("null"); + continue; + } + + const node = nodeMap.get(nodeId); + if (!node) { + serializedParts.push("null"); + continue; + } + + serializeTracker.selectElement(node.value, { nodeId, value: node.value, phase: "serialize" }); + serializedParts.push(String(node.value)); + serializeTracker.markBuilt(nodeId, { nodeId, serialized: serializedParts.join(",") }); + + bfsQueue.push(node.leftChildId ?? null); + bfsQueue.push(node.rightChildId ?? null); + } + + const serializedString = serializedParts.join(","); + serializeTracker.complete({ serializedString, totalNodes: nodes.length }); + + const serializeSteps = serializeTracker.getSteps(); + + // Phase 2: Deserialize from the serialized string + const deserializeTracker = new TreeConstructionTracker([], "", LINE_MAP); + deserializeTracker.initialize({ serializedString, phase: "deserialize" }); + + const parts = serializedString.split(","); + const firstPart = parts[0]; + + if (!firstPart || firstPart === "null") { + deserializeTracker.complete({ totalNodes: 0 }); + const earlySteps = [...serializeSteps, ...deserializeTracker.getSteps()]; + return earlySteps.map((step, combinedIndex) => ({ ...step, index: combinedIndex })); + } + + let nodeCounter = 0; + const makeNodeId = (): string => { + nodeCounter++; + return `rebuilt-${String(nodeCounter)}`; + }; + + deserializeTracker.selectElement(Number(firstPart), { value: Number(firstPart), partIndex: 0 }); + + const rootNodeId = makeNodeId(); + deserializeTracker.buildNode(rootNodeId, Number(firstPart), { + nodeId: rootNodeId, + value: Number(firstPart), + }); + + const deserializeQueue: { nodeId: string; depth: number }[] = [{ nodeId: rootNodeId, depth: 0 }]; + let partIndex = 1; + + while (deserializeQueue.length > 0 && partIndex < parts.length) { + const current = deserializeQueue.shift(); + if (!current) break; + + const leftValue = parts[partIndex]; + partIndex++; + + if (leftValue !== undefined && leftValue !== "null") { + deserializeTracker.selectElement(Number(leftValue), { + value: Number(leftValue), + partIndex: partIndex - 1, + }); + const leftNodeId = makeNodeId(); + const leftDepth = current.depth + 1; + deserializeTracker.buildNode(leftNodeId, Number(leftValue), { + nodeId: leftNodeId, + value: Number(leftValue), + depth: leftDepth, + }); + deserializeTracker.connectChild(current.nodeId, leftNodeId, "left", { + parentId: current.nodeId, + childId: leftNodeId, + side: "left", + }); + deserializeTracker.markBuilt(leftNodeId, { nodeId: leftNodeId, value: Number(leftValue) }); + deserializeQueue.push({ nodeId: leftNodeId, depth: leftDepth }); + } + + if (partIndex >= parts.length) break; + + const rightValue = parts[partIndex]; + partIndex++; + + if (rightValue !== undefined && rightValue !== "null") { + deserializeTracker.selectElement(Number(rightValue), { + value: Number(rightValue), + partIndex: partIndex - 1, + }); + const rightNodeId = makeNodeId(); + const rightDepth = current.depth + 1; + deserializeTracker.buildNode(rightNodeId, Number(rightValue), { + nodeId: rightNodeId, + value: Number(rightValue), + depth: rightDepth, + }); + deserializeTracker.connectChild(current.nodeId, rightNodeId, "right", { + parentId: current.nodeId, + childId: rightNodeId, + side: "right", + }); + deserializeTracker.markBuilt(rightNodeId, { nodeId: rightNodeId, value: Number(rightValue) }); + deserializeQueue.push({ nodeId: rightNodeId, depth: rightDepth }); + } + } + + deserializeTracker.markBuilt(rootNodeId, { + nodeId: rootNodeId, + value: Number(firstPart), + rebuilt: true, + }); + deserializeTracker.complete({ totalNodes: nodeCounter, serializedString }); + + const allSteps = [...serializeSteps, ...deserializeTracker.getSteps()]; + // Renumber step indices so they are monotonically incrementing across both phases + return allSteps.map((step, combinedIndex) => ({ ...step, index: combinedIndex })); +} diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/DeleteLeavesWithValuePipeline.stories.tsx b/src/algorithms/trees/manipulation/delete-leaves-with-value/DeleteLeavesWithValuePipeline.stories.tsx new file mode 100644 index 00000000..4a1175c2 --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/DeleteLeavesWithValuePipeline.stories.tsx @@ -0,0 +1,117 @@ +/** + * Storybook stories for the Delete Leaves With Value algorithm pipeline. + * Uses the real step generator with a 7-node BST and target value 1, + * rendering the TreeVisualizer at key post-order deletion states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateDeleteLeavesWithValueSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateDeleteLeavesWithValueSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 1, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Delete Leaves With Value", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before post-order deletion begins (target=1) */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some leaves checked or deleted */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all matching leaves removed */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/delete-leaves-with-value.test.ts b/src/algorithms/trees/manipulation/delete-leaves-with-value/delete-leaves-with-value.test.ts new file mode 100644 index 00000000..ab6624a0 --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/delete-leaves-with-value.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from "vitest"; +import { deleteLeavesWithValue } from "./sources/delete-leaves-with-value.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectLevelOrder(root: BinaryNode | null): number[] { + if (!root) return []; + const result: number[] = []; + const queue: BinaryNode[] = [root]; + while (queue.length > 0) { + const current = queue.shift()!; + result.push(current.value); + if (current.left) queue.push(current.left); + if (current.right) queue.push(current.right); + } + return result; +} + +describe("deleteLeavesWithValue", () => { + it("returns null for a single-node tree that is the target", () => { + const root = makeNode(1); + expect(deleteLeavesWithValue(root, 1)).toBeNull(); + }); + + it("returns the root unchanged if no leaf matches the target", () => { + const root = makeNode(1, makeNode(2), makeNode(3)); + const result = deleteLeavesWithValue(root, 9) as BinaryNode; + expect(collectLevelOrder(result)).toEqual([1, 2, 3]); + }); + + it("deletes a leaf with the target value", () => { + const root = makeNode(1, makeNode(2), makeNode(3)); + const result = deleteLeavesWithValue(root, 2) as BinaryNode; + expect(result.left).toBeNull(); + expect(result.right?.value).toBe(3); + }); + + it("cascades deletion when parent becomes leaf after child removal", () => { + // Tree: 1 → left=2(leaf,target) → left only. After removing 2, 1 has no children, but 1≠target so stays. + const root = makeNode(1, makeNode(2)); + const result = deleteLeavesWithValue(root, 2) as BinaryNode; + expect(result.value).toBe(1); + expect(result.left).toBeNull(); + }); + + it("deletes all matching leaves in a 7-node tree", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = deleteLeavesWithValue(root, 1) as BinaryNode; + // Node 1 (leaf, value=1) is deleted + expect(collectLevelOrder(result)).toEqual([4, 2, 6, 3, 5, 7]); + }); +}); diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/educational.ts b/src/algorithms/trees/manipulation/delete-leaves-with-value/educational.ts new file mode 100644 index 00000000..e3ebb98a --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/educational.ts @@ -0,0 +1,48 @@ +import type { EducationalContent } from "@/types"; + +export const deleteLeavesWithValueEducational: EducationalContent = { + overview: + "**Delete Leaves With Value** removes all leaf nodes from a binary tree whose value equals a given target. The process is repeated: after leaves are deleted, their former parents may become leaves and also need to be checked.\n\nPost-order traversal naturally handles this by processing children before parents, ensuring that a node is only evaluated after its subtrees have been cleaned up.", + + howItWorks: + "The recursive post-order algorithm:\n\n" + + "1. **Base case** — if the node is null, return null.\n" + + "2. **Recurse left** — recursively process the left subtree, which may prune some leaves.\n" + + "3. **Recurse right** — recursively process the right subtree.\n" + + "4. **Check deletion** — if the current node is now a leaf (both children are null) and its value equals `targetValue`, return null to delete it.\n" + + "5. **Keep** — otherwise return the current node.\n\n" + + "Post-order ensures that any internal node that becomes a leaf after its children are pruned is also deleted in the same pass.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The recursive call stack depth equals the height of the tree.", + + bestAndWorstCase: + "**Best case** — no target leaves exist: the algorithm still traverses all nodes but deletes nothing — `O(n)` time.\n\n" + + "**Worst case** — all nodes have the target value: the entire tree is deleted in a single post-order pass — `O(n)` time.", + + realWorldUses: [ + "**Dead code elimination** — Remove empty leaf nodes from ASTs where value indicates no-ops.", + "**File system cleanup** — Delete empty directories (leaves) that match a pattern, propagating upward.", + "**Game tree pruning** — Remove terminal states with a specific outcome from game trees.", + "**Data pipeline filtering** — Prune leaf-level entries that match a discard condition from decision trees.", + ], + + strengthsAndLimitations: { + strengths: [ + "Single-pass post-order traversal handles cascading deletions automatically.", + "Simple and elegant — closely mirrors the recursive definition of the operation.", + "Does not require explicit tracking of parent nodes.", + ], + limitations: [ + "Recursive — may stack overflow for very deep trees.", + "Modifies the original tree structure — clone if the original must be preserved.", + "Only removes leaves with the exact target value; does not handle partial matches.", + ], + }, + + whenToUseIt: + "Use delete-leaves-with-value whenever you need to prune leaf nodes that match a condition, including cascading cases where internal nodes become leaves after their children are pruned. The post-order recursion handles all cascades automatically in a single traversal.", +}; diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/index.ts b/src/algorithms/trees/manipulation/delete-leaves-with-value/index.ts new file mode 100644 index 00000000..fc3d2a9d --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/index.ts @@ -0,0 +1,132 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { deleteLeavesWithValue } from "./sources/delete-leaves-with-value.ts?fn"; +import { generateDeleteLeavesWithValueSteps } from "./step-generator"; +import type { DeleteLeavesWithValueInput } from "./step-generator"; +import { deleteLeavesWithValueEducational } from "./educational"; + +import typescriptSource from "./sources/delete-leaves-with-value.ts?raw"; +import pythonSource from "./sources/delete-leaves-with-value.py?raw"; +import javaSource from "./sources/DeleteLeavesWithValue.java?raw"; + +/** Standard 7-node balanced BST. Target=1 will delete leaf n1, then check if n2 becomes a leaf. */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeDeleteLeavesWithValue(input: DeleteLeavesWithValueInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + const resultRoot = deleteLeavesWithValue(root, input.targetValue) as BinaryNode | null; + const result: number[] = []; + const bfsQueue: (BinaryNode | null)[] = [resultRoot]; + while (bfsQueue.length > 0) { + const current = bfsQueue.shift(); + if (current != null) { + result.push(current.value); + bfsQueue.push(current.left, current.right); + } + } + return result; +} + +const deleteLeavesWithValueDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.DELETE_LEAVES_WITH_VALUE!, + name: "Delete Leaves With Value", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Post-order recursive deletion of all leaf nodes whose value equals the target, cascading upward as former internal nodes become leaves", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetValue: 1 }, + }, + execute: executeDeleteLeavesWithValue, + generateSteps: generateDeleteLeavesWithValueSteps, + educational: deleteLeavesWithValueEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(deleteLeavesWithValueDefinition); diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/DeleteLeavesWithValue.java b/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/DeleteLeavesWithValue.java new file mode 100644 index 00000000..1c3ada8e --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/DeleteLeavesWithValue.java @@ -0,0 +1,24 @@ +// Delete Leaves With Value — post-order recursive: remove leaf if value matches target + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class DeleteLeavesWithValue { + public BinaryNode deleteLeavesWithValue(BinaryNode root, int targetValue) { + if (root == null) return null; // @step:initialize + + // Recursively process children first (post-order) + root.left = deleteLeavesWithValue(root.left, targetValue); // @step:traverse-left + root.right = deleteLeavesWithValue(root.right, targetValue); // @step:traverse-right + + // Check if the current node is now a leaf with the target value + if (root.left == null && root.right == null && root.value == targetValue) { // @step:compare + return null; // @step:delete-node + } + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/delete-leaves-with-value.py b/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/delete-leaves-with-value.py new file mode 100644 index 00000000..ce15255c --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/delete-leaves-with-value.py @@ -0,0 +1,21 @@ +# Delete Leaves With Value — post-order recursive: remove leaf if value matches target + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def delete_leaves_with_value(root, target_value: int): + if root is None: # @step:initialize + return None + + # Recursively process children first (post-order) + root.left = delete_leaves_with_value(root.left, target_value) # @step:traverse-left + root.right = delete_leaves_with_value(root.right, target_value) # @step:traverse-right + + # Check if the current node is now a leaf with the target value + if root.left is None and root.right is None and root.value == target_value: # @step:compare + return None # @step:delete-node + + return root # @step:visit diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/delete-leaves-with-value.ts b/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/delete-leaves-with-value.ts new file mode 100644 index 00000000..1a1174db --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/sources/delete-leaves-with-value.ts @@ -0,0 +1,23 @@ +// Delete Leaves With Value — post-order recursive: remove leaf if value matches target + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function deleteLeavesWithValue(root: BinaryNode | null, targetValue: number): BinaryNode | null { + if (root === null) return null; // @step:initialize + + // Recursively process children first (post-order) + root.left = deleteLeavesWithValue(root.left, targetValue); // @step:traverse-left + root.right = deleteLeavesWithValue(root.right, targetValue); // @step:traverse-right + + // Check if the current node is now a leaf with the target value + if (root.left === null && root.right === null && root.value === targetValue) { + // @step:compare + return null; // @step:delete-node + } + + return root; // @step:visit +} diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/step-generator.test.ts b/src/algorithms/trees/manipulation/delete-leaves-with-value/step-generator.test.ts new file mode 100644 index 00000000..ceae034c --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/step-generator.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateDeleteLeavesWithValueSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateDeleteLeavesWithValueSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateDeleteLeavesWithValueSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 1, + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDeleteLeavesWithValueSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 1, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDeleteLeavesWithValueSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 1, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateDeleteLeavesWithValueSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 1, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateDeleteLeavesWithValueSteps({ + nodes: defaultNodes, + rootId: "n4", + targetValue: 1, + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/delete-leaves-with-value/step-generator.ts b/src/algorithms/trees/manipulation/delete-leaves-with-value/step-generator.ts new file mode 100644 index 00000000..43b4db35 --- /dev/null +++ b/src/algorithms/trees/manipulation/delete-leaves-with-value/step-generator.ts @@ -0,0 +1,61 @@ +/** Step generator for Delete Leaves With Value — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const DELETE_LEAVES_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.DELETE_LEAVES_WITH_VALUE!); + +export interface DeleteLeavesWithValueInput { + nodes: TreeNode[]; + rootId: string; + targetValue: number; +} + +export function generateDeleteLeavesWithValueSteps( + input: DeleteLeavesWithValueInput, +): ExecutionStep[] { + const { nodes, rootId, targetValue } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, DELETE_LEAVES_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Delete Leaves With Value", { rootId, targetValue }); + + function processNode(nodeId: string | null): string | null { + if (!nodeId) return null; + const currentNode = nodeMap.get(nodeId); + if (!currentNode) return null; + + // Recursively process children + const newLeftId = processNode(currentNode.leftChildId); + const newRightId = processNode(currentNode.rightChildId); + + // Update node map to reflect any deletions + currentNode.leftChildId = newLeftId; + currentNode.rightChildId = newRightId; + + // Check if now a leaf with the target value + const isLeaf = !newLeftId && !newRightId; + + tracker.compareNodes(nodeId, nodeId, { + nodeId, + value: currentNode.value, + targetValue, + isLeaf, + }); + + if (isLeaf && currentNode.value === targetValue) { + tracker.detachNode(nodeId, { nodeId, value: currentNode.value, deleted: true }); + return null; + } + + tracker.markProcessed(nodeId, { nodeId, value: currentNode.value, kept: true }); + return nodeId; + } + + const newRootId = processNode(rootId); + tracker.complete(null, { result: newRootId ?? "empty tree" }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/distribute-coins/DistributeCoinsPipeline.stories.tsx b/src/algorithms/trees/manipulation/distribute-coins/DistributeCoinsPipeline.stories.tsx new file mode 100644 index 00000000..5a5bb739 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/DistributeCoinsPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Distribute Coins algorithm pipeline. + * Uses the real step generator with a 7-node tree (coin distribution: 4,0,0,3,0,0,0), + * rendering the TreeVisualizer at key DFS excess-counting states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateDistributeCoinsSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n1", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n3", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 0, + parentId: "n1", + leftChildId: "n4", + rightChildId: "n5", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n3", + value: 0, + parentId: "n1", + leftChildId: "n6", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n4", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n5", + value: 0, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n6", + value: 0, + parentId: "n3", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 0, + parentId: "n3", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateDistributeCoinsSteps({ nodes: defaultNodes, rootId: "n1" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Distribute Coins", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree with coins (4,0,0,3,0,0,0) before distribution counting begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some nodes' excess computed during DFS */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — minimum moves counted */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/distribute-coins/distribute-coins.test.ts b/src/algorithms/trees/manipulation/distribute-coins/distribute-coins.test.ts new file mode 100644 index 00000000..42ce93ae --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/distribute-coins.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from "vitest"; +import { distributeCoins } from "./sources/distribute-coins.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("distributeCoins", () => { + it("returns 0 for a null root", () => { + expect(distributeCoins(null)).toBe(0); + }); + + it("returns 0 for a single-node tree with exactly 1 coin", () => { + expect(distributeCoins(makeNode(1))).toBe(0); + }); + + it("returns 1 for a two-node tree where root has 2 coins and child has 0", () => { + const root = makeNode(2, makeNode(0)); + expect(distributeCoins(root)).toBe(1); + }); + + it("returns 3 for a tree with root=3 coins and two children with 0", () => { + const root = makeNode(3, makeNode(0), makeNode(0)); + expect(distributeCoins(root)).toBe(2); + }); + + it("correctly counts moves for all coins concentrated at one leaf", () => { + // Tree: root=0, left=0, right=0, left.left=3 (3 extra coins at deepest node) + // Coins must travel: left.left→left(2 moves), left→root(1 move), root→right(1 move), 3 moves total + const root = makeNode(0, makeNode(0, makeNode(3), null), makeNode(0)); + expect(distributeCoins(root)).toBe(4); + }); +}); diff --git a/src/algorithms/trees/manipulation/distribute-coins/educational.ts b/src/algorithms/trees/manipulation/distribute-coins/educational.ts new file mode 100644 index 00000000..b0463681 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const distributeCoinsEducational: EducationalContent = { + overview: + "**Distribute Coins** counts the minimum number of moves needed to give every node in a binary tree exactly one coin, given that the total number of coins equals the number of nodes. Each move transfers one coin along one edge.\n\nThe key insight is that each edge must carry exactly `|excess|` coins, where excess is the net coins that need to pass through that edge.", + + howItWorks: + "The DFS algorithm computes the excess coins flowing through each edge:\n\n" + + "1. **Initialize** — `totalMoves = 0`.\n" + + "2. **DFS function** — for each node, return `node.value + leftExcess + rightExcess - 1`.\n" + + " - This is the net excess coins that must flow out of (or into) this node.\n" + + "3. **Accumulate moves** — `totalMoves += |leftExcess| + |rightExcess|`.\n" + + " - Each coin that crosses an edge from/to a child adds one move.\n" + + "4. **Return** — the final `totalMoves` count.\n\n" + + "For a tree where root=4 (4 coins), left=0, right=0, leftleft=3, the minimum moves is 4.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once during the DFS.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The recursive call stack depth equals the tree height.", + + bestAndWorstCase: + "**Best case** — coins perfectly distributed already: every node has exactly 1 coin — `O(n)` time, 0 moves.\n\n" + + "**Worst case** — all coins at one node (e.g., root): `O(n)` time and up to `O(n)` moves.", + + realWorldUses: [ + "**Resource balancing** — Distribute computing resources across a hierarchical cluster topology.", + "**Load balancing** — Calculate minimum data transfers needed to balance load across a tree of servers.", + "**Supply chain** — Find minimum shipments to equalize inventory across a distribution tree.", + "**Game mechanics** — Count minimum moves to equalize resources in a tree-structured game map.", + ], + + strengthsAndLimitations: { + strengths: [ + "Elegant single-DFS solution — computes the answer while traversing the tree once.", + "The excess-based formulation naturally captures the flow through each edge.", + "Works for any tree shape without special cases.", + ], + limitations: [ + "Requires exactly `totalCoins === nodeCount` — does not handle excess or deficit totals.", + "Recursive — may stack overflow for very deep trees.", + "Does not track the actual movement path, only the total count of moves.", + ], + }, + + whenToUseIt: + "Use distribute-coins when the total coins equal the total nodes and you need only the minimum move count. For problems where you need to track the actual redistribution path, maintain a parent pointer during DFS.", +}; diff --git a/src/algorithms/trees/manipulation/distribute-coins/index.ts b/src/algorithms/trees/manipulation/distribute-coins/index.ts new file mode 100644 index 00000000..0bdba1c9 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/index.ts @@ -0,0 +1,126 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { distributeCoins } from "./sources/distribute-coins.ts?fn"; +import { generateDistributeCoinsSteps } from "./step-generator"; +import type { DistributeCoinsInput } from "./step-generator"; +import { distributeCoinsEducational } from "./educational"; + +import typescriptSource from "./sources/distribute-coins.ts?raw"; +import pythonSource from "./sources/distribute-coins.py?raw"; +import javaSource from "./sources/DistributeCoins.java?raw"; + +/** + * 7-node tree where node values represent coin counts. + * Total coins = 7 = number of nodes. + * Distribution: root=4 (excess), left=0, right=0, left.left=3 (excess), etc. + */ +const defaultNodes: TreeNode[] = [ + { + id: "n1", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n3", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 0, + parentId: "n1", + leftChildId: "n4", + rightChildId: "n5", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n3", + value: 0, + parentId: "n1", + leftChildId: "n6", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n4", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n5", + value: 0, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n6", + value: 0, + parentId: "n3", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 0, + parentId: "n3", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeDistributeCoins(input: DistributeCoinsInput): number { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + return distributeCoins(root) as number; +} + +const distributeCoinsDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.DISTRIBUTE_COINS!, + name: "Distribute Coins", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "DFS that computes the minimum number of moves to give every node exactly one coin by tracking the excess or deficit flowing through each edge", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n1" }, + }, + execute: executeDistributeCoins, + generateSteps: generateDistributeCoinsSteps, + educational: distributeCoinsEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(distributeCoinsDefinition); diff --git a/src/algorithms/trees/manipulation/distribute-coins/sources/DistributeCoins.java b/src/algorithms/trees/manipulation/distribute-coins/sources/DistributeCoins.java new file mode 100644 index 00000000..0bdc329b --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/sources/DistributeCoins.java @@ -0,0 +1,31 @@ +// Distribute Coins — DFS: each node sends or receives excess coins from children + +class BinaryNode { + int value; // number of coins at this node + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class DistributeCoins { + private int totalMoves = 0; // @step:initialize + + public int distributeCoins(BinaryNode root) { + totalMoves = 0; + dfs(root); // @step:initialize + return totalMoves; // @step:complete + } + + private int dfs(BinaryNode node) { + if (node == null) return 0; // @step:initialize + + // Get excess from left and right children + int leftExcess = dfs(node.left); // @step:traverse-left + int rightExcess = dfs(node.right); // @step:traverse-right + + // Each move on the edge to a child counts + totalMoves += Math.abs(leftExcess) + Math.abs(rightExcess); // @step:accumulate + + // Excess this node sends upward: (coins here) + (excess from children) - 1 (keep 1) + return node.value + leftExcess + rightExcess - 1; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/distribute-coins/sources/distribute-coins.py b/src/algorithms/trees/manipulation/distribute-coins/sources/distribute-coins.py new file mode 100644 index 00000000..34cdb130 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/sources/distribute-coins.py @@ -0,0 +1,28 @@ +# Distribute Coins — DFS: each node sends or receives excess coins from children + +class BinaryNode: + def __init__(self, value: int): + self.value = value # number of coins at this node + self.left = None + self.right = None + +def distribute_coins(root) -> int: + total_moves = 0 # @step:initialize + + def dfs(node) -> int: + nonlocal total_moves + if node is None: # @step:initialize + return 0 + + # Get excess from left and right children + left_excess = dfs(node.left) # @step:traverse-left + right_excess = dfs(node.right) # @step:traverse-right + + # Each move on the edge to a child counts + total_moves += abs(left_excess) + abs(right_excess) # @step:accumulate + + # Excess this node sends upward: (coins here) + (excess from children) - 1 (keep 1) + return node.value + left_excess + right_excess - 1 # @step:visit + + dfs(root) # @step:initialize + return total_moves # @step:complete diff --git a/src/algorithms/trees/manipulation/distribute-coins/sources/distribute-coins.ts b/src/algorithms/trees/manipulation/distribute-coins/sources/distribute-coins.ts new file mode 100644 index 00000000..c8021277 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/sources/distribute-coins.ts @@ -0,0 +1,28 @@ +// Distribute Coins — DFS: each node sends or receives excess coins from children + +interface BinaryNode { + value: number; // number of coins at this node + left: BinaryNode | null; + right: BinaryNode | null; +} + +function distributeCoins(root: BinaryNode | null): number { + let totalMoves = 0; // @step:initialize + + function dfs(node: BinaryNode | null): number { + if (node === null) return 0; // @step:initialize + + // Get excess from left and right children + const leftExcess = dfs(node.left); // @step:traverse-left + const rightExcess = dfs(node.right); // @step:traverse-right + + // Each move on the edge to a child counts + totalMoves += Math.abs(leftExcess) + Math.abs(rightExcess); // @step:accumulate + + // Excess this node sends upward: (coins here) + (excess from children) - 1 (keep 1) + return node.value + leftExcess + rightExcess - 1; // @step:visit + } + + dfs(root); // @step:initialize + return totalMoves; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/distribute-coins/step-generator.test.ts b/src/algorithms/trees/manipulation/distribute-coins/step-generator.test.ts new file mode 100644 index 00000000..57a49d63 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateDistributeCoinsSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n1", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n3", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 0, + parentId: "n1", + leftChildId: "n4", + rightChildId: "n5", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n3", + value: 0, + parentId: "n1", + leftChildId: "n6", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n4", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n5", + value: 0, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n6", + value: 0, + parentId: "n3", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 0, + parentId: "n3", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateDistributeCoinsSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateDistributeCoinsSteps({ nodes: defaultNodes, rootId: "n1" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDistributeCoinsSteps({ nodes: defaultNodes, rootId: "n1" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDistributeCoinsSteps({ nodes: defaultNodes, rootId: "n1" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateDistributeCoinsSteps({ nodes: defaultNodes, rootId: "n1" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateDistributeCoinsSteps({ nodes: defaultNodes, rootId: "n1" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/distribute-coins/step-generator.ts b/src/algorithms/trees/manipulation/distribute-coins/step-generator.ts new file mode 100644 index 00000000..291376f9 --- /dev/null +++ b/src/algorithms/trees/manipulation/distribute-coins/step-generator.ts @@ -0,0 +1,54 @@ +/** Step generator for Distribute Coins — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const DISTRIBUTE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.DISTRIBUTE_COINS!); + +export interface DistributeCoinsInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateDistributeCoinsSteps(input: DistributeCoinsInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, DISTRIBUTE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Distribute Coins", { rootId }); + + let totalMoves = 0; + + function dfs(nodeId: string | null): number { + if (!nodeId) return 0; + const currentNode = nodeMap.get(nodeId); + if (!currentNode) return 0; + + const leftExcess = dfs(currentNode.leftChildId); + const rightExcess = dfs(currentNode.rightChildId); + + const moves = Math.abs(leftExcess) + Math.abs(rightExcess); + totalMoves += moves; + + const excess = currentNode.value + leftExcess + rightExcess - 1; + + tracker.markProcessed(nodeId, { + nodeId, + value: currentNode.value, + leftExcess, + rightExcess, + excess, + moves, + totalMoves, + }); + + return excess; + } + + dfs(rootId); + tracker.complete(null, { result: totalMoves }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/FlattenToLinkedListIterativePipeline.stories.tsx b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/FlattenToLinkedListIterativePipeline.stories.tsx new file mode 100644 index 00000000..28515a74 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/FlattenToLinkedListIterativePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Flatten Binary Tree to Linked List Iterative algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key Morris-like flattening states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateFlattenToLinkedListIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateFlattenToLinkedListIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Flatten to Linked List Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before iterative flattening begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some nodes rewired using Morris-like predecessor threading */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — tree fully flattened iteratively to a right-skewed linked list */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/educational.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/educational.ts new file mode 100644 index 00000000..fe3280a1 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/educational.ts @@ -0,0 +1,50 @@ +import type { EducationalContent } from "@/types"; + +export const flattenToLinkedListIterativeEducational: EducationalContent = { + overview: + "**Flatten Binary Tree to Linked List (Iterative)** uses a Morris-traversal-inspired technique to transform a binary tree into a right-skewed linked list in-place without recursion and in `O(1)` extra space.\n\nFor each node with a left child, the algorithm locates the rightmost node of the left subtree, connects the original right subtree there, then moves the entire left subtree to the right before advancing.", + + howItWorks: + "The algorithm walks through the tree one node at a time:\n\n" + + "1. **Initialize** — point `current` at the root.\n" + + "2. **Check left** — if the current node has no left child, advance to `current.right`.\n" + + "3. **Find rightmost** — walk `current.left` all the way right to find the tail of the left subtree.\n" + + "4. **Connect** — attach `current.right` (original right subtree) to that tail's right pointer.\n" + + "5. **Move left to right** — set `current.right = current.left`, then `current.left = null`.\n" + + "6. **Advance** — move `current` to `current.right` (the former left child).\n" + + "7. **Repeat** — continue until `current` is null.\n\n" + + "This is sometimes called the Morris Flatten because it shares the rightmost-predecessor logic with Morris traversal.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each node is visited at most twice (once as `current`, once as `rightmost` during the inner walk), giving amortized `O(n)` total.\n\n" + + "**Space Complexity: `O(1)`**\n\n" + + "No recursion stack or auxiliary data structures are needed — all pointers are rewired in-place.", + + bestAndWorstCase: + "**Best case** — already-right-skewed tree: no left children, so the inner loop never runs — `O(n)` time, `O(1)` space.\n\n" + + "**Worst case** — perfectly balanced tree: every node triggers an inner rightmost walk, but each node is visited at most twice total — still `O(n)` time.", + + realWorldUses: [ + "**Memory-constrained environments** — Flatten without any extra stack or queue allocation.", + "**Streaming tree processors** — Walk nodes sequentially without holding the whole call stack.", + "**Compiler IR linearization** — Convert expression trees to linear instruction sequences.", + "**Morris-like traversal extensions** — Base pattern for other pointer-threading algorithms.", + ], + + strengthsAndLimitations: { + strengths: [ + "`O(1)` extra space — no recursion stack or auxiliary queues needed.", + "Avoids stack overflow risk for arbitrarily deep trees.", + "Runs in a single forward pass through the tree.", + ], + limitations: [ + "More complex than the recursive version — harder to understand at a glance.", + "Modifies the original tree — clone if the original must be preserved.", + "The inner rightmost loop can be mistaken for `O(n²)` complexity without careful analysis.", + ], + }, + + whenToUseIt: + "Use the iterative variant when working in memory-constrained environments or when trees may be arbitrarily deep. For clarity and simpler code, the recursive version is preferred. Both run in `O(n)` time; only the iterative version achieves `O(1)` space.", +}; diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/flatten-to-linked-list-iterative.test.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/flatten-to-linked-list-iterative.test.ts new file mode 100644 index 00000000..11e42f98 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/flatten-to-linked-list-iterative.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from "vitest"; +import { flattenToLinkedListIterative } from "./sources/flatten-to-linked-list-iterative.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectRightChain(root: BinaryNode | null): number[] { + const result: number[] = []; + let current = root; + while (current) { + result.push(current.value); + current = current.right; + } + return result; +} + +describe("flattenToLinkedListIterative", () => { + it("does nothing for a null root", () => { + expect(() => flattenToLinkedListIterative(null)).not.toThrow(); + }); + + it("handles a single-node tree without change", () => { + const root = makeNode(1); + flattenToLinkedListIterative(root); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("flattens a two-node tree with left child", () => { + const root = makeNode(1, makeNode(2)); + flattenToLinkedListIterative(root); + expect(root.left).toBeNull(); + expect(collectRightChain(root)).toEqual([1, 2]); + }); + + it("flattens a 7-node BST in preorder", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + flattenToLinkedListIterative(root); + expect(collectRightChain(root)).toEqual([4, 2, 1, 3, 6, 5, 7]); + }); + + it("sets all left pointers to null after flattening", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + flattenToLinkedListIterative(root); + let current: BinaryNode | null = root; + while (current) { + expect(current.left).toBeNull(); + current = current.right; + } + }); + + it("flattens a right-skewed tree unchanged", () => { + const root = makeNode(1, null, makeNode(2, null, makeNode(3))); + flattenToLinkedListIterative(root); + expect(collectRightChain(root)).toEqual([1, 2, 3]); + }); +}); diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/index.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/index.ts new file mode 100644 index 00000000..aac7f489 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/index.ts @@ -0,0 +1,131 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { flattenToLinkedListIterative } from "./sources/flatten-to-linked-list-iterative.ts?fn"; +import { generateFlattenToLinkedListIterativeSteps } from "./step-generator"; +import type { FlattenToLinkedListIterativeInput } from "./step-generator"; +import { flattenToLinkedListIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/flatten-to-linked-list-iterative.ts?raw"; +import pythonSource from "./sources/flatten-to-linked-list-iterative.py?raw"; +import javaSource from "./sources/FlattenToLinkedListIterative.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeFlattenToLinkedListIterative(input: FlattenToLinkedListIterativeInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + if (!root) return []; + flattenToLinkedListIterative(root); + const result: number[] = []; + let current: BinaryNode | null = root; + while (current) { + result.push(current.value); + current = current.right; + } + return result; +} + +const flattenToLinkedListIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.FLATTEN_TO_LINKED_LIST_ITERATIVE!, + name: "Flatten to Linked List (Iterative)", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Iteratively flattens a binary tree into a right-skewed linked list in-place using a Morris-like predecessor-threading approach with O(1) extra space", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeFlattenToLinkedListIterative, + generateSteps: generateFlattenToLinkedListIterativeSteps, + educational: flattenToLinkedListIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, + }; + +registry.register(flattenToLinkedListIterativeDefinition); diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/FlattenToLinkedListIterative.java b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/FlattenToLinkedListIterative.java new file mode 100644 index 00000000..bd964ce4 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/FlattenToLinkedListIterative.java @@ -0,0 +1,32 @@ +// Flatten Binary Tree to Linked List Iterative — Morris-like: find rightmost of left subtree and rewire + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class FlattenToLinkedListIterative { + public void flattenToLinkedListIterative(BinaryNode root) { + BinaryNode current = root; // @step:initialize + + while (current != null) { // @step:visit + if (current.left != null) { // @step:visit + // Find the rightmost node of the left subtree + BinaryNode rightmost = current.left; // @step:connect-child + while (rightmost.right != null) { // @step:connect-child + rightmost = rightmost.right; // @step:connect-child + } + + // Attach original right subtree at the rightmost node + rightmost.right = current.right; // @step:connect-child + + // Move left subtree to right, clear left pointer + current.right = current.left; // @step:connect-child + current.left = null; // @step:connect-child + } + + current = current.right; // @step:visit + } + } +} diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/flatten-to-linked-list-iterative.py b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/flatten-to-linked-list-iterative.py new file mode 100644 index 00000000..2734c04a --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/flatten-to-linked-list-iterative.py @@ -0,0 +1,26 @@ +# Flatten Binary Tree to Linked List Iterative — Morris-like: find rightmost of left subtree and rewire + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def flatten_to_linked_list_iterative(root) -> None: + current = root # @step:initialize + + while current is not None: # @step:visit + if current.left is not None: # @step:visit + # Find the rightmost node of the left subtree + rightmost = current.left # @step:connect-child + while rightmost.right is not None: # @step:connect-child + rightmost = rightmost.right # @step:connect-child + + # Attach original right subtree at the rightmost node + rightmost.right = current.right # @step:connect-child + + # Move left subtree to right, clear left pointer + current.right = current.left # @step:connect-child + current.left = None # @step:connect-child + + current = current.right # @step:visit diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/flatten-to-linked-list-iterative.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/flatten-to-linked-list-iterative.ts new file mode 100644 index 00000000..fa52ce73 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/sources/flatten-to-linked-list-iterative.ts @@ -0,0 +1,33 @@ +// Flatten Binary Tree to Linked List Iterative — Morris-like: find rightmost of left subtree and rewire + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function flattenToLinkedListIterative(root: BinaryNode | null): void { + let current = root; // @step:initialize + + while (current !== null) { + // @step:visit + if (current.left !== null) { + // @step:visit + // Find the rightmost node of the left subtree + let rightmost = current.left; // @step:connect-child + while (rightmost.right !== null) { + // @step:connect-child + rightmost = rightmost.right; // @step:connect-child + } + + // Attach original right subtree at the rightmost node + rightmost.right = current.right; // @step:connect-child + + // Move left subtree to right, clear left pointer + current.right = current.left; // @step:connect-child + current.left = null; // @step:connect-child + } + + current = current.right; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/step-generator.test.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/step-generator.test.ts new file mode 100644 index 00000000..688eaaca --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateFlattenToLinkedListIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateFlattenToLinkedListIterativeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateFlattenToLinkedListIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateFlattenToLinkedListIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateFlattenToLinkedListIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateFlattenToLinkedListIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateFlattenToLinkedListIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/step-generator.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/step-generator.ts new file mode 100644 index 00000000..64dc3c1c --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list-iterative/step-generator.ts @@ -0,0 +1,76 @@ +/** Step generator for Flatten Binary Tree to Linked List Iterative — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const FLATTEN_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.FLATTEN_TO_LINKED_LIST_ITERATIVE!, +); + +export interface FlattenToLinkedListIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateFlattenToLinkedListIterativeSteps( + input: FlattenToLinkedListIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, FLATTEN_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Flatten Binary Tree to Linked List (Iterative)", { rootId }); + + let currentId: string | null = rootId; + + while (currentId) { + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + if (currentNode.leftChildId) { + // Find the rightmost node of the left subtree + let rightmostId = currentNode.leftChildId; + while (true) { + const rightmostNode = nodeMap.get(rightmostId); + if (!rightmostNode?.rightChildId) break; + rightmostId = rightmostNode.rightChildId; + } + + const rightmostNode = nodeMap.get(rightmostId); + const leftId = currentNode.leftChildId; + const rightId = currentNode.rightChildId; + + // Attach original right subtree at the rightmost node + if (rightmostNode) { + rightmostNode.rightChildId = rightId; + if (rightId) { + const rightNode = nodeMap.get(rightId); + if (rightNode) rightNode.parentId = rightmostId; + } + } + + // Move left subtree to right + tracker.moveNode(leftId, currentId, "right", { + currentId, + movedChildId: leftId, + originalRightId: rightId, + }); + + currentNode.rightChildId = leftId; + const leftNode = nodeMap.get(leftId); + if (leftNode) leftNode.parentId = currentId; + + // Clear left pointer + currentNode.leftChildId = null; + tracker.detachNode(currentId, { currentId, side: "left" }); + } + + tracker.markProcessed(currentId, { currentId, value: currentNode.value }); + currentId = currentNode.rightChildId; + } + + tracker.complete(null, { result: "flattened" }); + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/FlattenToLinkedListPipeline.stories.tsx b/src/algorithms/trees/manipulation/flatten-to-linked-list/FlattenToLinkedListPipeline.stories.tsx new file mode 100644 index 00000000..d26ac679 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/FlattenToLinkedListPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Flatten Binary Tree to Linked List algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key flattening states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateFlattenToLinkedListSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateFlattenToLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Flatten to Linked List", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before flattening begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some nodes rewired to the right */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — tree fully flattened to a right-skewed linked list */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/educational.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list/educational.ts new file mode 100644 index 00000000..92326166 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/educational.ts @@ -0,0 +1,51 @@ +import type { EducationalContent } from "@/types"; + +export const flattenToLinkedListEducational: EducationalContent = { + overview: + "**Flatten Binary Tree to Linked List** transforms a binary tree into a right-skewed linked list in-place, using the same nodes. The resulting order follows a preorder traversal: root, then left subtree, then right subtree.\n\nThis problem is commonly asked in interviews because it tests both tree traversal understanding and pointer manipulation skills.", + + howItWorks: + "The algorithm uses a recursive post-order approach to ensure subtrees are flattened before the parent rewires them:\n\n" + + "1. **Base case** — if the node is `null`, return.\n" + + "2. **Flatten left** — recursively flatten the left subtree into a linked list.\n" + + "3. **Flatten right** — recursively flatten the right subtree into a linked list.\n" + + "4. **Save right** — store the original right subtree pointer.\n" + + "5. **Move left to right** — set `right = left`, then set `left = null`.\n" + + "6. **Find tail** — walk to the rightmost node of the (now-right) former left subtree.\n" + + "7. **Attach** — connect the saved right subtree at the tail.\n\n" + + "After flattening a 7-node tree, the result is: `4 → 2 → 1 → 3 → 6 → 5 → 7` (all right pointers, all left pointers null).", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each node is visited exactly once during the recursion plus the rightmost walk, which amortizes to `O(n)` total.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The recursive call stack consumes space proportional to the height. `O(log n)` for balanced trees, `O(n)` for skewed trees.", + + bestAndWorstCase: + "**Best case** — balanced tree: `O(log n)` stack depth.\n\n" + + "**Worst case** — already-right-skewed tree: `O(n)` stack depth and no work to do in the inner loop.\n\n" + + "Time is always `O(n)` regardless of shape.", + + realWorldUses: [ + "**Serialization** — Convert tree structures to linear sequences for storage or transmission.", + "**In-place compaction** — Flatten trees for memory-efficient iteration without allocating a separate list.", + "**Parser trees** — Linearize ASTs or expression trees for sequential evaluation.", + "**Database B-tree pages** — Reorder node pointers for sequential leaf access patterns.", + ], + + strengthsAndLimitations: { + strengths: [ + "In-place transformation — no extra memory for a new data structure.", + "Recursive implementation closely mirrors the preorder traversal definition.", + "Produces a canonical right-skewed linked list usable by any linked list algorithm.", + ], + limitations: [ + "Destroys the original tree structure — caller must clone if the tree must be preserved.", + "Recursive version risks stack overflow on very deep trees.", + "The inner `while` loop makes the naive analysis appear quadratic, though it is amortized `O(n)`.", + ], + }, + + whenToUseIt: + "Use flatten-to-linked-list when you need to linearize a tree for sequential processing and can afford to modify the tree in-place. For immutable trees, collect values in preorder into a separate list instead. Use the iterative variant for very deep trees.", +}; diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/flatten-to-linked-list.test.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list/flatten-to-linked-list.test.ts new file mode 100644 index 00000000..e0e009b6 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/flatten-to-linked-list.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from "vitest"; +import { flattenToLinkedList } from "./sources/flatten-to-linked-list.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectRightChain(root: BinaryNode | null): number[] { + const result: number[] = []; + let current = root; + while (current) { + result.push(current.value); + current = current.right; + } + return result; +} + +describe("flattenToLinkedList", () => { + it("does nothing for a null root", () => { + expect(() => flattenToLinkedList(null)).not.toThrow(); + }); + + it("handles a single-node tree without change", () => { + const root = makeNode(1); + flattenToLinkedList(root); + expect(root.left).toBeNull(); + expect(root.right).toBeNull(); + }); + + it("flattens a two-node tree with left child", () => { + const root = makeNode(1, makeNode(2)); + flattenToLinkedList(root); + expect(root.left).toBeNull(); + expect(collectRightChain(root)).toEqual([1, 2]); + }); + + it("flattens a 7-node BST in preorder", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + flattenToLinkedList(root); + expect(collectRightChain(root)).toEqual([4, 2, 1, 3, 6, 5, 7]); + }); + + it("sets all left pointers to null after flattening", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + flattenToLinkedList(root); + let current: BinaryNode | null = root; + while (current) { + expect(current.left).toBeNull(); + current = current.right; + } + }); + + it("flattens a right-skewed tree unchanged", () => { + const root = makeNode(1, null, makeNode(2, null, makeNode(3))); + flattenToLinkedList(root); + expect(collectRightChain(root)).toEqual([1, 2, 3]); + }); +}); diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/index.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list/index.ts new file mode 100644 index 00000000..7b6b8b52 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/index.ts @@ -0,0 +1,131 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { flattenToLinkedList } from "./sources/flatten-to-linked-list.ts?fn"; +import { generateFlattenToLinkedListSteps } from "./step-generator"; +import type { FlattenToLinkedListInput } from "./step-generator"; +import { flattenToLinkedListEducational } from "./educational"; + +import typescriptSource from "./sources/flatten-to-linked-list.ts?raw"; +import pythonSource from "./sources/flatten-to-linked-list.py?raw"; +import javaSource from "./sources/FlattenToLinkedList.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeFlattenToLinkedList(input: FlattenToLinkedListInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + if (!root) return []; + flattenToLinkedList(root); + // Collect values by following right pointers + const result: number[] = []; + let current: BinaryNode | null = root; + while (current) { + result.push(current.value); + current = current.right; + } + return result; +} + +const flattenToLinkedListDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.FLATTEN_TO_LINKED_LIST!, + name: "Flatten to Linked List", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursively flattens a binary tree into a right-skewed linked list in-place using preorder traversal order", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeFlattenToLinkedList, + generateSteps: generateFlattenToLinkedListSteps, + educational: flattenToLinkedListEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(flattenToLinkedListDefinition); diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/FlattenToLinkedList.java b/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/FlattenToLinkedList.java new file mode 100644 index 00000000..01f84325 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/FlattenToLinkedList.java @@ -0,0 +1,33 @@ +// Flatten Binary Tree to Linked List — recursive preorder: rewire nodes in-place + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class FlattenToLinkedList { + public void flattenToLinkedList(BinaryNode root) { + if (root == null) return; // @step:initialize + + // Recursively flatten the left and right subtrees + flattenToLinkedList(root.left); // @step:traverse-left + flattenToLinkedList(root.right); // @step:traverse-right + + // Save the original right subtree + BinaryNode rightSubtree = root.right; // @step:connect-child + + // Move the left subtree to the right + root.right = root.left; // @step:connect-child + root.left = null; // @step:connect-child + + // Find the rightmost node of the newly-placed subtree + BinaryNode current = root; + while (current.right != null) { // @step:visit + current = current.right; // @step:visit + } + + // Attach the original right subtree at the tail + current.right = rightSubtree; // @step:connect-child + } +} diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/flatten-to-linked-list.py b/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/flatten-to-linked-list.py new file mode 100644 index 00000000..ae35aadb --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/flatten-to-linked-list.py @@ -0,0 +1,30 @@ +# Flatten Binary Tree to Linked List — recursive preorder: rewire nodes in-place + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def flatten_to_linked_list(root) -> None: + if root is None: # @step:initialize + return + + # Recursively flatten the left and right subtrees + flatten_to_linked_list(root.left) # @step:traverse-left + flatten_to_linked_list(root.right) # @step:traverse-right + + # Save the original right subtree + right_subtree = root.right # @step:connect-child + + # Move the left subtree to the right + root.right = root.left # @step:connect-child + root.left = None # @step:connect-child + + # Find the rightmost node of the newly-placed subtree + current = root + while current.right is not None: # @step:visit + current = current.right # @step:visit + + # Attach the original right subtree at the tail + current.right = right_subtree # @step:connect-child diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/flatten-to-linked-list.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/flatten-to-linked-list.ts new file mode 100644 index 00000000..c3acc5e4 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/sources/flatten-to-linked-list.ts @@ -0,0 +1,32 @@ +// Flatten Binary Tree to Linked List — recursive preorder: rewire nodes in-place + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function flattenToLinkedList(root: BinaryNode | null): void { + if (root === null) return; // @step:initialize + + // Recursively flatten the left and right subtrees + flattenToLinkedList(root.left); // @step:traverse-left + flattenToLinkedList(root.right); // @step:traverse-right + + // Save the original right subtree + const rightSubtree = root.right; // @step:connect-child + + // Move the left subtree to the right + root.right = root.left; // @step:connect-child + root.left = null; // @step:connect-child + + // Find the rightmost node of the newly-placed subtree + let current: BinaryNode = root; + while (current.right !== null) { + // @step:visit + current = current.right; // @step:visit + } + + // Attach the original right subtree at the tail + current.right = rightSubtree; // @step:connect-child +} diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/step-generator.test.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list/step-generator.test.ts new file mode 100644 index 00000000..ed98bcf7 --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateFlattenToLinkedListSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateFlattenToLinkedListSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateFlattenToLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateFlattenToLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateFlattenToLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateFlattenToLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateFlattenToLinkedListSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/flatten-to-linked-list/step-generator.ts b/src/algorithms/trees/manipulation/flatten-to-linked-list/step-generator.ts new file mode 100644 index 00000000..4756f94b --- /dev/null +++ b/src/algorithms/trees/manipulation/flatten-to-linked-list/step-generator.ts @@ -0,0 +1,80 @@ +/** Step generator for Flatten Binary Tree to Linked List — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const FLATTEN_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.FLATTEN_TO_LINKED_LIST!); + +export interface FlattenToLinkedListInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateFlattenToLinkedListSteps(input: FlattenToLinkedListInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, FLATTEN_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Flatten Binary Tree to Linked List", { rootId }); + + function flatten(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + const leftId = node.leftChildId; + const rightId = node.rightChildId; + + // Recursively flatten subtrees + flatten(leftId); + flatten(rightId); + + if (leftId) { + // Find the tail of the flattened left subtree + let tailId = leftId; + while (true) { + const tailNode = nodeMap.get(tailId); + if (!tailNode?.rightChildId) break; + tailId = tailNode.rightChildId; + } + + // Attach original right subtree at the tail + const tailNode = nodeMap.get(tailId); + if (tailNode) { + tailNode.rightChildId = rightId; + if (rightId) { + const rightNode = nodeMap.get(rightId); + if (rightNode) rightNode.parentId = tailId; + } + } + + // Move left subtree to right, clear left + tracker.moveNode(leftId, nodeId, "right", { + nodeId, + movedChildId: leftId, + originalRightId: rightId, + }); + + node.rightChildId = leftId; + node.leftChildId = null; + + const leftNode = nodeMap.get(leftId); + if (leftNode) leftNode.parentId = nodeId; + + // Update tracker's node to reflect cleared left + tracker.detachNode(nodeId, { nodeId, side: "left" }); + + // Reflect the null left in our node + node.leftChildId = null; + } + + tracker.markProcessed(nodeId, { nodeId, value: node.value }); + } + + flatten(rootId); + tracker.complete(null, { result: "flattened" }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/FlipEquivalentTreesPipeline.stories.tsx b/src/algorithms/trees/manipulation/flip-equivalent-trees/FlipEquivalentTreesPipeline.stories.tsx new file mode 100644 index 00000000..3c517472 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/FlipEquivalentTreesPipeline.stories.tsx @@ -0,0 +1,184 @@ +/** + * Storybook stories for the Flip Equivalent Trees algorithm pipeline. + * Uses the real step generator with Tree A (standard BST) and Tree B (root children swapped), + * rendering the TreeVisualizer at key comparison states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateFlipEquivalentTreesSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = [ + { + id: "m4", + value: 4, + parentId: null, + leftChildId: "m6", + rightChildId: "m2", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m6", + value: 6, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m2", + value: 2, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m5", + value: 5, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m7", + value: 7, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m1", + value: 1, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m3", + value: 3, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateFlipEquivalentTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Flip Equivalent Trees", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — Tree A and Tree B before flip-equivalence check begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some node pairs compared for both no-flip and with-flip cases */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — flip equivalence result determined */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/educational.ts b/src/algorithms/trees/manipulation/flip-equivalent-trees/educational.ts new file mode 100644 index 00000000..dd7b93a6 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/educational.ts @@ -0,0 +1,47 @@ +import type { EducationalContent } from "@/types"; + +export const flipEquivalentTreesEducational: EducationalContent = { + overview: + "**Flip Equivalent Trees** determines whether two binary trees can be made identical by flipping (swapping) the left and right children at any number of nodes. Two null trees are always flip-equivalent.\n\nThis problem generalizes structural tree equality: same-tree requires exact match, but flip-equivalent allows any combination of child swaps.", + + howItWorks: + "The recursive algorithm checks two possibilities at each node:\n\n" + + "1. **Base cases** — if both nodes are null: `true`; if only one is null: `false`; if values differ: `false`.\n" + + "2. **No-flip** — check if `(A.left, B.left)` and `(A.right, B.right)` are flip-equivalent.\n" + + "3. **With-flip** — check if `(A.left, B.right)` and `(A.right, B.left)` are flip-equivalent.\n" + + "4. **Return** — `noFlip || withFlip`.\n\n" + + "For the default input (Tree A: values 1–7 in standard BST, Tree B: same values but with left/right swapped at the root), the result is `true`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(min(n, m))` to `O(n × m)`**\n\n" + + "In the worst case (similar trees differing only near the leaves), both no-flip and with-flip branches are explored at every node, yielding exponential time. In practice, early termination on value mismatch keeps it much faster.\n\n" + + "**Space Complexity: `O(min(h₁, h₂))`**\n\n" + + "The recursive call stack depth is bounded by the height of the shallower tree.", + + bestAndWorstCase: + "**Best case** — root values differ: returns `false` immediately — `O(1)` time.\n\n" + + "**Worst case** — both trees have identical values throughout: both no-flip and with-flip are explored at every node.", + + realWorldUses: [ + "**Test equivalence** — Verify two compiler-generated syntax trees are equivalent despite reordering.", + "**Data normalization** — Check if two hierarchical data structures represent the same information regardless of child order.", + "**Symmetric comparisons** — Detect when two game state trees differ only by commutative operations.", + "**Tree canonicalization** — Used as a building block for canonical tree representations.", + ], + + strengthsAndLimitations: { + strengths: [ + "Handles all possible flip combinations in a clean recursive formulation.", + "Early exit on value mismatch avoids exploring unnecessary branches.", + "Extends naturally to n-ary trees by checking all child permutations.", + ], + limitations: [ + "Potentially exponential in pathological cases — use sorting-based canonicalization for repeated queries.", + "Recursive — may stack overflow for very deep trees.", + "For trees with unique values, canonical sorting at each node can reduce to `O(n log n)`.", + ], + }, + + whenToUseIt: + "Use flip-equivalent-trees for one-off equivalence checks on moderate-sized trees. For repeated checks or large trees with duplicate values, preprocess both trees into canonical form (sort children by value) and then compare normally.", +}; diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/flip-equivalent-trees.test.ts b/src/algorithms/trees/manipulation/flip-equivalent-trees/flip-equivalent-trees.test.ts new file mode 100644 index 00000000..c430f8e5 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/flip-equivalent-trees.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "vitest"; +import { flipEquivalentTrees } from "./sources/flip-equivalent-trees.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("flipEquivalentTrees", () => { + it("returns true for two null trees", () => { + expect(flipEquivalentTrees(null, null)).toBe(true); + }); + + it("returns false when one tree is null", () => { + expect(flipEquivalentTrees(makeNode(1), null)).toBe(false); + expect(flipEquivalentTrees(null, makeNode(1))).toBe(false); + }); + + it("returns true for two identical trees", () => { + const treeA = makeNode(1, makeNode(2), makeNode(3)); + const treeB = makeNode(1, makeNode(2), makeNode(3)); + expect(flipEquivalentTrees(treeA, treeB)).toBe(true); + }); + + it("returns true for two trees that are flip-equivalent at root", () => { + const treeA = makeNode(1, makeNode(2), makeNode(3)); + const treeB = makeNode(1, makeNode(3), makeNode(2)); + expect(flipEquivalentTrees(treeA, treeB)).toBe(true); + }); + + it("returns true for 7-node BST where root children are swapped", () => { + const treeA = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const treeB = makeNode( + 4, + makeNode(6, makeNode(5), makeNode(7)), + makeNode(2, makeNode(1), makeNode(3)), + ); + expect(flipEquivalentTrees(treeA, treeB)).toBe(true); + }); + + it("returns false for trees with different root values", () => { + expect(flipEquivalentTrees(makeNode(1), makeNode(2))).toBe(false); + }); + + it("returns false for trees with same structure but different leaf values", () => { + const treeA = makeNode(1, makeNode(2), makeNode(3)); + const treeB = makeNode(1, makeNode(9), makeNode(3)); + expect(flipEquivalentTrees(treeA, treeB)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/index.ts b/src/algorithms/trees/manipulation/flip-equivalent-trees/index.ts new file mode 100644 index 00000000..072b770e --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/index.ts @@ -0,0 +1,192 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { flipEquivalentTrees } from "./sources/flip-equivalent-trees.ts?fn"; +import { generateFlipEquivalentTreesSteps } from "./step-generator"; +import type { FlipEquivalentTreesInput } from "./step-generator"; +import { flipEquivalentTreesEducational } from "./educational"; + +import typescriptSource from "./sources/flip-equivalent-trees.ts?raw"; +import pythonSource from "./sources/flip-equivalent-trees.py?raw"; +import javaSource from "./sources/FlipEquivalentTrees.java?raw"; + +/** Tree A: standard 7-node balanced BST */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Tree B: same values but root children are swapped (6 on left, 2 on right) */ +const secondaryNodes: TreeNode[] = [ + { + id: "m4", + value: 4, + parentId: null, + leftChildId: "m6", + rightChildId: "m2", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m6", + value: 6, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m2", + value: 2, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m5", + value: 5, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m7", + value: 7, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m1", + value: 1, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m3", + value: 3, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeFlipEquivalentTrees(input: FlipEquivalentTreesInput): boolean { + function buildNode(nodeMap: Map, id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: buildNode(nodeMap, node.leftChildId), + right: buildNode(nodeMap, node.rightChildId), + }; + } + const mapA = new Map(input.nodes.map((node) => [node.id, node])); + const mapB = new Map(input.secondaryNodes.map((node) => [node.id, node])); + return flipEquivalentTrees( + buildNode(mapA, input.rootId), + buildNode(mapB, input.secondaryRootId), + ) as boolean; +} + +const flipEquivalentTreesDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.FLIP_EQUIVALENT_TREES!, + name: "Flip Equivalent Trees", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursively checks whether two binary trees can be made identical by flipping left/right children at any number of nodes", + timeComplexity: { best: "O(1)", average: "O(n)", worst: "O(n²)" }, + spaceComplexity: "O(min(h1,h2))", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", secondaryNodes, secondaryRootId: "m4" }, + }, + execute: executeFlipEquivalentTrees, + generateSteps: generateFlipEquivalentTreesSteps, + educational: flipEquivalentTreesEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(flipEquivalentTreesDefinition); diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/FlipEquivalentTrees.java b/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/FlipEquivalentTrees.java new file mode 100644 index 00000000..c430dd83 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/FlipEquivalentTrees.java @@ -0,0 +1,27 @@ +// Flip Equivalent Trees — recursive: trees are flip-equivalent if children match or are swapped + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class FlipEquivalentTrees { + public boolean flipEquivalentTrees(BinaryNode treeA, BinaryNode treeB) { + if (treeA == null && treeB == null) return true; // @step:initialize + if (treeA == null || treeB == null) return false; // @step:compare + if (treeA.value != treeB.value) return false; // @step:compare + + // Check if children match without flipping + boolean noFlip = // @step:traverse-left + flipEquivalentTrees(treeA.left, treeB.left) && // @step:traverse-left + flipEquivalentTrees(treeA.right, treeB.right); // @step:traverse-right + + // Check if children match with flipping + boolean withFlip = // @step:traverse-left + flipEquivalentTrees(treeA.left, treeB.right) && // @step:traverse-left + flipEquivalentTrees(treeA.right, treeB.left); // @step:traverse-right + + return noFlip || withFlip; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/flip-equivalent-trees.py b/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/flip-equivalent-trees.py new file mode 100644 index 00000000..e6ce5aa3 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/flip-equivalent-trees.py @@ -0,0 +1,29 @@ +# Flip Equivalent Trees — recursive: trees are flip-equivalent if children match or are swapped + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def flip_equivalent_trees(tree_a, tree_b) -> bool: + if tree_a is None and tree_b is None: # @step:initialize + return True + if tree_a is None or tree_b is None: # @step:compare + return False + if tree_a.value != tree_b.value: # @step:compare + return False + + # Check if children match without flipping + no_flip = ( # @step:traverse-left + flip_equivalent_trees(tree_a.left, tree_b.left) and # @step:traverse-left + flip_equivalent_trees(tree_a.right, tree_b.right) # @step:traverse-right + ) + + # Check if children match with flipping + with_flip = ( # @step:traverse-left + flip_equivalent_trees(tree_a.left, tree_b.right) and # @step:traverse-left + flip_equivalent_trees(tree_a.right, tree_b.left) # @step:traverse-right + ) + + return no_flip or with_flip # @step:visit diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/flip-equivalent-trees.ts b/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/flip-equivalent-trees.ts new file mode 100644 index 00000000..83bc8296 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/sources/flip-equivalent-trees.ts @@ -0,0 +1,25 @@ +// Flip Equivalent Trees — recursive: trees are flip-equivalent if children match or are swapped + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function flipEquivalentTrees(treeA: BinaryNode | null, treeB: BinaryNode | null): boolean { + if (treeA === null && treeB === null) return true; // @step:initialize + if (treeA === null || treeB === null) return false; // @step:compare + if (treeA.value !== treeB.value) return false; // @step:compare + + // Check if children match without flipping + const noFlip = // @step:traverse-left + flipEquivalentTrees(treeA.left, treeB.left) && // @step:traverse-left + flipEquivalentTrees(treeA.right, treeB.right); // @step:traverse-right + + // Check if children match with flipping + const withFlip = // @step:traverse-left + flipEquivalentTrees(treeA.left, treeB.right) && // @step:traverse-left + flipEquivalentTrees(treeA.right, treeB.left); // @step:traverse-right + + return noFlip || withFlip; // @step:visit +} diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/step-generator.test.ts b/src/algorithms/trees/manipulation/flip-equivalent-trees/step-generator.test.ts new file mode 100644 index 00000000..8a3d0bfd --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/step-generator.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateFlipEquivalentTreesSteps } from "./step-generator"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = [ + { + id: "m4", + value: 4, + parentId: null, + leftChildId: "m6", + rightChildId: "m2", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m6", + value: 6, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m2", + value: 2, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m5", + value: 5, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m7", + value: 7, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m1", + value: 1, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m3", + value: 3, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateFlipEquivalentTreesSteps", () => { + it("produces steps for two flip-equivalent trees", () => { + const steps = generateFlipEquivalentTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateFlipEquivalentTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateFlipEquivalentTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateFlipEquivalentTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateFlipEquivalentTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/flip-equivalent-trees/step-generator.ts b/src/algorithms/trees/manipulation/flip-equivalent-trees/step-generator.ts new file mode 100644 index 00000000..60d31259 --- /dev/null +++ b/src/algorithms/trees/manipulation/flip-equivalent-trees/step-generator.ts @@ -0,0 +1,70 @@ +/** Step generator for Flip Equivalent Trees — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const FLIP_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.FLIP_EQUIVALENT_TREES!); + +export interface FlipEquivalentTreesInput { + nodes: TreeNode[]; + rootId: string; + secondaryNodes: TreeNode[]; + secondaryRootId: string; +} + +export function generateFlipEquivalentTreesSteps(input: FlipEquivalentTreesInput): ExecutionStep[] { + const { nodes, rootId, secondaryNodes, secondaryRootId } = input; + const tracker = new TreeManipulationTracker( + nodes, + rootId, + FLIP_LINE_MAP, + secondaryNodes, + secondaryRootId, + ); + const nodeMapA = new Map(nodes.map((node) => [node.id, node])); + const nodeMapB = new Map(secondaryNodes.map((node) => [node.id, node])); + + tracker.initialize("Flip Equivalent Trees", { rootId, secondaryRootId }); + + function flipEquiv(idA: string | null, idB: string | null): boolean { + if (!idA && !idB) return true; + if (!idA || !idB) return false; + + const nodeA = nodeMapA.get(idA); + const nodeB = nodeMapB.get(idB); + + if (!nodeA || !nodeB) return false; + + tracker.compareNodes(idA, idB, { + idA, + idB, + valueA: nodeA.value, + valueB: nodeB.value, + }); + + if (nodeA.value !== nodeB.value) return false; + + const noFlip = + flipEquiv(nodeA.leftChildId, nodeB.leftChildId) && + flipEquiv(nodeA.rightChildId, nodeB.rightChildId); + + const withFlip = + flipEquiv(nodeA.leftChildId, nodeB.rightChildId) && + flipEquiv(nodeA.rightChildId, nodeB.leftChildId); + + const result = noFlip || withFlip; + + if (result) { + tracker.markProcessed(idA, { idA, idB, equivalent: true, flipped: withFlip && !noFlip }); + } + + return result; + } + + const result = flipEquiv(rootId, secondaryRootId); + tracker.complete(result, { result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/InvertBinaryTreeIterativePipeline.stories.tsx b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/InvertBinaryTreeIterativePipeline.stories.tsx new file mode 100644 index 00000000..09d12983 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/InvertBinaryTreeIterativePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Invert Binary Tree Iterative algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key BFS-level manipulation states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateInvertBinaryTreeIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Invert Binary Tree Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before BFS inversion begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some levels swapped via BFS */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — tree fully inverted iteratively */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/educational.ts b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/educational.ts new file mode 100644 index 00000000..8dd0e5e9 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const invertBinaryTreeIterativeEducational: EducationalContent = { + overview: + "**Invert Binary Tree (Iterative)** mirrors a binary tree using a BFS queue, swapping left and right children level by level. This avoids the recursive call stack entirely, making it safe for very deep trees that would otherwise risk a stack overflow.", + + howItWorks: + "The algorithm uses a BFS queue approach:\n\n" + + "1. **Initialize** — add the root node to the queue.\n" + + "2. **Dequeue** — take the front node from the queue.\n" + + "3. **Swap** — exchange the node's left and right child pointers.\n" + + "4. **Enqueue children** — add the non-null children to the queue.\n" + + "5. **Repeat** — continue until the queue is empty.\n\n" + + "Processing level by level means all nodes at depth `d` are swapped before moving to depth `d+1`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is dequeued exactly once and its children swapped in `O(1)` time.\n\n" + + "**Space Complexity: `O(w)` where `w` is the maximum tree width**\n\n" + + "The BFS queue holds at most one full level of the tree. For balanced trees, the maximum width is `n/2` at the leaf level, so space is `O(n)` in the worst case.", + + bestAndWorstCase: + "**Best case** — a single-node or skewed tree: queue holds at most one node at a time — `O(1)` space.\n\n" + + "**Worst case** — a complete binary tree: the bottom level has `n/2` nodes in the queue simultaneously — `O(n)` space.\n\n" + + "Time is always `O(n)` for any tree shape.", + + realWorldUses: [ + "**Memory-safe mirroring** — Invert large trees where recursive stack depth would be a risk.", + "**Level-by-level visualization** — BFS order makes it easy to animate the inversion per level.", + "**Parallel processing** — Each level's swaps are independent and can be parallelized.", + "**Iterators and streaming** — BFS naturally fits iterator-based or streaming tree processing APIs.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — safe for arbitrarily deep trees without stack overflow risk.", + "BFS order makes it easy to observe the inversion progress level by level.", + "Straightforward queue-based implementation widely understood by engineers.", + ], + limitations: [ + "Uses `O(w)` extra memory for the queue, which can be `O(n)` for wide trees.", + "Slightly more verbose than the elegant single-line recursive version.", + "BFS traversal order may be less intuitive than post-order recursion for tree problems.", + ], + }, + + whenToUseIt: + "Prefer the iterative BFS variant over recursion when working with trees that may be very deep or have unpredictable depth. Use the recursive version for shallow trees where code clarity is more important than stack safety.", +}; diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/index.ts b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/index.ts new file mode 100644 index 00000000..2ab8dadb --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/index.ts @@ -0,0 +1,131 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { invertBinaryTreeIterative } from "./sources/invert-binary-tree-iterative.ts?fn"; +import { generateInvertBinaryTreeIterativeSteps } from "./step-generator"; +import type { InvertBinaryTreeIterativeInput } from "./step-generator"; +import { invertBinaryTreeIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/invert-binary-tree-iterative.ts?raw"; +import pythonSource from "./sources/invert-binary-tree-iterative.py?raw"; +import javaSource from "./sources/InvertBinaryTreeIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeInvertBinaryTreeIterative(input: InvertBinaryTreeIterativeInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + const inverted = invertBinaryTreeIterative(root) as BinaryNode | null; + const result: number[] = []; + const queue: (BinaryNode | null)[] = [inverted]; + while (queue.length > 0) { + const current = queue.shift(); + if (current != null) { + result.push(current.value); + queue.push(current.left, current.right); + } + } + return result; +} + +const invertBinaryTreeIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.INVERT_BINARY_TREE_ITERATIVE!, + name: "Invert Binary Tree (Iterative)", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Iteratively mirrors a binary tree using a BFS queue, swapping left and right children level by level without recursion", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeInvertBinaryTreeIterative, + generateSteps: generateInvertBinaryTreeIterativeSteps, + educational: invertBinaryTreeIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(invertBinaryTreeIterativeDefinition); diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/invert-binary-tree-iterative.test.ts b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/invert-binary-tree-iterative.test.ts new file mode 100644 index 00000000..8a6b6253 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/invert-binary-tree-iterative.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "vitest"; +import { invertBinaryTreeIterative } from "./sources/invert-binary-tree-iterative.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectLevelOrder(root: BinaryNode | null): number[] { + if (!root) return []; + const result: number[] = []; + const queue: BinaryNode[] = [root]; + while (queue.length > 0) { + const current = queue.shift()!; + result.push(current.value); + if (current.left) queue.push(current.left); + if (current.right) queue.push(current.right); + } + return result; +} + +describe("invertBinaryTreeIterative", () => { + it("returns null for a null root", () => { + expect(invertBinaryTreeIterative(null)).toBeNull(); + }); + + it("returns the same single node for a single-node tree", () => { + const root = makeNode(1); + const result = invertBinaryTreeIterative(root) as BinaryNode; + expect(result.value).toBe(1); + expect(result.left).toBeNull(); + expect(result.right).toBeNull(); + }); + + it("swaps children of a two-node tree", () => { + const root = makeNode(1, makeNode(2)); + const result = invertBinaryTreeIterative(root) as BinaryNode; + expect(result.left).toBeNull(); + expect(result.right?.value).toBe(2); + }); + + it("inverts a balanced 7-node BST in level-order", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = invertBinaryTreeIterative(root) as BinaryNode; + expect(collectLevelOrder(result)).toEqual([4, 6, 2, 7, 5, 3, 1]); + }); + + it("inverts a right-skewed tree to left-skewed", () => { + const root = makeNode(1, null, makeNode(2, null, makeNode(3))); + const result = invertBinaryTreeIterative(root) as BinaryNode; + expect(result.left?.value).toBe(2); + expect(result.left?.left?.value).toBe(3); + expect(result.right).toBeNull(); + }); + + it("double inversion restores the original tree level-order", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const originalOrder = collectLevelOrder(root); + invertBinaryTreeIterative(root); + invertBinaryTreeIterative(root); + expect(collectLevelOrder(root)).toEqual(originalOrder); + }); +}); diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/InvertBinaryTreeIterative.java b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/InvertBinaryTreeIterative.java new file mode 100644 index 00000000..43c2a9a2 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/InvertBinaryTreeIterative.java @@ -0,0 +1,33 @@ +// Invert Binary Tree Iterative — BFS with queue: swap children level by level +import java.util.LinkedList; +import java.util.Queue; + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class InvertBinaryTreeIterative { + public BinaryNode invertBinaryTreeIterative(BinaryNode root) { + if (root == null) return null; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(root); // @step:initialize + + while (!queue.isEmpty()) { // @step:initialize + BinaryNode current = queue.poll(); // @step:dequeue + + // Swap left and right children + BinaryNode temp = current.left; // @step:swap-children + current.left = current.right; // @step:swap-children + current.right = temp; // @step:swap-children + + // Enqueue non-null children for processing + if (current.left != null) queue.offer(current.left); // @step:enqueue + if (current.right != null) queue.offer(current.right); // @step:enqueue + } + + return root; // @step:complete + } +} diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/invert-binary-tree-iterative.py b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/invert-binary-tree-iterative.py new file mode 100644 index 00000000..5a9a6296 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/invert-binary-tree-iterative.py @@ -0,0 +1,28 @@ +# Invert Binary Tree Iterative — BFS with queue: swap children level by level +from collections import deque + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def invert_binary_tree_iterative(root) -> 'BinaryNode | None': + if root is None: # @step:initialize + return None + + queue = deque([root]) # @step:initialize + + while queue: # @step:initialize + current = queue.popleft() # @step:dequeue + + # Swap left and right children + current.left, current.right = current.right, current.left # @step:swap-children + + # Enqueue non-null children for processing + if current.left is not None: # @step:enqueue + queue.append(current.left) + if current.right is not None: # @step:enqueue + queue.append(current.right) + + return root # @step:complete diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/invert-binary-tree-iterative.ts b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/invert-binary-tree-iterative.ts new file mode 100644 index 00000000..054a8e85 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/sources/invert-binary-tree-iterative.ts @@ -0,0 +1,29 @@ +// Invert Binary Tree Iterative — BFS with queue: swap children level by level + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function invertBinaryTreeIterative(root: BinaryNode | null): BinaryNode | null { + if (root === null) return null; // @step:initialize + + const queue: BinaryNode[] = [root]; // @step:initialize + + while (queue.length > 0) { + // @step:initialize + const current = queue.shift()!; // @step:dequeue + + // Swap left and right children + const temp = current.left; // @step:swap-children + current.left = current.right; // @step:swap-children + current.right = temp; // @step:swap-children + + // Enqueue non-null children for processing + if (current.left !== null) queue.push(current.left); // @step:enqueue + if (current.right !== null) queue.push(current.right); // @step:enqueue + } + + return root; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/step-generator.test.ts b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/step-generator.test.ts new file mode 100644 index 00000000..88e28a04 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateInvertBinaryTreeIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateInvertBinaryTreeIterativeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces swap-children steps for each node", () => { + const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const swapSteps = steps.filter((step) => step.type === "swap-children"); + expect(swapSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateInvertBinaryTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/invert-binary-tree-iterative/step-generator.ts b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/step-generator.ts new file mode 100644 index 00000000..c6e73333 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree-iterative/step-generator.ts @@ -0,0 +1,57 @@ +/** Step generator for Invert Binary Tree Iterative — BFS level-by-level swap. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const INVERT_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.INVERT_BINARY_TREE_ITERATIVE!, +); + +export interface InvertBinaryTreeIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateInvertBinaryTreeIterativeSteps( + input: InvertBinaryTreeIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, INVERT_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Invert Binary Tree (Iterative BFS)", { rootId, queueSize: 1 }); + + const queue: string[] = [rootId]; + + while (queue.length > 0) { + const currentId = queue.shift(); + if (!currentId) continue; + const currentNode = nodeMap.get(currentId); + if (!currentNode) continue; + + const leftId = currentNode.leftChildId; + const rightId = currentNode.rightChildId; + + tracker.swapChildren(currentId, { + nodeId: currentId, + leftChildId: rightId, + rightChildId: leftId, + queueSize: queue.length, + }); + + // Update local map to reflect swap + currentNode.leftChildId = rightId; + currentNode.rightChildId = leftId; + + tracker.markProcessed(currentId, { nodeId: currentId, value: currentNode.value }); + + // Enqueue children after swap (so we process them in their new positions) + if (currentNode.leftChildId) queue.push(currentNode.leftChildId); + if (currentNode.rightChildId) queue.push(currentNode.rightChildId); + } + + tracker.complete(null, { result: "inverted" }); + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/InvertBinaryTreePipeline.stories.tsx b/src/algorithms/trees/manipulation/invert-binary-tree/InvertBinaryTreePipeline.stories.tsx new file mode 100644 index 00000000..658181b5 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/InvertBinaryTreePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Invert Binary Tree algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key manipulation states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateInvertBinaryTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Invert Binary Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before inversion begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some nodes swapped */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — tree fully inverted */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/educational.ts b/src/algorithms/trees/manipulation/invert-binary-tree/educational.ts new file mode 100644 index 00000000..d06b900f --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const invertBinaryTreeEducational: EducationalContent = { + overview: + "**Invert Binary Tree** mirrors a binary tree by recursively swapping the left and right children of every node. The result is a reflection of the original tree — the left subtree becomes the right subtree and vice versa at every level.\n\nThis is the classic algorithm famously associated with the 2015 tweet claiming it was asked in a Google interview, making it one of the most recognized tree manipulation problems.", + + howItWorks: + "The algorithm uses a post-order recursive approach:\n\n" + + "1. **Base case** — if the node is `null`, return immediately.\n" + + "2. **Recurse left** — invert the left subtree completely.\n" + + "3. **Recurse right** — invert the right subtree completely.\n" + + "4. **Swap** — exchange the left and right child pointers of the current node.\n\n" + + "### Example: Inverting a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + " style n4 fill:#06b6d4,stroke:#0891b2\n" + + "```\n\n" + + "After inversion: root 4 has right=2, left=6; node 2 has right=1, left=3; node 6 has right=5, left=7.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once to perform the swap.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "Recursive call stack depth equals tree height. For balanced trees, `h = O(log n)`; for skewed trees, `h = O(n)`.", + + bestAndWorstCase: + "**Best case** — balanced tree: `O(log n)` call stack depth.\n\n" + + "**Worst case** — completely skewed tree (all nodes in one direction): `O(n)` call stack depth.\n\n" + + "Time is always `O(n)` regardless of tree shape since every node must be swapped.", + + realWorldUses: [ + "**UI rendering** — Mirror layouts horizontally for right-to-left language support.", + "**Image processing** — Flip binary space partition trees to mirror regions.", + "**Game development** — Reflect AI decision trees for mirrored scenarios.", + "**Data normalization** — Standardize tree representations for canonical comparisons.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple and elegant recursive implementation — directly models the problem definition.", + "In-place modification avoids allocating a separate result tree.", + "Post-order traversal ensures children are processed before parents.", + ], + limitations: [ + "Modifies the original tree — caller must clone if the original must be preserved.", + "Recursive version may stack overflow on very deep skewed trees.", + "Not useful for BSTs if BST ordering properties need to be maintained after inversion.", + ], + }, + + whenToUseIt: + "Use invert-binary-tree whenever you need a mirrored copy of a tree in-place. For immutable trees, create a cloned inverted tree instead. The iterative BFS variant is preferred for very deep trees to avoid stack overflow.", +}; diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/index.ts b/src/algorithms/trees/manipulation/invert-binary-tree/index.ts new file mode 100644 index 00000000..bae298ff --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { invertBinaryTree } from "./sources/invert-binary-tree.ts?fn"; +import { generateInvertBinaryTreeSteps } from "./step-generator"; +import type { InvertBinaryTreeInput } from "./step-generator"; +import { invertBinaryTreeEducational } from "./educational"; + +import typescriptSource from "./sources/invert-binary-tree.ts?raw"; +import pythonSource from "./sources/invert-binary-tree.py?raw"; +import javaSource from "./sources/InvertBinaryTree.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeInvertBinaryTree(input: InvertBinaryTreeInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + const inverted = invertBinaryTree(root) as BinaryNode | null; + // Collect values in level-order for result + const result: number[] = []; + const queue: (BinaryNode | null)[] = [inverted]; + while (queue.length > 0) { + const current = queue.shift(); + if (current != null) { + result.push(current.value); + queue.push(current.left, current.right); + } + } + return result; +} + +const invertBinaryTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.INVERT_BINARY_TREE!, + name: "Invert Binary Tree", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursively mirrors a binary tree by swapping left and right children at every node, producing a reflected version of the original tree", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeInvertBinaryTree, + generateSteps: generateInvertBinaryTreeSteps, + educational: invertBinaryTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(invertBinaryTreeDefinition); diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/invert-binary-tree.test.ts b/src/algorithms/trees/manipulation/invert-binary-tree/invert-binary-tree.test.ts new file mode 100644 index 00000000..a2e47d04 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/invert-binary-tree.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "vitest"; +import { invertBinaryTree } from "./sources/invert-binary-tree.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectLevelOrder(root: BinaryNode | null): number[] { + if (!root) return []; + const result: number[] = []; + const queue: BinaryNode[] = [root]; + while (queue.length > 0) { + const current = queue.shift()!; + result.push(current.value); + if (current.left) queue.push(current.left); + if (current.right) queue.push(current.right); + } + return result; +} + +describe("invertBinaryTree", () => { + it("returns null for a null root", () => { + expect(invertBinaryTree(null)).toBeNull(); + }); + + it("returns the same single node for a single-node tree", () => { + const root = makeNode(1); + const result = invertBinaryTree(root) as BinaryNode; + expect(result.value).toBe(1); + expect(result.left).toBeNull(); + expect(result.right).toBeNull(); + }); + + it("swaps children of a two-node tree", () => { + const root = makeNode(1, makeNode(2)); + const result = invertBinaryTree(root) as BinaryNode; + expect(result.left).toBeNull(); + expect(result.right?.value).toBe(2); + }); + + it("inverts a balanced 7-node BST in level-order", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = invertBinaryTree(root) as BinaryNode; + expect(collectLevelOrder(result)).toEqual([4, 6, 2, 7, 5, 3, 1]); + }); + + it("inverts a right-skewed tree to left-skewed", () => { + const root = makeNode(1, null, makeNode(2, null, makeNode(3))); + const result = invertBinaryTree(root) as BinaryNode; + expect(result.left?.value).toBe(2); + expect(result.left?.left?.value).toBe(3); + expect(result.right).toBeNull(); + }); + + it("double inversion restores the original tree", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const originalOrder = collectLevelOrder(root); + invertBinaryTree(root); + invertBinaryTree(root); + expect(collectLevelOrder(root)).toEqual(originalOrder); + }); +}); diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/sources/InvertBinaryTree.java b/src/algorithms/trees/manipulation/invert-binary-tree/sources/InvertBinaryTree.java new file mode 100644 index 00000000..85f4bd53 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/sources/InvertBinaryTree.java @@ -0,0 +1,24 @@ +// Invert Binary Tree — recursive: swap left and right children at every node + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class InvertBinaryTree { + public BinaryNode invertBinaryTree(BinaryNode root) { + if (root == null) return null; // @step:initialize + + // Recursively invert the left subtree + BinaryNode invertedLeft = invertBinaryTree(root.left); // @step:traverse-left + // Recursively invert the right subtree + BinaryNode invertedRight = invertBinaryTree(root.right); // @step:traverse-right + + // Swap left and right children + root.left = invertedRight; // @step:swap-children + root.right = invertedLeft; // @step:swap-children + + return root; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/sources/invert-binary-tree.py b/src/algorithms/trees/manipulation/invert-binary-tree/sources/invert-binary-tree.py new file mode 100644 index 00000000..e96b3941 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/sources/invert-binary-tree.py @@ -0,0 +1,22 @@ +# Invert Binary Tree — recursive: swap left and right children at every node + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def invert_binary_tree(root) -> 'BinaryNode | None': + if root is None: # @step:initialize + return None + + # Recursively invert the left subtree + inverted_left = invert_binary_tree(root.left) # @step:traverse-left + # Recursively invert the right subtree + inverted_right = invert_binary_tree(root.right) # @step:traverse-right + + # Swap left and right children + root.left = inverted_right # @step:swap-children + root.right = inverted_left # @step:swap-children + + return root # @step:visit diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/sources/invert-binary-tree.ts b/src/algorithms/trees/manipulation/invert-binary-tree/sources/invert-binary-tree.ts new file mode 100644 index 00000000..a9b5268a --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/sources/invert-binary-tree.ts @@ -0,0 +1,22 @@ +// Invert Binary Tree — recursive: swap left and right children at every node + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function invertBinaryTree(root: BinaryNode | null): BinaryNode | null { + if (root === null) return null; // @step:initialize + + // Recursively invert the left subtree + const invertedLeft = invertBinaryTree(root.left); // @step:traverse-left + // Recursively invert the right subtree + const invertedRight = invertBinaryTree(root.right); // @step:traverse-right + + // Swap left and right children + root.left = invertedRight; // @step:swap-children + root.right = invertedLeft; // @step:swap-children + + return root; // @step:visit +} diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/step-generator.test.ts b/src/algorithms/trees/manipulation/invert-binary-tree/step-generator.test.ts new file mode 100644 index 00000000..fff91547 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateInvertBinaryTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateInvertBinaryTreeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("produces swap-children steps for each node", () => { + const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + const swapSteps = steps.filter((step) => step.type === "swap-children"); + expect(swapSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateInvertBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/invert-binary-tree/step-generator.ts b/src/algorithms/trees/manipulation/invert-binary-tree/step-generator.ts new file mode 100644 index 00000000..d6f0ab50 --- /dev/null +++ b/src/algorithms/trees/manipulation/invert-binary-tree/step-generator.ts @@ -0,0 +1,50 @@ +/** Step generator for Invert Binary Tree — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const INVERT_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.INVERT_BINARY_TREE!); + +export interface InvertBinaryTreeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateInvertBinaryTreeSteps(input: InvertBinaryTreeInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, INVERT_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Invert Binary Tree", { rootId }); + + function invert(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + // Recurse into left and right before swapping + invert(node.leftChildId); + invert(node.rightChildId); + + // Swap children + tracker.swapChildren(nodeId, { + nodeId, + leftChildId: node.rightChildId, + rightChildId: node.leftChildId, + }); + + // Update local map to reflect swap + const leftId = node.leftChildId; + node.leftChildId = node.rightChildId; + node.rightChildId = leftId; + + tracker.markProcessed(nodeId, { nodeId, value: node.value }); + } + + invert(rootId); + tracker.complete(null, { result: "inverted" }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/LowestCommonAncestorIterativePipeline.stories.tsx b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/LowestCommonAncestorIterativePipeline.stories.tsx new file mode 100644 index 00000000..853ebeff --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/LowestCommonAncestorIterativePipeline.stories.tsx @@ -0,0 +1,118 @@ +/** + * Storybook stories for the Lowest Common Ancestor Iterative algorithm pipeline. + * Uses the real step generator with a 7-node BST and targets 1 and 3, + * rendering the TreeVisualizer at key BFS parent-map and ancestor-trace states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateLowestCommonAncestorIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Lowest Common Ancestor Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before BFS parent-map construction begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation during BFS parent-map phase */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — LCA found via ancestor chain tracing */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/educational.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/educational.ts new file mode 100644 index 00000000..321d056e --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/educational.ts @@ -0,0 +1,50 @@ +import type { EducationalContent } from "@/types"; + +export const lowestCommonAncestorIterativeEducational: EducationalContent = { + overview: + "**Lowest Common Ancestor (Iterative)** finds the LCA without recursion using two phases: first build a parent map with BFS, then trace the ancestor chain of each target node until a common ancestor is found.\n\nThis is safe for arbitrarily deep trees and provides explicit access to the full ancestry chain.", + + howItWorks: + "The algorithm has two phases:\n\n" + + "**Phase 1 — Build parent map:**\n" + + "1. Initialize a `parentMap` with `root → null`.\n" + + "2. BFS through the tree, recording each node's parent in `parentMap`.\n" + + "3. Stop early once both target nodes are found.\n\n" + + "**Phase 2 — Trace ancestors:**\n" + + "4. Walk the ancestry chain from `nodeA` up to root, collecting all ancestors into a set.\n" + + "5. Walk the ancestry chain from `nodeB` upward; return the first node present in the set.\n\n" + + "For the default tree with targets 1 and 3, Phase 1 finds both targets and Phase 2 returns node 2 as the LCA.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "BFS visits at most all `n` nodes in Phase 1; the ancestor traces in Phase 2 are `O(h)` each.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The parent map stores an entry for every node visited during BFS.", + + bestAndWorstCase: + "**Best case** — both target nodes are near the root: BFS terminates early — sub-linear time.\n\n" + + "**Worst case** — targets are deep leaves on opposite sides: BFS visits all nodes — `O(n)` time and space.", + + realWorldUses: [ + "**Safe deep-tree LCA** — Avoid stack overflow for trees with thousands of levels.", + "**Parent-map reuse** — After building the map once, multiple LCA queries can be answered in `O(h)` each.", + "**Iterative ancestor traversal** — The ancestor chain from each node is explicitly available for inspection.", + "**Framework integration** — Iterative implementation fits naturally in callback-based or async tree processors.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — safe for arbitrarily deep trees.", + "Parent map can be reused for multiple LCA queries without re-traversal.", + "BFS early exit avoids visiting the entire tree when both targets are near the top.", + ], + limitations: [ + "`O(n)` space for the parent map — more memory than the recursive approach.", + "Two-phase algorithm is more complex than the simple recursive version.", + "Not beneficial for a single query on a shallow tree where the recursive version is simpler.", + ], + }, + + whenToUseIt: + "Use the iterative LCA when trees may be very deep (risking stack overflow) or when you need to reuse the parent map for multiple LCA queries. For shallow trees or single queries, the recursive version is simpler and more memory-efficient.", +}; diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/index.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/index.ts new file mode 100644 index 00000000..46c8441e --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/index.ts @@ -0,0 +1,129 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { lowestCommonAncestorIterative } from "./sources/lowest-common-ancestor-iterative.ts?fn"; +import { generateLowestCommonAncestorIterativeSteps } from "./step-generator"; +import type { LowestCommonAncestorIterativeInput } from "./step-generator"; +import { lowestCommonAncestorIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/lowest-common-ancestor-iterative.ts?raw"; +import pythonSource from "./sources/lowest-common-ancestor-iterative.py?raw"; +import javaSource from "./sources/LowestCommonAncestorIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeLowestCommonAncestorIterative( + input: LowestCommonAncestorIterativeInput, +): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + const result = lowestCommonAncestorIterative( + root, + input.nodeValueA, + input.nodeValueB, + ) as BinaryNode | null; + return result?.value ?? null; +} + +const lowestCommonAncestorIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.LOWEST_COMMON_ANCESTOR_ITERATIVE!, + name: "Lowest Common Ancestor (Iterative)", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "BFS builds a parent map, then traces ancestry chains from both target nodes until a common ancestor is found — no recursion required", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", nodeValueA: 1, nodeValueB: 3 }, + }, + execute: executeLowestCommonAncestorIterative, + generateSteps: generateLowestCommonAncestorIterativeSteps, + educational: lowestCommonAncestorIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, + }; + +registry.register(lowestCommonAncestorIterativeDefinition); diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/lowest-common-ancestor-iterative.test.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/lowest-common-ancestor-iterative.test.ts new file mode 100644 index 00000000..80522bba --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/lowest-common-ancestor-iterative.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { lowestCommonAncestorIterative } from "./sources/lowest-common-ancestor-iterative.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("lowestCommonAncestorIterative", () => { + it("returns null for a null root", () => { + expect(lowestCommonAncestorIterative(null, 1, 2)).toBeNull(); + }); + + it("returns root when root matches one of the targets", () => { + const root = makeNode(4, makeNode(2), makeNode(6)); + const result = lowestCommonAncestorIterative(root, 4, 6) as BinaryNode; + expect(result.value).toBe(4); + }); + + it("returns node 2 as LCA for targets 1 and 3 in 7-node BST", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = lowestCommonAncestorIterative(root, 1, 3) as BinaryNode; + expect(result.value).toBe(2); + }); + + it("returns root as LCA for targets in opposite subtrees", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = lowestCommonAncestorIterative(root, 3, 5) as BinaryNode; + expect(result.value).toBe(4); + }); + + it("returns the target node itself when one is ancestor of the other", () => { + const root = makeNode(4, makeNode(2, makeNode(1))); + const result = lowestCommonAncestorIterative(root, 2, 1) as BinaryNode; + expect(result.value).toBe(2); + }); +}); diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/LowestCommonAncestorIterative.java b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/LowestCommonAncestorIterative.java new file mode 100644 index 00000000..3f3d4804 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/LowestCommonAncestorIterative.java @@ -0,0 +1,58 @@ +// Lowest Common Ancestor Iterative — BFS to build parent map, then trace ancestors + +import java.util.*; + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class LowestCommonAncestorIterative { + public BinaryNode lowestCommonAncestorIterative(BinaryNode root, int nodeValueA, int nodeValueB) { + if (root == null) return null; // @step:initialize + + // Build parent map using BFS + Map parentMap = new HashMap<>(); // @step:initialize + parentMap.put(root, null); // @step:initialize + Queue bfsQueue = new LinkedList<>(); // @step:initialize + bfsQueue.add(root); // @step:initialize + + BinaryNode nodeA = null, nodeB = null; + + while (!bfsQueue.isEmpty() && (nodeA == null || nodeB == null)) { // @step:visit + BinaryNode current = bfsQueue.poll(); // @step:dequeue + + if (current.value == nodeValueA) nodeA = current; // @step:compare + if (current.value == nodeValueB) nodeB = current; // @step:compare + + if (current.left != null) { // @step:enqueue + parentMap.put(current.left, current); // @step:enqueue + bfsQueue.add(current.left); // @step:enqueue + } + if (current.right != null) { // @step:enqueue + parentMap.put(current.right, current); // @step:enqueue + bfsQueue.add(current.right); // @step:enqueue + } + } + + if (nodeA == null || nodeB == null) return null; + + // Trace ancestors of nodeA into a set + Set ancestorsA = new HashSet<>(); // @step:visit + BinaryNode traceNode = nodeA; + while (traceNode != null) { // @step:visit + ancestorsA.add(traceNode); // @step:visit + traceNode = parentMap.get(traceNode); // @step:visit + } + + // Walk ancestors of nodeB until we hit the first shared ancestor + traceNode = nodeB; + while (traceNode != null) { // @step:visit + if (ancestorsA.contains(traceNode)) return traceNode; // @step:compare + traceNode = parentMap.get(traceNode); // @step:visit + } + + return null; // @step:complete + } +} diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/lowest-common-ancestor-iterative.py b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/lowest-common-ancestor-iterative.py new file mode 100644 index 00000000..144688fb --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/lowest-common-ancestor-iterative.py @@ -0,0 +1,53 @@ +# Lowest Common Ancestor Iterative — BFS to build parent map, then trace ancestors + +from collections import deque + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def lowest_common_ancestor_iterative(root, node_value_a: int, node_value_b: int): + if root is None: # @step:initialize + return None + + # Build parent map using BFS + parent_map = {root: None} # @step:initialize + queue = deque([root]) # @step:initialize + node_a = None + node_b = None + + while queue and (node_a is None or node_b is None): # @step:visit + current = queue.popleft() # @step:dequeue + + if current.value == node_value_a: # @step:compare + node_a = current + if current.value == node_value_b: # @step:compare + node_b = current + + if current.left is not None: # @step:enqueue + parent_map[current.left] = current # @step:enqueue + queue.append(current.left) # @step:enqueue + if current.right is not None: # @step:enqueue + parent_map[current.right] = current # @step:enqueue + queue.append(current.right) # @step:enqueue + + if node_a is None or node_b is None: + return None + + # Trace ancestors of node_a into a set + ancestors_a = set() # @step:visit + trace = node_a + while trace is not None: # @step:visit + ancestors_a.add(trace) # @step:visit + trace = parent_map.get(trace) # @step:visit + + # Walk ancestors of node_b until we hit the first shared ancestor + trace = node_b + while trace is not None: # @step:visit + if trace in ancestors_a: # @step:compare + return trace + trace = parent_map.get(trace) # @step:visit + + return None # @step:complete diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/lowest-common-ancestor-iterative.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/lowest-common-ancestor-iterative.ts new file mode 100644 index 00000000..3e8af939 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/sources/lowest-common-ancestor-iterative.ts @@ -0,0 +1,64 @@ +// Lowest Common Ancestor Iterative — BFS to build parent map, then trace ancestors + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function lowestCommonAncestorIterative( + root: BinaryNode | null, + nodeValueA: number, + nodeValueB: number, +): BinaryNode | null { + if (root === null) return null; // @step:initialize + + // Build parent map using BFS + const parentMap = new Map(); // @step:initialize + parentMap.set(root, null); // @step:initialize + const bfsQueue: BinaryNode[] = [root]; // @step:initialize + + // BFS until we find both target nodes + let nodeA: BinaryNode | null = null; + let nodeB: BinaryNode | null = null; + + while (bfsQueue.length > 0 && (nodeA === null || nodeB === null)) { + // @step:visit + const current = bfsQueue.shift()!; // @step:dequeue + + if (current.value === nodeValueA) nodeA = current; // @step:compare + if (current.value === nodeValueB) nodeB = current; // @step:compare + + if (current.left !== null) { + // @step:enqueue + parentMap.set(current.left, current); // @step:enqueue + bfsQueue.push(current.left); // @step:enqueue + } + if (current.right !== null) { + // @step:enqueue + parentMap.set(current.right, current); // @step:enqueue + bfsQueue.push(current.right); // @step:enqueue + } + } + + if (nodeA === null || nodeB === null) return null; + + // Trace ancestors of nodeA into a set + const ancestorsA = new Set(); // @step:visit + let traceNode: BinaryNode | null = nodeA; + while (traceNode !== null) { + // @step:visit + ancestorsA.add(traceNode); // @step:visit + traceNode = parentMap.get(traceNode) ?? null; // @step:visit + } + + // Walk ancestors of nodeB until we hit the first ancestor also in ancestorsA + traceNode = nodeB; + while (traceNode !== null) { + // @step:visit + if (ancestorsA.has(traceNode)) return traceNode; // @step:compare + traceNode = parentMap.get(traceNode) ?? null; // @step:visit + } + + return null; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/step-generator.test.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/step-generator.test.ts new file mode 100644 index 00000000..d460dd23 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/step-generator.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateLowestCommonAncestorIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateLowestCommonAncestorIterativeSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateLowestCommonAncestorIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/step-generator.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/step-generator.ts new file mode 100644 index 00000000..0f98885b --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor-iterative/step-generator.ts @@ -0,0 +1,90 @@ +/** Step generator for Lowest Common Ancestor Iterative — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LCA_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.LOWEST_COMMON_ANCESTOR_ITERATIVE!, +); + +export interface LowestCommonAncestorIterativeInput { + nodes: TreeNode[]; + rootId: string; + nodeValueA: number; + nodeValueB: number; +} + +export function generateLowestCommonAncestorIterativeSteps( + input: LowestCommonAncestorIterativeInput, +): ExecutionStep[] { + const { nodes, rootId, nodeValueA, nodeValueB } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, LCA_ITERATIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Lowest Common Ancestor (Iterative)", { rootId, nodeValueA, nodeValueB }); + + // Phase 1: build parent map with BFS + const parentMap = new Map(); + parentMap.set(rootId, null); + const bfsQueue: string[] = [rootId]; + let foundNodeAId: string | null = null; + let foundNodeBId: string | null = null; + + while (bfsQueue.length > 0 && (!foundNodeAId || !foundNodeBId)) { + const currentId = bfsQueue.shift(); + if (!currentId) continue; + const currentNode = nodeMap.get(currentId); + if (!currentNode) continue; + + tracker.compareNodes(currentId, currentId, { + currentId, + value: currentNode.value, + nodeValueA, + nodeValueB, + }); + + if (currentNode.value === nodeValueA) foundNodeAId = currentId; + if (currentNode.value === nodeValueB) foundNodeBId = currentId; + + if (currentNode.leftChildId) { + parentMap.set(currentNode.leftChildId, currentId); + bfsQueue.push(currentNode.leftChildId); + } + if (currentNode.rightChildId) { + parentMap.set(currentNode.rightChildId, currentId); + bfsQueue.push(currentNode.rightChildId); + } + } + + // Phase 2: trace ancestors + let lcaId: string | null = null; + + if (foundNodeAId && foundNodeBId) { + const ancestorsA = new Set(); + let traceId: string | null = foundNodeAId; + while (traceId) { + ancestorsA.add(traceId); + traceId = parentMap.get(traceId) ?? null; + } + + traceId = foundNodeBId; + while (traceId) { + if (ancestorsA.has(traceId)) { + lcaId = traceId; + break; + } + traceId = parentMap.get(traceId) ?? null; + } + } + + if (lcaId) { + tracker.markProcessed(lcaId, { lcaId, value: nodeMap.get(lcaId)?.value }); + } + + const lcaValue = lcaId ? (nodeMap.get(lcaId)?.value ?? null) : null; + tracker.complete(null, { result: lcaValue, lcaNodeId: lcaId }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/LowestCommonAncestorPipeline.stories.tsx b/src/algorithms/trees/manipulation/lowest-common-ancestor/LowestCommonAncestorPipeline.stories.tsx new file mode 100644 index 00000000..5d4b0ac9 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/LowestCommonAncestorPipeline.stories.tsx @@ -0,0 +1,118 @@ +/** + * Storybook stories for the Lowest Common Ancestor algorithm pipeline. + * Uses the real step generator with a 7-node BST and targets 1 and 3, + * rendering the TreeVisualizer at key search states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateLowestCommonAncestorSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Lowest Common Ancestor", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before LCA search begins (targets: 1 and 3) */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some nodes visited during post-order search */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — LCA node identified */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/educational.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor/educational.ts new file mode 100644 index 00000000..a8c45631 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const lowestCommonAncestorEducational: EducationalContent = { + overview: + "**Lowest Common Ancestor (LCA)** finds the deepest node in a binary tree that is an ancestor of both a given node A and node B. A node is considered an ancestor of itself.\n\nThis is a fundamental tree problem with applications in computational biology, network routing, and data structures like segment trees.", + + howItWorks: + "The recursive post-order algorithm works bottom-up:\n\n" + + "1. **Base case** — if the current node is null, return null.\n" + + "2. **Found target** — if the current node's value equals `nodeValueA` or `nodeValueB`, return the current node.\n" + + "3. **Recurse left** — search the left subtree for either target.\n" + + "4. **Recurse right** — search the right subtree for either target.\n" + + "5. **Both found** — if both left and right return non-null, the current node is the LCA.\n" + + "6. **One found** — return whichever side is non-null (the other target must be in this subtree or is this node itself).\n\n" + + "For the default 7-node BST with targets 1 and 3, the LCA is node 2.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node may be visited in the worst case (when the targets are in separate deepest subtrees).\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The recursive call stack depth equals the tree height.", + + bestAndWorstCase: + "**Best case** — both targets are descendants of the root's children in separate subtrees — LCA is the root, found quickly.\n\n" + + "**Worst case** — both targets are in one long path to the deepest leaf — every node is visited, `O(n)` time and `O(n)` stack space.", + + realWorldUses: [ + "**Phylogenetics** — Find the most recent common ancestor of two species in an evolutionary tree.", + "**Version control** — Find the common ancestor commit of two branches in a DAG.", + "**Network routing** — Identify the lowest common router in a hierarchical network tree.", + "**DOM queries** — Find the nearest common container of two HTML elements.", + ], + + strengthsAndLimitations: { + strengths: [ + "Handles any binary tree — not limited to BSTs.", + "Single-pass `O(n)` algorithm — optimal time complexity.", + "Post-order traversal ensures children are fully processed before the parent makes a decision.", + ], + limitations: [ + "Recursive — may stack overflow on very deep trees.", + "Requires both nodes to be in the tree; does not validate their presence.", + "For repeated LCA queries on the same tree, preprocess with Euler tour + sparse table for `O(1)` per query.", + ], + }, + + whenToUseIt: + "Use the recursive LCA for one-off queries on general binary trees. For BSTs, the BST-specific LCA algorithm is more efficient. For repeated queries, preprocess the tree with a binary lifting or Euler tour approach.", +}; diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/index.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor/index.ts new file mode 100644 index 00000000..926a1bc1 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/index.ts @@ -0,0 +1,127 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { lowestCommonAncestor } from "./sources/lowest-common-ancestor.ts?fn"; +import { generateLowestCommonAncestorSteps } from "./step-generator"; +import type { LowestCommonAncestorInput } from "./step-generator"; +import { lowestCommonAncestorEducational } from "./educational"; + +import typescriptSource from "./sources/lowest-common-ancestor.ts?raw"; +import pythonSource from "./sources/lowest-common-ancestor.py?raw"; +import javaSource from "./sources/LowestCommonAncestor.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeLowestCommonAncestor(input: LowestCommonAncestorInput): number | null { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + const result = lowestCommonAncestor( + root, + input.nodeValueA, + input.nodeValueB, + ) as BinaryNode | null; + return result?.value ?? null; +} + +const lowestCommonAncestorDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.LOWEST_COMMON_ANCESTOR!, + name: "Lowest Common Ancestor", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursive post-order search that finds the deepest node in a general binary tree that is an ancestor of both given target node values", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", nodeValueA: 1, nodeValueB: 3 }, + }, + execute: executeLowestCommonAncestor, + generateSteps: generateLowestCommonAncestorSteps, + educational: lowestCommonAncestorEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(lowestCommonAncestorDefinition); diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/lowest-common-ancestor.test.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor/lowest-common-ancestor.test.ts new file mode 100644 index 00000000..f5967836 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/lowest-common-ancestor.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { lowestCommonAncestor } from "./sources/lowest-common-ancestor.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("lowestCommonAncestor", () => { + it("returns null for a null root", () => { + expect(lowestCommonAncestor(null, 1, 2)).toBeNull(); + }); + + it("returns root when root matches one of the targets", () => { + const root = makeNode(4, makeNode(2), makeNode(6)); + const result = lowestCommonAncestor(root, 4, 6) as BinaryNode; + expect(result.value).toBe(4); + }); + + it("returns node 2 as LCA for targets 1 and 3 in 7-node BST", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = lowestCommonAncestor(root, 1, 3) as BinaryNode; + expect(result.value).toBe(2); + }); + + it("returns root as LCA for targets in opposite subtrees", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const result = lowestCommonAncestor(root, 3, 5) as BinaryNode; + expect(result.value).toBe(4); + }); + + it("returns the target node itself when one is ancestor of the other", () => { + const root = makeNode(4, makeNode(2, makeNode(1))); + const result = lowestCommonAncestor(root, 2, 1) as BinaryNode; + expect(result.value).toBe(2); + }); +}); diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/LowestCommonAncestor.java b/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/LowestCommonAncestor.java new file mode 100644 index 00000000..7766d42b --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/LowestCommonAncestor.java @@ -0,0 +1,24 @@ +// Lowest Common Ancestor — recursive post-order: for general binary tree (not BST) + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class LowestCommonAncestor { + public BinaryNode lowestCommonAncestor(BinaryNode root, int nodeValueA, int nodeValueB) { + if (root == null) return null; // @step:initialize + if (root.value == nodeValueA || root.value == nodeValueB) return root; // @step:compare + + // Search left and right subtrees + BinaryNode leftResult = lowestCommonAncestor(root.left, nodeValueA, nodeValueB); // @step:traverse-left + BinaryNode rightResult = lowestCommonAncestor(root.right, nodeValueA, nodeValueB); // @step:traverse-right + + // If both sides found a target node, current node is the LCA + if (leftResult != null && rightResult != null) return root; // @step:visit + + // Otherwise return whichever side found a target node + return leftResult != null ? leftResult : rightResult; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/lowest-common-ancestor.py b/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/lowest-common-ancestor.py new file mode 100644 index 00000000..f729b211 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/lowest-common-ancestor.py @@ -0,0 +1,24 @@ +# Lowest Common Ancestor — recursive post-order: for general binary tree (not BST) + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def lowest_common_ancestor(root, node_value_a: int, node_value_b: int): + if root is None: # @step:initialize + return None + if root.value == node_value_a or root.value == node_value_b: # @step:compare + return root + + # Search left and right subtrees + left_result = lowest_common_ancestor(root.left, node_value_a, node_value_b) # @step:traverse-left + right_result = lowest_common_ancestor(root.right, node_value_a, node_value_b) # @step:traverse-right + + # If both sides found a target node, current node is the LCA + if left_result is not None and right_result is not None: # @step:visit + return root + + # Otherwise return whichever side found a target node + return left_result if left_result is not None else right_result # @step:visit diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/lowest-common-ancestor.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/lowest-common-ancestor.ts new file mode 100644 index 00000000..47d97293 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/sources/lowest-common-ancestor.ts @@ -0,0 +1,26 @@ +// Lowest Common Ancestor — recursive post-order: for general binary tree (not BST) + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function lowestCommonAncestor( + root: BinaryNode | null, + nodeValueA: number, + nodeValueB: number, +): BinaryNode | null { + if (root === null) return null; // @step:initialize + if (root.value === nodeValueA || root.value === nodeValueB) return root; // @step:compare + + // Search left and right subtrees + const leftResult = lowestCommonAncestor(root.left, nodeValueA, nodeValueB); // @step:traverse-left + const rightResult = lowestCommonAncestor(root.right, nodeValueA, nodeValueB); // @step:traverse-right + + // If both sides found a target node, current node is the LCA + if (leftResult !== null && rightResult !== null) return root; // @step:visit + + // Otherwise return whichever side found a target node + return leftResult !== null ? leftResult : rightResult; // @step:visit +} diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/step-generator.test.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor/step-generator.test.ts new file mode 100644 index 00000000..7f819516 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/step-generator.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateLowestCommonAncestorSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateLowestCommonAncestorSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateLowestCommonAncestorSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 3, + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/lowest-common-ancestor/step-generator.ts b/src/algorithms/trees/manipulation/lowest-common-ancestor/step-generator.ts new file mode 100644 index 00000000..29e2b994 --- /dev/null +++ b/src/algorithms/trees/manipulation/lowest-common-ancestor/step-generator.ts @@ -0,0 +1,64 @@ +/** Step generator for Lowest Common Ancestor — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LCA_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.LOWEST_COMMON_ANCESTOR!); + +export interface LowestCommonAncestorInput { + nodes: TreeNode[]; + rootId: string; + nodeValueA: number; + nodeValueB: number; +} + +export function generateLowestCommonAncestorSteps( + input: LowestCommonAncestorInput, +): ExecutionStep[] { + const { nodes, rootId, nodeValueA, nodeValueB } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, LCA_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Lowest Common Ancestor", { rootId, nodeValueA, nodeValueB }); + + let lcaNodeId: string | null = null; + + function findLCA(nodeId: string | null): string | null { + if (!nodeId) return null; + const currentNode = nodeMap.get(nodeId); + if (!currentNode) return null; + + tracker.compareNodes(nodeId, nodeId, { + nodeId, + value: currentNode.value, + nodeValueA, + nodeValueB, + }); + + if (currentNode.value === nodeValueA || currentNode.value === nodeValueB) { + tracker.markProcessed(nodeId, { nodeId, value: currentNode.value, isTarget: true }); + return nodeId; + } + + const leftResult = findLCA(currentNode.leftChildId); + const rightResult = findLCA(currentNode.rightChildId); + + if (leftResult && rightResult) { + lcaNodeId = nodeId; + tracker.markProcessed(nodeId, { nodeId, value: currentNode.value, isLCA: true }); + return nodeId; + } + + return leftResult ?? rightResult; + } + + const result = findLCA(rootId); + if (!lcaNodeId) lcaNodeId = result; + + const lcaValue = lcaNodeId ? (nodeMap.get(lcaNodeId)?.value ?? null) : null; + tracker.complete(null, { result: lcaValue, lcaNodeId }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/MergeBinaryTreesIterativePipeline.stories.tsx b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/MergeBinaryTreesIterativePipeline.stories.tsx new file mode 100644 index 00000000..4b1182c3 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/MergeBinaryTreesIterativePipeline.stories.tsx @@ -0,0 +1,184 @@ +/** + * Storybook stories for the Merge Binary Trees Iterative algorithm pipeline. + * Uses the real step generator with two 7-node trees, + * rendering the TreeVisualizer at key stack-based merging states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMergeBinaryTreesIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = [ + { + id: "m4", + value: 40, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 20, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 60, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 10, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 30, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 50, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 70, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMergeBinaryTreesIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Merge Binary Trees Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — both trees before stack-based merging begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some node pairs popped and merged */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all overlapping nodes merged iteratively */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/educational.ts b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/educational.ts new file mode 100644 index 00000000..37623d36 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const mergeBinaryTreesIterativeEducational: EducationalContent = { + overview: + "**Merge Binary Trees (Iterative)** uses an explicit stack of node pairs to merge two binary trees without recursion. At each step, a pair `(nodeA, nodeB)` is popped, their values are summed, and pairs of corresponding children are pushed for future processing.\n\nThis avoids stack overflow risk for very deep trees.", + + howItWorks: + "The algorithm uses a stack of paired nodes:\n\n" + + "1. **Initialize** — if Tree A is null, return Tree B. Otherwise, push `(rootA, rootB)` onto the stack.\n" + + "2. **Pop pair** — take the top pair `(nodeA, nodeB)` from the stack.\n" + + "3. **Merge value** — add `nodeB.value` to `nodeA.value`.\n" + + "4. **Handle right** — if `nodeA.right` is null, assign `nodeB.right`; otherwise push `(nodeA.right, nodeB.right)`.\n" + + "5. **Handle left** — if `nodeA.left` is null, assign `nodeB.left`; otherwise push `(nodeA.left, nodeB.left)`.\n" + + "6. **Repeat** — continue until the stack is empty.\n" + + "7. **Return** — return the modified Tree A.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(min(n, m))`**\n\n" + + "Only overlapping nodes require a merge step.\n\n" + + "**Space Complexity: `O(min(h₁, h₂))`** where `h₁` and `h₂` are the heights of the two trees\n\n" + + "The explicit stack holds at most one pair per depth level. Same asymptotic space as the recursive version's call stack, but no risk of call-stack overflow.", + + bestAndWorstCase: + "**Best case** — one tree is empty: returns the other tree in `O(1)` time.\n\n" + + "**Worst case** — identical-shaped trees: every node is processed — `O(n)` time and `O(h)` space.", + + realWorldUses: [ + "**Deep tree merging** — Safe for arbitrarily deep trees without recursion stack risk.", + "**Incremental merging** — Stack-based approach can be paused and resumed for cooperative multitasking.", + "**Streaming merge** — Easily adapted to merge trees produced by generators or lazy iterators.", + "**Game state merging** — Combine two game state trees iteratively during state reconciliation.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — safe for arbitrarily deep trees.", + "Explicit stack makes the merge order easy to visualize and trace.", + "Same time and space complexity as the recursive version without overflow risk.", + ], + limitations: [ + "More verbose than the elegant recursive version.", + "Stack allocates pair arrays for every pushed node, increasing GC pressure.", + "DFS stack order (LIFO) means the right subtree is processed before the left.", + ], + }, + + whenToUseIt: + "Prefer the iterative version when working with potentially very deep trees. For shallow or balanced trees, the recursive version is cleaner. Both have the same asymptotic complexity.", +}; diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/index.ts b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/index.ts new file mode 100644 index 00000000..62a87eb6 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/index.ts @@ -0,0 +1,200 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { mergeBinaryTreesIterative } from "./sources/merge-binary-trees-iterative.ts?fn"; +import { generateMergeBinaryTreesIterativeSteps } from "./step-generator"; +import type { MergeBinaryTreesIterativeInput } from "./step-generator"; +import { mergeBinaryTreesIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/merge-binary-trees-iterative.ts?raw"; +import pythonSource from "./sources/merge-binary-trees-iterative.py?raw"; +import javaSource from "./sources/MergeBinaryTreesIterative.java?raw"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const secondaryNodes: TreeNode[] = [ + { + id: "m4", + value: 40, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 20, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 60, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 10, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 30, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 50, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 70, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeMergeBinaryTreesIterative(input: MergeBinaryTreesIterativeInput): number[] { + function buildNode(nodeMap: Map, id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: buildNode(nodeMap, node.leftChildId), + right: buildNode(nodeMap, node.rightChildId), + }; + } + const mapA = new Map(input.nodes.map((node) => [node.id, node])); + const mapB = new Map(input.secondaryNodes.map((node) => [node.id, node])); + const merged = mergeBinaryTreesIterative( + buildNode(mapA, input.rootId), + buildNode(mapB, input.secondaryRootId), + ) as BinaryNode | null; + const result: number[] = []; + const bfsQueue: (BinaryNode | null)[] = [merged]; + while (bfsQueue.length > 0) { + const current = bfsQueue.shift(); + if (current != null) { + result.push(current.value); + bfsQueue.push(current.left, current.right); + } + } + return result; +} + +const mergeBinaryTreesIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MERGE_BINARY_TREES_ITERATIVE!, + name: "Merge Binary Trees (Iterative)", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Stack-based iterative merge of two binary trees by summing overlapping node values and adopting non-overlapping subtrees", + timeComplexity: { best: "O(1)", average: "O(min(n,m))", worst: "O(min(n,m))" }, + spaceComplexity: "O(min(h1,h2))", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", secondaryNodes, secondaryRootId: "m4" }, + }, + execute: executeMergeBinaryTreesIterative, + generateSteps: generateMergeBinaryTreesIterativeSteps, + educational: mergeBinaryTreesIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(mergeBinaryTreesIterativeDefinition); diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/merge-binary-trees-iterative.test.ts b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/merge-binary-trees-iterative.test.ts new file mode 100644 index 00000000..26ce5b17 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/merge-binary-trees-iterative.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import { mergeBinaryTreesIterative } from "./sources/merge-binary-trees-iterative.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectLevelOrder(root: BinaryNode | null): number[] { + if (!root) return []; + const result: number[] = []; + const queue: BinaryNode[] = [root]; + while (queue.length > 0) { + const current = queue.shift()!; + result.push(current.value); + if (current.left) queue.push(current.left); + if (current.right) queue.push(current.right); + } + return result; +} + +describe("mergeBinaryTreesIterative", () => { + it("returns tree B when tree A is null", () => { + const treeB = makeNode(1); + expect(mergeBinaryTreesIterative(null, treeB)).toBe(treeB); + }); + + it("returns tree A when tree B is null", () => { + const treeA = makeNode(1); + expect(mergeBinaryTreesIterative(treeA, null)).toBe(treeA); + }); + + it("sums values at the root for two single-node trees", () => { + const treeA = makeNode(3); + const treeB = makeNode(5); + const result = mergeBinaryTreesIterative(treeA, treeB) as BinaryNode; + expect(result.value).toBe(8); + }); + + it("merges two identical-shape 7-node trees by summing all values", () => { + const treeA = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const treeB = makeNode( + 40, + makeNode(20, makeNode(10), makeNode(30)), + makeNode(60, makeNode(50), makeNode(70)), + ); + const result = mergeBinaryTreesIterative(treeA, treeB) as BinaryNode; + expect(collectLevelOrder(result)).toEqual([44, 22, 66, 11, 33, 55, 77]); + }); + + it("keeps tree A nodes when tree B has no corresponding node", () => { + const treeA = makeNode(1, makeNode(2), null); + const treeB = makeNode(3); + const result = mergeBinaryTreesIterative(treeA, treeB) as BinaryNode; + expect(result.value).toBe(4); + expect(result.left?.value).toBe(2); + }); +}); diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/MergeBinaryTreesIterative.java b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/MergeBinaryTreesIterative.java new file mode 100644 index 00000000..b7a91042 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/MergeBinaryTreesIterative.java @@ -0,0 +1,46 @@ +// Merge Binary Trees Iterative — stack-based pair comparison and merge + +import java.util.*; + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class MergeBinaryTreesIterative { + public BinaryNode mergeBinaryTreesIterative(BinaryNode treeA, BinaryNode treeB) { + if (treeA == null) return treeB; // @step:initialize + + Deque stack = new ArrayDeque<>(); // @step:initialize + + if (treeB != null) { // @step:initialize + stack.push(new BinaryNode[]{treeA, treeB}); // @step:initialize + } + + while (!stack.isEmpty()) { // @step:visit + BinaryNode[] pair = stack.pop(); // @step:visit + BinaryNode nodeA = pair[0]; + BinaryNode nodeB = pair[1]; + + // Merge values + nodeA.value = nodeA.value + nodeB.value; // @step:merge-node + + // Handle right children + if (nodeA.right == null) { // @step:connect-child + nodeA.right = nodeB.right; // @step:connect-child + } else if (nodeB.right != null) { // @step:connect-child + stack.push(new BinaryNode[]{nodeA.right, nodeB.right}); // @step:enqueue + } + + // Handle left children + if (nodeA.left == null) { // @step:connect-child + nodeA.left = nodeB.left; // @step:connect-child + } else if (nodeB.left != null) { // @step:connect-child + stack.push(new BinaryNode[]{nodeA.left, nodeB.left}); // @step:enqueue + } + } + + return treeA; // @step:complete + } +} diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/merge-binary-trees-iterative.py b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/merge-binary-trees-iterative.py new file mode 100644 index 00000000..0ee900b8 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/merge-binary-trees-iterative.py @@ -0,0 +1,36 @@ +# Merge Binary Trees Iterative — stack-based pair comparison and merge + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def merge_binary_trees_iterative(tree_a, tree_b): + if tree_a is None: # @step:initialize + return tree_b + + stack = [] # @step:initialize + + if tree_b is not None: # @step:initialize + stack.append((tree_a, tree_b)) # @step:initialize + + while stack: # @step:visit + node_a, node_b = stack.pop() # @step:visit + + # Merge values + node_a.value = node_a.value + node_b.value # @step:merge-node + + # Handle right children + if node_a.right is None: # @step:connect-child + node_a.right = node_b.right # @step:connect-child + elif node_b.right is not None: # @step:connect-child + stack.append((node_a.right, node_b.right)) # @step:enqueue + + # Handle left children + if node_a.left is None: # @step:connect-child + node_a.left = node_b.left # @step:connect-child + elif node_b.left is not None: # @step:connect-child + stack.append((node_a.left, node_b.left)) # @step:enqueue + + return tree_a # @step:complete diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/merge-binary-trees-iterative.ts b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/merge-binary-trees-iterative.ts new file mode 100644 index 00000000..75e2ed61 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/sources/merge-binary-trees-iterative.ts @@ -0,0 +1,51 @@ +// Merge Binary Trees Iterative — stack-based pair comparison and merge + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function mergeBinaryTreesIterative( + treeA: BinaryNode | null, + treeB: BinaryNode | null, +): BinaryNode | null { + if (treeA === null) return treeB; // @step:initialize + + const stack: [BinaryNode, BinaryNode][] = []; // @step:initialize + + if (treeB !== null) { + // @step:initialize + stack.push([treeA, treeB]); // @step:initialize + } + + while (stack.length > 0) { + // @step:visit + const pair = stack.pop()!; // @step:visit + const nodeA = pair[0]; + const nodeB = pair[1]; + + // Merge values + nodeA.value = nodeA.value + nodeB.value; // @step:merge-node + + // Handle right children + if (nodeA.right === null) { + // @step:connect-child + nodeA.right = nodeB.right; // @step:connect-child + } else if (nodeB.right !== null) { + // @step:connect-child + stack.push([nodeA.right, nodeB.right]); // @step:enqueue + } + + // Handle left children + if (nodeA.left === null) { + // @step:connect-child + nodeA.left = nodeB.left; // @step:connect-child + } else if (nodeB.left !== null) { + // @step:connect-child + stack.push([nodeA.left, nodeB.left]); // @step:enqueue + } + } + + return treeA; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/step-generator.test.ts b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/step-generator.test.ts new file mode 100644 index 00000000..984423e9 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/step-generator.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMergeBinaryTreesIterativeSteps } from "./step-generator"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = [ + { + id: "m4", + value: 40, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 20, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 60, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 10, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 30, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 50, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 70, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMergeBinaryTreesIterativeSteps", () => { + it("produces steps for two 7-node trees", () => { + const steps = generateMergeBinaryTreesIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMergeBinaryTreesIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMergeBinaryTreesIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMergeBinaryTreesIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMergeBinaryTreesIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/merge-binary-trees-iterative/step-generator.ts b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/step-generator.ts new file mode 100644 index 00000000..6ac59fc0 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees-iterative/step-generator.ts @@ -0,0 +1,73 @@ +/** Step generator for Merge Binary Trees Iterative — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const MERGE_ITERATIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.MERGE_BINARY_TREES_ITERATIVE!, +); + +export interface MergeBinaryTreesIterativeInput { + nodes: TreeNode[]; + rootId: string; + secondaryNodes: TreeNode[]; + secondaryRootId: string; +} + +export function generateMergeBinaryTreesIterativeSteps( + input: MergeBinaryTreesIterativeInput, +): ExecutionStep[] { + const { nodes, rootId, secondaryNodes, secondaryRootId } = input; + const tracker = new TreeManipulationTracker( + nodes, + rootId, + MERGE_ITERATIVE_LINE_MAP, + secondaryNodes, + secondaryRootId, + ); + const nodeMapA = new Map(nodes.map((node) => [node.id, node])); + const nodeMapB = new Map(secondaryNodes.map((node) => [node.id, node])); + + tracker.initialize("Merge Binary Trees (Iterative)", { rootId, secondaryRootId }); + + const pairStack: [string, string][] = [[rootId, secondaryRootId]]; + + while (pairStack.length > 0) { + const pair = pairStack.pop(); + if (!pair) continue; + const [idA, idB] = pair; + + const nodeA = nodeMapA.get(idA); + const nodeB = nodeMapB.get(idB); + + if (!nodeA || !nodeB) continue; + + const mergedValue = nodeA.value + nodeB.value; + tracker.mergeNodes(idA, idB, mergedValue, { + idA, + idB, + valueA: nodeA.value, + valueB: nodeB.value, + mergedValue, + }); + + // Handle right children + if (!nodeA.rightChildId && nodeB.rightChildId) { + tracker.markProcessed(idA, { idA, side: "right", adopted: nodeB.rightChildId }); + } else if (nodeA.rightChildId && nodeB.rightChildId) { + pairStack.push([nodeA.rightChildId, nodeB.rightChildId]); + } + + // Handle left children + if (!nodeA.leftChildId && nodeB.leftChildId) { + tracker.markProcessed(idA, { idA, side: "left", adopted: nodeB.leftChildId }); + } else if (nodeA.leftChildId && nodeB.leftChildId) { + pairStack.push([nodeA.leftChildId, nodeB.leftChildId]); + } + } + + tracker.complete(null, { result: "merged" }); + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/MergeBinaryTreesPipeline.stories.tsx b/src/algorithms/trees/manipulation/merge-binary-trees/MergeBinaryTreesPipeline.stories.tsx new file mode 100644 index 00000000..78e0df88 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/MergeBinaryTreesPipeline.stories.tsx @@ -0,0 +1,184 @@ +/** + * Storybook stories for the Merge Binary Trees algorithm pipeline. + * Uses the real step generator with a 7-node BST (values 1-7) and second tree (values 10-70), + * rendering the TreeVisualizer at key merging states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMergeBinaryTreesSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = [ + { + id: "m4", + value: 40, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 20, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 60, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 10, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 30, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 50, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 70, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMergeBinaryTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Merge Binary Trees", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — both trees before merging begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some nodes merged and values summed */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all overlapping nodes merged with summed values */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/educational.ts b/src/algorithms/trees/manipulation/merge-binary-trees/educational.ts new file mode 100644 index 00000000..55ceeb8b --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const mergeBinaryTreesEducational: EducationalContent = { + overview: + "**Merge Binary Trees** overlays two binary trees: where both trees have nodes at the same position, their values are summed; where only one tree has a node, that node is used as-is.\n\nImagine placing two trees on top of each other — nodes that overlap are combined, and nodes that stick out on one side are kept.", + + howItWorks: + "The recursive algorithm handles three cases at each position:\n\n" + + "1. **Tree A is null** — return Tree B's node directly.\n" + + "2. **Tree B is null** — return Tree A's node directly.\n" + + "3. **Both exist** — sum their values into Tree A's node, then recursively merge left and right subtrees.\n\n" + + "The algorithm modifies Tree A in-place. After merging the default trees (A: values 1–7, B: values 10–70), every node value in the result is the sum of the corresponding positions.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(min(n, m))`** where `n` and `m` are the sizes of the two trees\n\n" + + "Only the overlapping nodes require a merge step; non-overlapping subtrees are returned directly.\n\n" + + "**Space Complexity: `O(min(h₁, h₂))`** where `h₁` and `h₂` are the heights of the two trees\n\n" + + "The recursion depth is bounded by the height of the shallower tree.", + + bestAndWorstCase: + "**Best case** — one tree is empty: returns the other tree in `O(1)` time.\n\n" + + "**Worst case** — identical-shaped trees: every node is visited — `O(n)` time.", + + realWorldUses: [ + "**Conflict-free data merging** — Combine distributed tree-structured data by summing at overlap points.", + "**Game maps** — Merge two tile maps by summing overlapping tile values.", + "**Priority combining** — Combine two priority trees where shared positions add their weights.", + "**Sensor fusion** — Aggregate tree-structured sensor readings from two data sources.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple three-case recursive structure directly mirrors the problem definition.", + "In-place modification of Tree A avoids allocating a new tree.", + "Short-circuits by reusing non-overlapping subtrees without traversing them.", + ], + limitations: [ + "Modifies Tree A in-place — clone if the original must be preserved.", + "Recursive — may stack overflow for very deep trees.", + "Not suitable when the merge rule is more complex than simple value addition.", + ], + }, + + whenToUseIt: + "Use merge-binary-trees when you need to overlay two trees and combine overlapping nodes. For a non-destructive merge that creates a new tree, allocate new nodes instead of modifying Tree A. Use the iterative variant for deep trees.", +}; diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/index.ts b/src/algorithms/trees/manipulation/merge-binary-trees/index.ts new file mode 100644 index 00000000..7f8f5094 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/index.ts @@ -0,0 +1,202 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { mergeBinaryTrees } from "./sources/merge-binary-trees.ts?fn"; +import { generateMergeBinaryTreesSteps } from "./step-generator"; +import type { MergeBinaryTreesInput } from "./step-generator"; +import { mergeBinaryTreesEducational } from "./educational"; + +import typescriptSource from "./sources/merge-binary-trees.ts?raw"; +import pythonSource from "./sources/merge-binary-trees.py?raw"; +import javaSource from "./sources/MergeBinaryTrees.java?raw"; + +/** Tree A: standard 7-node balanced BST with values 1–7 */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Tree B: same structure but values 40, 20, 60, 10, 30, 50, 70 */ +const secondaryNodes: TreeNode[] = [ + { + id: "m4", + value: 40, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 20, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 60, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 10, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 30, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 50, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 70, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeMergeBinaryTrees(input: MergeBinaryTreesInput): number[] { + function buildNode(nodeMap: Map, id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: buildNode(nodeMap, node.leftChildId), + right: buildNode(nodeMap, node.rightChildId), + }; + } + const mapA = new Map(input.nodes.map((node) => [node.id, node])); + const mapB = new Map(input.secondaryNodes.map((node) => [node.id, node])); + const merged = mergeBinaryTrees( + buildNode(mapA, input.rootId), + buildNode(mapB, input.secondaryRootId), + ) as BinaryNode | null; + const result: number[] = []; + const bfsQueue: (BinaryNode | null)[] = [merged]; + while (bfsQueue.length > 0) { + const current = bfsQueue.shift(); + if (current != null) { + result.push(current.value); + bfsQueue.push(current.left, current.right); + } + } + return result; +} + +const mergeBinaryTreesDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MERGE_BINARY_TREES!, + name: "Merge Binary Trees", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursively overlays two binary trees by summing values at overlapping positions and keeping non-overlapping nodes as-is", + timeComplexity: { best: "O(1)", average: "O(min(n,m))", worst: "O(min(n,m))" }, + spaceComplexity: "O(min(h1,h2))", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", secondaryNodes, secondaryRootId: "m4" }, + }, + execute: executeMergeBinaryTrees, + generateSteps: generateMergeBinaryTreesSteps, + educational: mergeBinaryTreesEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(mergeBinaryTreesDefinition); diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/merge-binary-trees.test.ts b/src/algorithms/trees/manipulation/merge-binary-trees/merge-binary-trees.test.ts new file mode 100644 index 00000000..595617ca --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/merge-binary-trees.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import { mergeBinaryTrees } from "./sources/merge-binary-trees.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +function collectLevelOrder(root: BinaryNode | null): number[] { + if (!root) return []; + const result: number[] = []; + const queue: BinaryNode[] = [root]; + while (queue.length > 0) { + const current = queue.shift()!; + result.push(current.value); + if (current.left) queue.push(current.left); + if (current.right) queue.push(current.right); + } + return result; +} + +describe("mergeBinaryTrees", () => { + it("returns tree B when tree A is null", () => { + const treeB = makeNode(1); + expect(mergeBinaryTrees(null, treeB)).toBe(treeB); + }); + + it("returns tree A when tree B is null", () => { + const treeA = makeNode(1); + expect(mergeBinaryTrees(treeA, null)).toBe(treeA); + }); + + it("sums values at the root for two single-node trees", () => { + const treeA = makeNode(3); + const treeB = makeNode(5); + const result = mergeBinaryTrees(treeA, treeB) as BinaryNode; + expect(result.value).toBe(8); + }); + + it("merges two identical-shape 7-node trees by summing all values", () => { + const treeA = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const treeB = makeNode( + 40, + makeNode(20, makeNode(10), makeNode(30)), + makeNode(60, makeNode(50), makeNode(70)), + ); + const result = mergeBinaryTrees(treeA, treeB) as BinaryNode; + expect(collectLevelOrder(result)).toEqual([44, 22, 66, 11, 33, 55, 77]); + }); + + it("keeps tree A nodes when tree B has no corresponding node", () => { + const treeA = makeNode(1, makeNode(2), null); + const treeB = makeNode(3); + const result = mergeBinaryTrees(treeA, treeB) as BinaryNode; + expect(result.value).toBe(4); + expect(result.left?.value).toBe(2); + }); +}); diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/sources/MergeBinaryTrees.java b/src/algorithms/trees/manipulation/merge-binary-trees/sources/MergeBinaryTrees.java new file mode 100644 index 00000000..b90b465c --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/sources/MergeBinaryTrees.java @@ -0,0 +1,23 @@ +// Merge Binary Trees — recursive: if both nodes exist, sum values; otherwise take non-null node + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class MergeBinaryTrees { + public BinaryNode mergeBinaryTrees(BinaryNode treeA, BinaryNode treeB) { + if (treeA == null) return treeB; // @step:initialize + if (treeB == null) return treeA; // @step:initialize + + // Both nodes exist — merge by summing values + treeA.value = treeA.value + treeB.value; // @step:merge-node + + // Recursively merge left and right subtrees + treeA.left = mergeBinaryTrees(treeA.left, treeB.left); // @step:traverse-left + treeA.right = mergeBinaryTrees(treeA.right, treeB.right); // @step:traverse-right + + return treeA; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/sources/merge-binary-trees.py b/src/algorithms/trees/manipulation/merge-binary-trees/sources/merge-binary-trees.py new file mode 100644 index 00000000..8b774684 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/sources/merge-binary-trees.py @@ -0,0 +1,22 @@ +# Merge Binary Trees — recursive: if both nodes exist, sum values; otherwise take non-null node + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def merge_binary_trees(tree_a, tree_b): + if tree_a is None: # @step:initialize + return tree_b + if tree_b is None: # @step:initialize + return tree_a + + # Both nodes exist — merge by summing values + tree_a.value = tree_a.value + tree_b.value # @step:merge-node + + # Recursively merge left and right subtrees + tree_a.left = merge_binary_trees(tree_a.left, tree_b.left) # @step:traverse-left + tree_a.right = merge_binary_trees(tree_a.right, tree_b.right) # @step:traverse-right + + return tree_a # @step:visit diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/sources/merge-binary-trees.ts b/src/algorithms/trees/manipulation/merge-binary-trees/sources/merge-binary-trees.ts new file mode 100644 index 00000000..ba55047c --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/sources/merge-binary-trees.ts @@ -0,0 +1,21 @@ +// Merge Binary Trees — recursive: if both nodes exist, sum values; otherwise take non-null node + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function mergeBinaryTrees(treeA: BinaryNode | null, treeB: BinaryNode | null): BinaryNode | null { + if (treeA === null) return treeB; // @step:initialize + if (treeB === null) return treeA; // @step:initialize + + // Both nodes exist — merge by summing values + treeA.value = treeA.value + treeB.value; // @step:merge-node + + // Recursively merge left and right subtrees + treeA.left = mergeBinaryTrees(treeA.left, treeB.left); // @step:traverse-left + treeA.right = mergeBinaryTrees(treeA.right, treeB.right); // @step:traverse-right + + return treeA; // @step:visit +} diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/step-generator.test.ts b/src/algorithms/trees/manipulation/merge-binary-trees/step-generator.test.ts new file mode 100644 index 00000000..9d9d0992 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/step-generator.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMergeBinaryTreesSteps } from "./step-generator"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = [ + { + id: "m4", + value: 40, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 20, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 60, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 10, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 30, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 50, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 70, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMergeBinaryTreesSteps", () => { + it("produces steps for two 7-node trees", () => { + const steps = generateMergeBinaryTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMergeBinaryTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMergeBinaryTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMergeBinaryTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMergeBinaryTreesSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/merge-binary-trees/step-generator.ts b/src/algorithms/trees/manipulation/merge-binary-trees/step-generator.ts new file mode 100644 index 00000000..14e84c16 --- /dev/null +++ b/src/algorithms/trees/manipulation/merge-binary-trees/step-generator.ts @@ -0,0 +1,64 @@ +/** Step generator for Merge Binary Trees — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const MERGE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MERGE_BINARY_TREES!); + +export interface MergeBinaryTreesInput { + nodes: TreeNode[]; + rootId: string; + secondaryNodes: TreeNode[]; + secondaryRootId: string; +} + +export function generateMergeBinaryTreesSteps(input: MergeBinaryTreesInput): ExecutionStep[] { + const { nodes, rootId, secondaryNodes, secondaryRootId } = input; + const tracker = new TreeManipulationTracker( + nodes, + rootId, + MERGE_LINE_MAP, + secondaryNodes, + secondaryRootId, + ); + const nodeMapA = new Map(nodes.map((node) => [node.id, node])); + const nodeMapB = new Map(secondaryNodes.map((node) => [node.id, node])); + + tracker.initialize("Merge Binary Trees", { rootId, secondaryRootId }); + + function mergeNodes(idA: string | null, idB: string | null): void { + if (!idA && !idB) return; + + const nodeA = idA ? nodeMapA.get(idA) : undefined; + const nodeB = idB ? nodeMapB.get(idB) : undefined; + + if (!nodeA) return; // Tree B node adopted by Tree A — no step needed + if (!nodeB) { + // Tree A node stays, mark it processed + tracker.markProcessed(idA!, { idA, idB: null, merged: false }); + return; + } + + // Both exist — merge values + const mergedValue = nodeA.value + nodeB.value; + tracker.mergeNodes(idA!, idB!, mergedValue, { + idA, + idB, + valueA: nodeA.value, + valueB: nodeB.value, + mergedValue, + }); + + mergeNodes(nodeA.leftChildId, nodeB.leftChildId); + mergeNodes(nodeA.rightChildId, nodeB.rightChildId); + + tracker.markProcessed(idA!, { idA, idB, mergedValue }); + } + + mergeNodes(rootId, secondaryRootId); + tracker.complete(null, { result: "merged" }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/RightSideViewRecursivePipeline.stories.tsx b/src/algorithms/trees/manipulation/right-side-view-recursive/RightSideViewRecursivePipeline.stories.tsx new file mode 100644 index 00000000..45d67c08 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/RightSideViewRecursivePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Right Side View Recursive algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key DFS depth-tracking states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateRightSideViewRecursiveSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateRightSideViewRecursiveSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Right Side View Recursive", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before recursive DFS traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with right subtree partially traversed */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all rightmost-per-depth nodes collected */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/educational.ts b/src/algorithms/trees/manipulation/right-side-view-recursive/educational.ts new file mode 100644 index 00000000..93c5330f --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const rightSideViewRecursiveEducational: EducationalContent = { + overview: + "**Right Side View (Recursive)** uses a depth-first search that visits the right child before the left child. By recording only the first node seen at each depth, it naturally captures the rightmost node at every level.\n\nThis DFS approach is more space-efficient than BFS for tall, narrow trees since the call stack is proportional to the tree height rather than the maximum level width.", + + howItWorks: + "The recursive DFS approach exploits depth tracking:\n\n" + + "1. **Initialize** — create an empty result array, call `dfs(root, depth=0)`.\n" + + "2. **Base case** — if the node is null, return.\n" + + "3. **Record if new depth** — if `depth === result.length`, this is the first (rightmost) node at this depth; add its value.\n" + + "4. **Recurse right first** — visit right child at `depth + 1`.\n" + + "5. **Recurse left** — visit left child at `depth + 1`.\n\n" + + "Because the right subtree is always visited before the left, the first node encountered at each depth is guaranteed to be the rightmost visible node.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once during the DFS.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The recursive call stack depth equals the tree height. `O(log n)` for balanced trees, `O(n)` for skewed trees.", + + bestAndWorstCase: + "**Best case** — balanced tree: `O(log n)` call stack depth.\n\n" + + "**Worst case** — skewed tree: `O(n)` call stack depth.\n\n" + + "For wide, shallow trees, the DFS stack uses less memory than BFS queue.", + + realWorldUses: [ + "**Narrow-tree optimization** — Prefer over BFS when trees are taller than they are wide.", + "**Recursive tree processors** — Natural fit in codebases already using recursive tree traversal.", + "**Right-boundary traversal** — Used as a sub-step in boundary traversal algorithms.", + "**Depth-limited views** — Easily extended to collect rightmost node only up to a specified depth.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simpler than BFS — no queue management or level-size bookkeeping needed.", + "Uses `O(h)` space, which is better than BFS for tall narrow trees.", + "Easily extended to collect left-side view by swapping DFS order.", + ], + limitations: [ + "Recursive — may stack overflow for very deep trees.", + "For wide trees, BFS is more space-efficient than this DFS approach.", + "Visiting right-then-left may be counterintuitive compared to standard left-then-right DFS.", + ], + }, + + whenToUseIt: + "Use the recursive DFS variant when trees are tall and narrow, or when the codebase already uses recursive tree algorithms. For wide, shallow trees or when an iterative implementation is required, prefer the BFS version.", +}; diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/index.ts b/src/algorithms/trees/manipulation/right-side-view-recursive/index.ts new file mode 100644 index 00000000..6d6aa333 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/index.ts @@ -0,0 +1,122 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { rightSideViewRecursive } from "./sources/right-side-view-recursive.ts?fn"; +import { generateRightSideViewRecursiveSteps } from "./step-generator"; +import type { RightSideViewRecursiveInput } from "./step-generator"; +import { rightSideViewRecursiveEducational } from "./educational"; + +import typescriptSource from "./sources/right-side-view-recursive.ts?raw"; +import pythonSource from "./sources/right-side-view-recursive.py?raw"; +import javaSource from "./sources/RightSideViewRecursive.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeRightSideViewRecursive(input: RightSideViewRecursiveInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + return rightSideViewRecursive(root) as number[]; +} + +const rightSideViewRecursiveDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.RIGHT_SIDE_VIEW_RECURSIVE!, + name: "Right Side View (Recursive)", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursive DFS that visits the right child before the left child, recording the first node encountered at each depth to capture the right-side view", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeRightSideViewRecursive, + generateSteps: generateRightSideViewRecursiveSteps, + educational: rightSideViewRecursiveEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(rightSideViewRecursiveDefinition); diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/right-side-view-recursive.test.ts b/src/algorithms/trees/manipulation/right-side-view-recursive/right-side-view-recursive.test.ts new file mode 100644 index 00000000..32b57e24 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/right-side-view-recursive.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from "vitest"; +import { rightSideViewRecursive } from "./sources/right-side-view-recursive.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("rightSideViewRecursive", () => { + it("returns empty array for a null root", () => { + expect(rightSideViewRecursive(null)).toEqual([]); + }); + + it("returns the root value for a single-node tree", () => { + const root = makeNode(1); + expect(rightSideViewRecursive(root)).toEqual([1]); + }); + + it("returns right-side values for a 7-node BST", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + expect(rightSideViewRecursive(root)).toEqual([4, 6, 7]); + }); + + it("returns only left nodes for a left-skewed tree", () => { + const root = makeNode(1, makeNode(2, makeNode(3))); + expect(rightSideViewRecursive(root)).toEqual([1, 2, 3]); + }); + + it("returns right nodes for a right-skewed tree", () => { + const root = makeNode(1, null, makeNode(2, null, makeNode(3))); + expect(rightSideViewRecursive(root)).toEqual([1, 2, 3]); + }); + + it("returns deepest right-side node when left branch is deeper", () => { + const root = makeNode(1, makeNode(2, makeNode(5), null), makeNode(3, null, makeNode(4))); + expect(rightSideViewRecursive(root)).toEqual([1, 3, 4]); + }); +}); diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/sources/RightSideViewRecursive.java b/src/algorithms/trees/manipulation/right-side-view-recursive/sources/RightSideViewRecursive.java new file mode 100644 index 00000000..6ecbe1c0 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/sources/RightSideViewRecursive.java @@ -0,0 +1,30 @@ +// Right Side View Recursive — DFS: visit right child first, record first node seen at each depth + +import java.util.*; + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class RightSideViewRecursive { + public List rightSideViewRecursive(BinaryNode root) { + List result = new ArrayList<>(); // @step:initialize + dfs(root, 0, result); + return result; // @step:complete + } + + private void dfs(BinaryNode node, int depth, List result) { + if (node == null) return; // @step:initialize + + // First node encountered at this depth is visible from the right + if (depth == result.size()) { // @step:visit + result.add(node.value); // @step:collect-element + } + + // Visit right child first to ensure rightmost value is recorded first + dfs(node.right, depth + 1, result); // @step:traverse-right + dfs(node.left, depth + 1, result); // @step:traverse-left + } +} diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/sources/right-side-view-recursive.py b/src/algorithms/trees/manipulation/right-side-view-recursive/sources/right-side-view-recursive.py new file mode 100644 index 00000000..9f427413 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/sources/right-side-view-recursive.py @@ -0,0 +1,25 @@ +# Right Side View Recursive — DFS: visit right child first, record first node seen at each depth + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def right_side_view_recursive(root) -> list: + result = [] # @step:initialize + + def dfs(node, depth: int) -> None: + if node is None: # @step:initialize + return + + # First node encountered at this depth is visible from the right + if depth == len(result): # @step:visit + result.append(node.value) # @step:collect-element + + # Visit right child first to ensure rightmost value is recorded first + dfs(node.right, depth + 1) # @step:traverse-right + dfs(node.left, depth + 1) # @step:traverse-left + + dfs(root, 0) # @step:initialize + return result # @step:complete diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/sources/right-side-view-recursive.ts b/src/algorithms/trees/manipulation/right-side-view-recursive/sources/right-side-view-recursive.ts new file mode 100644 index 00000000..316cfa4e --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/sources/right-side-view-recursive.ts @@ -0,0 +1,28 @@ +// Right Side View Recursive — DFS: visit right child first, record first node seen at each depth + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function rightSideViewRecursive(root: BinaryNode | null): number[] { + const result: number[] = []; // @step:initialize + + function dfs(node: BinaryNode | null, depth: number): void { + if (node === null) return; // @step:initialize + + // First node encountered at this depth is visible from the right + if (depth === result.length) { + // @step:visit + result.push(node.value); // @step:collect-element + } + + // Visit right child first to ensure rightmost value is recorded first + dfs(node.right, depth + 1); // @step:traverse-right + dfs(node.left, depth + 1); // @step:traverse-left + } + + dfs(root, 0); // @step:initialize + return result; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/step-generator.test.ts b/src/algorithms/trees/manipulation/right-side-view-recursive/step-generator.test.ts new file mode 100644 index 00000000..2d99c468 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateRightSideViewRecursiveSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateRightSideViewRecursiveSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateRightSideViewRecursiveSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateRightSideViewRecursiveSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateRightSideViewRecursiveSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateRightSideViewRecursiveSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateRightSideViewRecursiveSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/right-side-view-recursive/step-generator.ts b/src/algorithms/trees/manipulation/right-side-view-recursive/step-generator.ts new file mode 100644 index 00000000..cb323efe --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view-recursive/step-generator.ts @@ -0,0 +1,58 @@ +/** Step generator for Right Side View Recursive (DFS) — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const RIGHT_SIDE_VIEW_RECURSIVE_LINE_MAP = buildLineMapFromSources( + ALGORITHM_ID.RIGHT_SIDE_VIEW_RECURSIVE!, +); + +export interface RightSideViewRecursiveInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateRightSideViewRecursiveSteps( + input: RightSideViewRecursiveInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, RIGHT_SIDE_VIEW_RECURSIVE_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Right Side View (Recursive DFS)", { rootId, result: [] }); + + const result: number[] = []; + + function dfs(nodeId: string | null, depth: number): void { + if (!nodeId) return; + const currentNode = nodeMap.get(nodeId); + if (!currentNode) return; + + if (depth === result.length) { + result.push(currentNode.value); + tracker.markProcessed(nodeId, { + nodeId, + depth, + value: currentNode.value, + isRightmost: true, + result: [...result], + }); + } else { + tracker.compareNodes(nodeId, nodeId, { + nodeId, + depth, + value: currentNode.value, + isRightmost: false, + }); + } + + dfs(currentNode.rightChildId, depth + 1); + dfs(currentNode.leftChildId, depth + 1); + } + + dfs(rootId, 0); + tracker.complete(null, { result }); + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/right-side-view/RightSideViewPipeline.stories.tsx b/src/algorithms/trees/manipulation/right-side-view/RightSideViewPipeline.stories.tsx new file mode 100644 index 00000000..7387bd19 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/RightSideViewPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Right Side View algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key BFS level-collection states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateRightSideViewSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateRightSideViewSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Right Side View", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before BFS traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some levels processed and rightmost nodes collected */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all rightmost nodes collected */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/right-side-view/educational.ts b/src/algorithms/trees/manipulation/right-side-view/educational.ts new file mode 100644 index 00000000..c6720d95 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const rightSideViewEducational: EducationalContent = { + overview: + "**Right Side View** returns the values of all nodes visible when looking at the binary tree from the right side. For each depth level, only the rightmost node is visible.\n\nThis BFS variant processes the tree level-by-level and records the last node seen at each level.", + + howItWorks: + "The algorithm uses BFS (level-order traversal):\n\n" + + "1. **Initialize** — add root to the queue.\n" + + "2. **Process each level** — at the start of each BFS iteration, snapshot `levelSize = queue.length`.\n" + + "3. **Dequeue all level nodes** — process exactly `levelSize` nodes from the queue.\n" + + "4. **Record rightmost** — when `position === levelSize - 1`, the current node is the last at this depth; add its value to results.\n" + + "5. **Enqueue children** — add left then right children for the next level.\n" + + "6. **Repeat** — continue until the queue is empty.\n\n" + + "For the default 7-node BST, the right side view is `[4, 6, 7]`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is dequeued exactly once.\n\n" + + "**Space Complexity: `O(w)` where `w` is the maximum tree width**\n\n" + + "The BFS queue holds at most one full level. For balanced trees, the widest level has `n/2` nodes — `O(n)` worst case.", + + bestAndWorstCase: + "**Best case** — skewed tree: queue never holds more than one node — `O(1)` extra space.\n\n" + + "**Worst case** — complete binary tree: bottom level has `⌊n/2⌋` nodes in the queue simultaneously — `O(n)` space.", + + realWorldUses: [ + "**UI rendering** — Determine which elements are visible at each depth in a UI tree.", + "**Rendering engines** — Compute rightmost visible nodes in BSP or scene graph trees.", + "**Game AI** — Find the last visible entity per depth tier in search trees.", + "**Data visualization** — Annotate hierarchical data with the boundary element at each level.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple BFS implementation — easy to understand and maintain.", + "Level-by-level processing naturally maps to the visual metaphor.", + "Can be trivially adapted to return the left-side view by picking `position === 0`.", + ], + limitations: [ + "Does not handle n-ary trees without modification.", + "Returns values only, not node references — add a separate `nodeId` array if references are needed.", + "BFS queue uses `O(w)` space; for very wide trees, DFS recursion may be more space-efficient.", + ], + }, + + whenToUseIt: + "Use right-side-view BFS when you need level-by-level access to the rightmost nodes and want a straightforward queue-based implementation. For deep, narrow trees where recursion depth is acceptable, the recursive DFS variant uses less space.", +}; diff --git a/src/algorithms/trees/manipulation/right-side-view/index.ts b/src/algorithms/trees/manipulation/right-side-view/index.ts new file mode 100644 index 00000000..933fe6aa --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/index.ts @@ -0,0 +1,122 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { rightSideView } from "./sources/right-side-view.ts?fn"; +import { generateRightSideViewSteps } from "./step-generator"; +import type { RightSideViewInput } from "./step-generator"; +import { rightSideViewEducational } from "./educational"; + +import typescriptSource from "./sources/right-side-view.ts?raw"; +import pythonSource from "./sources/right-side-view.py?raw"; +import javaSource from "./sources/RightSideView.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeRightSideView(input: RightSideViewInput): number[] { + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBinaryNode(id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBinaryNode(node.leftChildId), + right: toBinaryNode(node.rightChildId), + }; + } + const root = toBinaryNode(input.rootId); + return rightSideView(root) as number[]; +} + +const rightSideViewDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.RIGHT_SIDE_VIEW!, + name: "Right Side View", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "BFS level-order traversal that collects the rightmost node at each depth level, returning the values visible when viewing the tree from the right", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeRightSideView, + generateSteps: generateRightSideViewSteps, + educational: rightSideViewEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(rightSideViewDefinition); diff --git a/src/algorithms/trees/manipulation/right-side-view/right-side-view.test.ts b/src/algorithms/trees/manipulation/right-side-view/right-side-view.test.ts new file mode 100644 index 00000000..8bd22e6c --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/right-side-view.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from "vitest"; +import { rightSideView } from "./sources/right-side-view.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("rightSideView", () => { + it("returns empty array for a null root", () => { + expect(rightSideView(null)).toEqual([]); + }); + + it("returns the root value for a single-node tree", () => { + const root = makeNode(1); + expect(rightSideView(root)).toEqual([1]); + }); + + it("returns right-side values for a 7-node BST", () => { + const root = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + expect(rightSideView(root)).toEqual([4, 6, 7]); + }); + + it("returns only left nodes for a left-skewed tree", () => { + const root = makeNode(1, makeNode(2, makeNode(3))); + expect(rightSideView(root)).toEqual([1, 2, 3]); + }); + + it("returns right nodes for a right-skewed tree", () => { + const root = makeNode(1, null, makeNode(2, null, makeNode(3))); + expect(rightSideView(root)).toEqual([1, 2, 3]); + }); + + it("returns deepest right-side node when left branch is deeper", () => { + const root = makeNode(1, makeNode(2, makeNode(5), null), makeNode(3, null, makeNode(4))); + expect(rightSideView(root)).toEqual([1, 3, 4]); + }); +}); diff --git a/src/algorithms/trees/manipulation/right-side-view/sources/RightSideView.java b/src/algorithms/trees/manipulation/right-side-view/sources/RightSideView.java new file mode 100644 index 00000000..a4207e26 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/sources/RightSideView.java @@ -0,0 +1,37 @@ +// Right Side View — BFS: collect the last node of each level + +import java.util.*; + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class RightSideView { + public List rightSideView(BinaryNode root) { + List result = new ArrayList<>(); + if (root == null) return result; // @step:initialize + + Queue queue = new LinkedList<>(); + queue.add(root); // @step:initialize + + while (!queue.isEmpty()) { // @step:visit + int levelSize = queue.size(); // @step:visit + + for (int position = 0; position < levelSize; position++) { // @step:visit + BinaryNode node = queue.poll(); // @step:dequeue + + // The last node of this level is visible from the right side + if (position == levelSize - 1) { // @step:collect-element + result.add(node.value); // @step:collect-element + } + + if (node.left != null) queue.add(node.left); // @step:enqueue + if (node.right != null) queue.add(node.right); // @step:enqueue + } + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/manipulation/right-side-view/sources/right-side-view.py b/src/algorithms/trees/manipulation/right-side-view/sources/right-side-view.py new file mode 100644 index 00000000..5aaad513 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/sources/right-side-view.py @@ -0,0 +1,33 @@ +# Right Side View — BFS: collect the last node of each level + +from collections import deque + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def right_side_view(root) -> list: + if root is None: # @step:initialize + return [] + + result = [] # @step:initialize + queue = deque([root]) # @step:initialize + + while queue: # @step:visit + level_size = len(queue) # @step:visit + + for position in range(level_size): # @step:visit + node = queue.popleft() # @step:dequeue + + # The last node of this level is visible from the right side + if position == level_size - 1: # @step:collect-element + result.append(node.value) # @step:collect-element + + if node.left is not None: + queue.append(node.left) # @step:enqueue + if node.right is not None: + queue.append(node.right) # @step:enqueue + + return result # @step:complete diff --git a/src/algorithms/trees/manipulation/right-side-view/sources/right-side-view.ts b/src/algorithms/trees/manipulation/right-side-view/sources/right-side-view.ts new file mode 100644 index 00000000..8a0f174f --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/sources/right-side-view.ts @@ -0,0 +1,35 @@ +// Right Side View — BFS: collect the last node of each level + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function rightSideView(root: BinaryNode | null): number[] { + if (root === null) return []; // @step:initialize + + const result: number[] = []; // @step:initialize + const queue: BinaryNode[] = [root]; // @step:initialize + + while (queue.length > 0) { + // @step:visit + const levelSize = queue.length; // @step:visit + + for (let position = 0; position < levelSize; position++) { + // @step:visit + const node = queue.shift()!; // @step:dequeue + + // The last node of this level is visible from the right side + if (position === levelSize - 1) { + // @step:collect-element + result.push(node.value); // @step:collect-element + } + + if (node.left !== null) queue.push(node.left); // @step:enqueue + if (node.right !== null) queue.push(node.right); // @step:enqueue + } + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/right-side-view/step-generator.test.ts b/src/algorithms/trees/manipulation/right-side-view/step-generator.test.ts new file mode 100644 index 00000000..3df2614d --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateRightSideViewSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateRightSideViewSteps", () => { + it("produces steps for a 7-node tree", () => { + const steps = generateRightSideViewSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateRightSideViewSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateRightSideViewSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateRightSideViewSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateRightSideViewSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/right-side-view/step-generator.ts b/src/algorithms/trees/manipulation/right-side-view/step-generator.ts new file mode 100644 index 00000000..bd5881c5 --- /dev/null +++ b/src/algorithms/trees/manipulation/right-side-view/step-generator.ts @@ -0,0 +1,58 @@ +/** Step generator for Right Side View (BFS) — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const RIGHT_SIDE_VIEW_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.RIGHT_SIDE_VIEW!); + +export interface RightSideViewInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateRightSideViewSteps(input: RightSideViewInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeManipulationTracker(nodes, rootId, RIGHT_SIDE_VIEW_LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize("Right Side View (BFS)", { rootId, result: [] }); + + const bfsQueue: string[] = [rootId]; + const result: number[] = []; + + while (bfsQueue.length > 0) { + const levelSize = bfsQueue.length; + + for (let position = 0; position < levelSize; position++) { + const currentId = bfsQueue.shift(); + if (!currentId) continue; + const currentNode = nodeMap.get(currentId); + if (!currentNode) continue; + + if (position === levelSize - 1) { + // Rightmost node at this level + result.push(currentNode.value); + tracker.markProcessed(currentId, { + currentId, + value: currentNode.value, + isRightmost: true, + result: [...result], + }); + } else { + tracker.compareNodes(currentId, currentId, { + currentId, + value: currentNode.value, + isRightmost: false, + }); + } + + if (currentNode.leftChildId) bfsQueue.push(currentNode.leftChildId); + if (currentNode.rightChildId) bfsQueue.push(currentNode.rightChildId); + } + } + + tracker.complete(null, { result }); + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/SameTreeIterativePipeline.stories.tsx b/src/algorithms/trees/manipulation/same-tree-iterative/SameTreeIterativePipeline.stories.tsx new file mode 100644 index 00000000..8b510872 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/SameTreeIterativePipeline.stories.tsx @@ -0,0 +1,126 @@ +/** + * Storybook stories for the Same Tree Iterative algorithm pipeline. + * Uses the real step generator with two identical 7-node BSTs, + * rendering the TreeVisualizer at key BFS paired-comparison states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSameTreeIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = treeANodes.map((node) => ({ + ...node, + id: node.id.replace("n", "m"), + parentId: node.parentId ? node.parentId.replace("n", "m") : null, + leftChildId: node.leftChildId ? node.leftChildId.replace("n", "m") : null, + rightChildId: node.rightChildId ? node.rightChildId.replace("n", "m") : null, +})); + +const steps = generateSameTreeIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Same Tree Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — both trees queued before iterative comparison begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some node pairs compared via BFS */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all node pairs compared, result determined */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/educational.ts b/src/algorithms/trees/manipulation/same-tree-iterative/educational.ts new file mode 100644 index 00000000..11ddcf10 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/educational.ts @@ -0,0 +1,50 @@ +import type { EducationalContent } from "@/types"; + +export const sameTreeIterativeEducational: EducationalContent = { + overview: + "**Same Tree (Iterative)** uses a queue of node pairs to compare two binary trees iteratively. At each step, the front pair is dequeued and compared; their child pairs are then enqueued for the next comparisons.\n\nThis avoids recursion entirely, making it safe for arbitrarily deep trees.", + + howItWorks: + "The algorithm uses BFS with paired nodes:\n\n" + + "1. **Initialize** — enqueue `(rootA, rootB)` as the first pair.\n" + + "2. **Dequeue** — pop the front pair `(nodeA, nodeB)` from the queue.\n" + + "3. **Both null** — skip (structural match at this position).\n" + + "4. **One null** — return `false` (structural mismatch).\n" + + "5. **Different values** — return `false` (value mismatch).\n" + + "6. **Enqueue children** — add `(nodeA.left, nodeB.left)` and `(nodeA.right, nodeB.right)` to the queue.\n" + + "7. **Repeat** — continue until queue is empty.\n" + + "8. **Return true** — all pairs matched.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(min(n, m))`**\n\n" + + "Stops at first mismatch; visits all nodes only when trees are identical.\n\n" + + "**Space Complexity: `O(min(w₁, w₂))`** where `w₁` and `w₂` are the maximum widths of the two trees\n\n" + + "The queue holds at most one full level of paired nodes. Worst case is `O(n/2)` for a complete tree.", + + bestAndWorstCase: + "**Best case** — root values differ: returns `false` after the first dequeue — `O(1)` time.\n\n" + + "**Worst case** — identical trees: every node is dequeued — `O(n)` time and `O(w)` space.", + + realWorldUses: [ + "**Deep tree comparison** — Compare deep trees without risking call stack overflow.", + "**Streaming tree validation** — Queue-based comparison integrates well with iterative pipeline processors.", + "**Unit test trees** — Iterative assertion helpers for tree equality in test frameworks.", + "**Serialized tree comparison** — BFS order maps directly to level-order serialized representations.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — safe for arbitrarily deep trees.", + "BFS order makes it easy to inspect which level caused a mismatch.", + "Straightforward queue-based extension of the recursive approach.", + ], + limitations: [ + "Uses more memory than the recursive approach for wide trees.", + "Slightly more verbose than the elegant recursive version.", + "Queue allocates paired arrays for every node, increasing GC pressure.", + ], + }, + + whenToUseIt: + "Prefer the iterative version when trees may be very deep. For shallow or moderately-sized trees, the recursive version is cleaner. Both run in the same asymptotic time; the iterative version trades call stack for queue memory.", +}; diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/index.ts b/src/algorithms/trees/manipulation/same-tree-iterative/index.ts new file mode 100644 index 00000000..99c8d077 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/index.ts @@ -0,0 +1,138 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sameTreeIterative } from "./sources/same-tree-iterative.ts?fn"; +import { generateSameTreeIterativeSteps } from "./step-generator"; +import type { SameTreeIterativeInput } from "./step-generator"; +import { sameTreeIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/same-tree-iterative.ts?raw"; +import pythonSource from "./sources/same-tree-iterative.py?raw"; +import javaSource from "./sources/SameTreeIterative.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const secondaryNodes: TreeNode[] = defaultNodes.map((node) => ({ + ...node, + id: node.id.replace("n", "m"), + parentId: node.parentId ? node.parentId.replace("n", "m") : null, + leftChildId: node.leftChildId ? node.leftChildId.replace("n", "m") : null, + rightChildId: node.rightChildId ? node.rightChildId.replace("n", "m") : null, +})); + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeSameTreeIterative(input: SameTreeIterativeInput): boolean { + function buildNode(nodeMap: Map, id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: buildNode(nodeMap, node.leftChildId), + right: buildNode(nodeMap, node.rightChildId), + }; + } + const mapA = new Map(input.nodes.map((node) => [node.id, node])); + const mapB = new Map(input.secondaryNodes.map((node) => [node.id, node])); + return sameTreeIterative( + buildNode(mapA, input.rootId), + buildNode(mapB, input.secondaryRootId), + ) as boolean; +} + +const sameTreeIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SAME_TREE_ITERATIVE!, + name: "Same Tree (Iterative)", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Queue-based iterative comparison that processes node pairs level by level to determine whether two binary trees are structurally identical", + timeComplexity: { best: "O(1)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + secondaryNodes, + secondaryRootId: "m4", + }, + }, + execute: executeSameTreeIterative, + generateSteps: generateSameTreeIterativeSteps, + educational: sameTreeIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(sameTreeIterativeDefinition); diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/same-tree-iterative.test.ts b/src/algorithms/trees/manipulation/same-tree-iterative/same-tree-iterative.test.ts new file mode 100644 index 00000000..ffd43a98 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/same-tree-iterative.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { sameTreeIterative } from "./sources/same-tree-iterative.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("sameTreeIterative", () => { + it("returns true for two null trees", () => { + expect(sameTreeIterative(null, null)).toBe(true); + }); + + it("returns false when one tree is null", () => { + expect(sameTreeIterative(makeNode(1), null)).toBe(false); + expect(sameTreeIterative(null, makeNode(1))).toBe(false); + }); + + it("returns true for two identical single-node trees", () => { + expect(sameTreeIterative(makeNode(1), makeNode(1))).toBe(true); + }); + + it("returns false for single-node trees with different values", () => { + expect(sameTreeIterative(makeNode(1), makeNode(2))).toBe(false); + }); + + it("returns true for two identical 7-node BSTs", () => { + const treeA = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const treeB = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + expect(sameTreeIterative(treeA, treeB)).toBe(true); + }); + + it("returns false for trees with different leaf values", () => { + const treeA = makeNode(1, makeNode(2), makeNode(3)); + const treeB = makeNode(1, makeNode(2), makeNode(4)); + expect(sameTreeIterative(treeA, treeB)).toBe(false); + }); + + it("returns false for trees with different structures", () => { + const treeA = makeNode(1, makeNode(2)); + const treeB = makeNode(1, null, makeNode(2)); + expect(sameTreeIterative(treeA, treeB)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/sources/SameTreeIterative.java b/src/algorithms/trees/manipulation/same-tree-iterative/sources/SameTreeIterative.java new file mode 100644 index 00000000..bbf3727c --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/sources/SameTreeIterative.java @@ -0,0 +1,31 @@ +// Same Tree Iterative — queue-based: compare pairs of nodes from both trees simultaneously + +import java.util.*; + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class SameTreeIterative { + public boolean sameTreeIterative(BinaryNode treeA, BinaryNode treeB) { + Queue queue = new LinkedList<>(); + queue.add(new BinaryNode[]{treeA, treeB}); // @step:initialize + + while (!queue.isEmpty()) { // @step:visit + BinaryNode[] pair = queue.poll(); // @step:dequeue + BinaryNode nodeA = pair[0]; + BinaryNode nodeB = pair[1]; + + if (nodeA == null && nodeB == null) continue; // @step:compare + if (nodeA == null || nodeB == null) return false; // @step:compare + if (nodeA.value != nodeB.value) return false; // @step:compare + + queue.add(new BinaryNode[]{nodeA.left, nodeB.left}); // @step:enqueue + queue.add(new BinaryNode[]{nodeA.right, nodeB.right}); // @step:enqueue + } + + return true; // @step:complete + } +} diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/sources/same-tree-iterative.py b/src/algorithms/trees/manipulation/same-tree-iterative/sources/same-tree-iterative.py new file mode 100644 index 00000000..cb86c635 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/sources/same-tree-iterative.py @@ -0,0 +1,27 @@ +# Same Tree Iterative — queue-based: compare pairs of nodes from both trees simultaneously + +from collections import deque + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def same_tree_iterative(tree_a, tree_b) -> bool: + queue = deque([(tree_a, tree_b)]) # @step:initialize + + while queue: # @step:visit + node_a, node_b = queue.popleft() # @step:dequeue + + if node_a is None and node_b is None: # @step:compare + continue + if node_a is None or node_b is None: # @step:compare + return False + if node_a.value != node_b.value: # @step:compare + return False + + queue.append((node_a.left, node_b.left)) # @step:enqueue + queue.append((node_a.right, node_b.right)) # @step:enqueue + + return True # @step:complete diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/sources/same-tree-iterative.ts b/src/algorithms/trees/manipulation/same-tree-iterative/sources/same-tree-iterative.ts new file mode 100644 index 00000000..142ba28a --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/sources/same-tree-iterative.ts @@ -0,0 +1,27 @@ +// Same Tree Iterative — queue-based: compare pairs of nodes from both trees simultaneously + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function sameTreeIterative(treeA: BinaryNode | null, treeB: BinaryNode | null): boolean { + const queue: [BinaryNode | null, BinaryNode | null][] = [[treeA, treeB]]; // @step:initialize + + while (queue.length > 0) { + // @step:visit + const pair = queue.shift()!; // @step:dequeue + const nodeA = pair[0]; + const nodeB = pair[1]; + + if (nodeA === null && nodeB === null) continue; // @step:compare + if (nodeA === null || nodeB === null) return false; // @step:compare + if (nodeA.value !== nodeB.value) return false; // @step:compare + + queue.push([nodeA.left, nodeB.left]); // @step:enqueue + queue.push([nodeA.right, nodeB.right]); // @step:enqueue + } + + return true; // @step:complete +} diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/step-generator.test.ts b/src/algorithms/trees/manipulation/same-tree-iterative/step-generator.test.ts new file mode 100644 index 00000000..21f59975 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/step-generator.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSameTreeIterativeSteps } from "./step-generator"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = treeANodes.map((node) => ({ + ...node, + id: node.id.replace("n", "m"), + parentId: node.parentId ? node.parentId.replace("n", "m") : null, + leftChildId: node.leftChildId ? node.leftChildId.replace("n", "m") : null, + rightChildId: node.rightChildId ? node.rightChildId.replace("n", "m") : null, +})); + +describe("generateSameTreeIterativeSteps", () => { + it("produces steps for two identical 7-node trees", () => { + const steps = generateSameTreeIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSameTreeIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSameTreeIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSameTreeIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSameTreeIterativeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/same-tree-iterative/step-generator.ts b/src/algorithms/trees/manipulation/same-tree-iterative/step-generator.ts new file mode 100644 index 00000000..fc93f4fc --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree-iterative/step-generator.ts @@ -0,0 +1,69 @@ +/** Step generator for Same Tree Iterative — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SAME_TREE_ITERATIVE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SAME_TREE_ITERATIVE!); + +export interface SameTreeIterativeInput { + nodes: TreeNode[]; + rootId: string; + secondaryNodes: TreeNode[]; + secondaryRootId: string; +} + +export function generateSameTreeIterativeSteps(input: SameTreeIterativeInput): ExecutionStep[] { + const { nodes, rootId, secondaryNodes, secondaryRootId } = input; + const tracker = new TreeManipulationTracker( + nodes, + rootId, + SAME_TREE_ITERATIVE_LINE_MAP, + secondaryNodes, + secondaryRootId, + ); + const nodeMapA = new Map(nodes.map((node) => [node.id, node])); + const nodeMapB = new Map(secondaryNodes.map((node) => [node.id, node])); + + tracker.initialize("Same Tree (Iterative)", { rootId, secondaryRootId }); + + const bfsQueue: [string | null, string | null][] = [[rootId, secondaryRootId]]; + let result = true; + + while (bfsQueue.length > 0) { + const pair = bfsQueue.shift(); + if (!pair) continue; + const [idA, idB] = pair; + + if (!idA && !idB) continue; + + if (!idA || !idB) { + result = false; + break; + } + + const nodeA = nodeMapA.get(idA); + const nodeB = nodeMapB.get(idB); + + if (!nodeA || !nodeB) { + result = false; + break; + } + + tracker.compareNodes(idA, idB, { idA, idB, valueA: nodeA.value, valueB: nodeB.value }); + + if (nodeA.value !== nodeB.value) { + result = false; + break; + } + + tracker.markProcessed(idA, { idA, idB, matched: true }); + + bfsQueue.push([nodeA.leftChildId, nodeB.leftChildId]); + bfsQueue.push([nodeA.rightChildId, nodeB.rightChildId]); + } + + tracker.complete(result, { result }); + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/same-tree/SameTreePipeline.stories.tsx b/src/algorithms/trees/manipulation/same-tree/SameTreePipeline.stories.tsx new file mode 100644 index 00000000..e4eae53d --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/SameTreePipeline.stories.tsx @@ -0,0 +1,126 @@ +/** + * Storybook stories for the Same Tree algorithm pipeline. + * Uses the real step generator with two identical 7-node BSTs, + * rendering the TreeVisualizer at key comparison states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSameTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = treeANodes.map((node) => ({ + ...node, + id: node.id.replace("n", "m"), + parentId: node.parentId ? node.parentId.replace("n", "m") : null, + leftChildId: node.leftChildId ? node.leftChildId.replace("n", "m") : null, + rightChildId: node.rightChildId ? node.rightChildId.replace("n", "m") : null, +})); + +const steps = generateSameTreeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Same Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — both trees before comparison begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some node pairs compared */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — all nodes compared, result determined */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/same-tree/educational.ts b/src/algorithms/trees/manipulation/same-tree/educational.ts new file mode 100644 index 00000000..fd764af2 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/educational.ts @@ -0,0 +1,49 @@ +import type { EducationalContent } from "@/types"; + +export const sameTreeEducational: EducationalContent = { + overview: + "**Same Tree** determines whether two binary trees are structurally identical and have the same node values at every position. Two null trees are considered the same; a null and a non-null tree are not.\n\nThis is a fundamental tree comparison algorithm used as a building block for subtree checks, tree equality assertions, and structural validation.", + + howItWorks: + "The recursive algorithm compares two trees simultaneously:\n\n" + + "1. **Both null** — return `true` (two empty trees are identical).\n" + + "2. **One null** — return `false` (different structure).\n" + + "3. **Different values** — return `false`.\n" + + "4. **Recurse left** — check if left subtrees are the same.\n" + + "5. **Recurse right** — check if right subtrees are the same.\n" + + "6. **Return** — `leftMatch && rightMatch`.\n\n" + + "The algorithm short-circuits on the first mismatch, so it runs faster in practice on dissimilar trees.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(min(n, m))`** where `n` and `m` are the sizes of the two trees\n\n" + + "The comparison stops at the first mismatch, but must visit all nodes in the worst case (identical trees).\n\n" + + "**Space Complexity: `O(min(h₁, h₂))`** where `h₁` and `h₂` are the heights of the two trees\n\n" + + "The recursion stack depth is bounded by the height of the shallower tree.", + + bestAndWorstCase: + "**Best case** — root values differ: returns `false` after comparing only the roots — `O(1)` time.\n\n" + + "**Worst case** — identical trees or trees that differ only at the deepest leaf: every node is visited — `O(n)` time.", + + realWorldUses: [ + "**Version control** — Compare two versions of a syntax tree to detect changes.", + "**Test assertions** — Verify that a tree transformation produced the expected result.", + "**Subtree detection** — Used as a helper in subtree-of-another-tree algorithms.", + "**Memoized tree hashing** — Detect duplicate subtrees in DP tree problems.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple and direct — the recursive structure mirrors the problem definition.", + "Short-circuits on first mismatch, making it fast in practice for different trees.", + "Easily extended to compare trees with additional node attributes.", + ], + limitations: [ + "Recursive — may stack overflow for very deep trees.", + "Cannot compare trees with cycles without cycle detection.", + "For comparing large trees frequently, a hash-based approach is more efficient.", + ], + }, + + whenToUseIt: + "Use same-tree whenever you need exact structural and value equality between two binary trees. For checking if one tree contains another as a subtree, combine same-tree with a pre-order traversal of the larger tree. Use the iterative variant for deep trees.", +}; diff --git a/src/algorithms/trees/manipulation/same-tree/index.ts b/src/algorithms/trees/manipulation/same-tree/index.ts new file mode 100644 index 00000000..119b6350 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/index.ts @@ -0,0 +1,196 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sameTree } from "./sources/same-tree.ts?fn"; +import { generateSameTreeSteps } from "./step-generator"; +import type { SameTreeInput } from "./step-generator"; +import { sameTreeEducational } from "./educational"; + +import typescriptSource from "./sources/same-tree.ts?raw"; +import pythonSource from "./sources/same-tree.py?raw"; +import javaSource from "./sources/SameTree.java?raw"; + +/** Standard 7-node balanced BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Identical second tree — same structure and values */ +const secondaryNodes: TreeNode[] = [ + { + id: "m4", + value: 4, + parentId: null, + leftChildId: "m2", + rightChildId: "m6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "m2", + value: 2, + parentId: "m4", + leftChildId: "m1", + rightChildId: "m3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "m6", + value: 6, + parentId: "m4", + leftChildId: "m5", + rightChildId: "m7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "m1", + value: 1, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "m3", + value: 3, + parentId: "m2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "m5", + value: 5, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "m7", + value: 7, + parentId: "m6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeSameTree(input: SameTreeInput): boolean { + function buildNode(nodeMap: Map, id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: buildNode(nodeMap, node.leftChildId), + right: buildNode(nodeMap, node.rightChildId), + }; + } + const mapA = new Map(input.nodes.map((node) => [node.id, node])); + const mapB = new Map(input.secondaryNodes.map((node) => [node.id, node])); + const rootA = buildNode(mapA, input.rootId); + const rootB = buildNode(mapB, input.secondaryRootId); + return sameTree(rootA, rootB) as boolean; +} + +const sameTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SAME_TREE!, + name: "Same Tree", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursively checks whether two binary trees are structurally identical and have the same node values at every position", + timeComplexity: { best: "O(1)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + secondaryNodes, + secondaryRootId: "m4", + }, + }, + execute: executeSameTree, + generateSteps: generateSameTreeSteps, + educational: sameTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(sameTreeDefinition); diff --git a/src/algorithms/trees/manipulation/same-tree/same-tree.test.ts b/src/algorithms/trees/manipulation/same-tree/same-tree.test.ts new file mode 100644 index 00000000..490ef9bf --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/same-tree.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { sameTree } from "./sources/same-tree.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("sameTree", () => { + it("returns true for two null trees", () => { + expect(sameTree(null, null)).toBe(true); + }); + + it("returns false when one tree is null", () => { + expect(sameTree(makeNode(1), null)).toBe(false); + expect(sameTree(null, makeNode(1))).toBe(false); + }); + + it("returns true for two identical single-node trees", () => { + expect(sameTree(makeNode(1), makeNode(1))).toBe(true); + }); + + it("returns false for single-node trees with different values", () => { + expect(sameTree(makeNode(1), makeNode(2))).toBe(false); + }); + + it("returns true for two identical 7-node BSTs", () => { + const treeA = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const treeB = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + expect(sameTree(treeA, treeB)).toBe(true); + }); + + it("returns false for trees with different leaf values", () => { + const treeA = makeNode(1, makeNode(2), makeNode(3)); + const treeB = makeNode(1, makeNode(2), makeNode(4)); + expect(sameTree(treeA, treeB)).toBe(false); + }); + + it("returns false for trees with different structures", () => { + const treeA = makeNode(1, makeNode(2)); + const treeB = makeNode(1, null, makeNode(2)); + expect(sameTree(treeA, treeB)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/manipulation/same-tree/sources/SameTree.java b/src/algorithms/trees/manipulation/same-tree/sources/SameTree.java new file mode 100644 index 00000000..f1fd929c --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/sources/SameTree.java @@ -0,0 +1,21 @@ +// Same Tree — recursive: check structural equality and value equality + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class SameTree { + public boolean sameTree(BinaryNode treeA, BinaryNode treeB) { + if (treeA == null && treeB == null) return true; // @step:initialize + if (treeA == null || treeB == null) return false; // @step:compare + if (treeA.value != treeB.value) return false; // @step:compare + + // Recursively check left and right subtrees + boolean leftMatch = sameTree(treeA.left, treeB.left); // @step:traverse-left + boolean rightMatch = sameTree(treeA.right, treeB.right); // @step:traverse-right + + return leftMatch && rightMatch; // @step:visit + } +} diff --git a/src/algorithms/trees/manipulation/same-tree/sources/same-tree.py b/src/algorithms/trees/manipulation/same-tree/sources/same-tree.py new file mode 100644 index 00000000..7c76a9f6 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/sources/same-tree.py @@ -0,0 +1,21 @@ +# Same Tree — recursive: check structural equality and value equality + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def same_tree(tree_a, tree_b) -> bool: + if tree_a is None and tree_b is None: # @step:initialize + return True + if tree_a is None or tree_b is None: # @step:compare + return False + if tree_a.value != tree_b.value: # @step:compare + return False + + # Recursively check left and right subtrees + left_match = same_tree(tree_a.left, tree_b.left) # @step:traverse-left + right_match = same_tree(tree_a.right, tree_b.right) # @step:traverse-right + + return left_match and right_match # @step:visit diff --git a/src/algorithms/trees/manipulation/same-tree/sources/same-tree.ts b/src/algorithms/trees/manipulation/same-tree/sources/same-tree.ts new file mode 100644 index 00000000..ba67e4b4 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/sources/same-tree.ts @@ -0,0 +1,19 @@ +// Same Tree — recursive: check structural equality and value equality + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function sameTree(treeA: BinaryNode | null, treeB: BinaryNode | null): boolean { + if (treeA === null && treeB === null) return true; // @step:initialize + if (treeA === null || treeB === null) return false; // @step:compare + if (treeA.value !== treeB.value) return false; // @step:compare + + // Recursively check left and right subtrees + const leftMatch = sameTree(treeA.left, treeB.left); // @step:traverse-left + const rightMatch = sameTree(treeA.right, treeB.right); // @step:traverse-right + + return leftMatch && rightMatch; // @step:visit +} diff --git a/src/algorithms/trees/manipulation/same-tree/step-generator.test.ts b/src/algorithms/trees/manipulation/same-tree/step-generator.test.ts new file mode 100644 index 00000000..9ae007c0 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/step-generator.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSameTreeSteps } from "./step-generator"; + +const treeANodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const treeBNodes: TreeNode[] = treeANodes.map((node) => ({ + ...node, + id: node.id.replace("n", "m"), + parentId: node.parentId ? node.parentId.replace("n", "m") : null, + leftChildId: node.leftChildId ? node.leftChildId.replace("n", "m") : null, + rightChildId: node.rightChildId ? node.rightChildId.replace("n", "m") : null, +})); + +describe("generateSameTreeSteps", () => { + it("produces steps for two identical 7-node trees", () => { + const steps = generateSameTreeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSameTreeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSameTreeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states with secondary tree", () => { + const steps = generateSameTreeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSameTreeSteps({ + nodes: treeANodes, + rootId: "n4", + secondaryNodes: treeBNodes, + secondaryRootId: "m4", + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/same-tree/step-generator.ts b/src/algorithms/trees/manipulation/same-tree/step-generator.ts new file mode 100644 index 00000000..db3760a4 --- /dev/null +++ b/src/algorithms/trees/manipulation/same-tree/step-generator.ts @@ -0,0 +1,70 @@ +/** Step generator for Same Tree — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SAME_TREE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SAME_TREE!); + +export interface SameTreeInput { + nodes: TreeNode[]; + rootId: string; + secondaryNodes: TreeNode[]; + secondaryRootId: string; +} + +export function generateSameTreeSteps(input: SameTreeInput): ExecutionStep[] { + const { nodes, rootId, secondaryNodes, secondaryRootId } = input; + const tracker = new TreeManipulationTracker( + nodes, + rootId, + SAME_TREE_LINE_MAP, + secondaryNodes, + secondaryRootId, + ); + const nodeMapA = new Map(nodes.map((node) => [node.id, node])); + const nodeMapB = new Map(secondaryNodes.map((node) => [node.id, node])); + + tracker.initialize("Same Tree", { rootId, secondaryRootId }); + + let result = true; + + function compare(idA: string | null, idB: string | null): boolean { + if (!idA && !idB) return true; + if (!idA || !idB) { + result = false; + return false; + } + + const nodeA = nodeMapA.get(idA); + const nodeB = nodeMapB.get(idB); + + if (!nodeA || !nodeB) { + result = false; + return false; + } + + tracker.compareNodes(idA, idB, { idA, idB, valueA: nodeA.value, valueB: nodeB.value }); + + if (nodeA.value !== nodeB.value) { + result = false; + return false; + } + + const leftMatch = compare(nodeA.leftChildId, nodeB.leftChildId); + const rightMatch = compare(nodeA.rightChildId, nodeB.rightChildId); + const matches = leftMatch && rightMatch; + + if (matches) { + tracker.markProcessed(idA, { idA, idB, matched: true }); + } + + return matches; + } + + compare(rootId, secondaryRootId); + tracker.complete(result, { result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/SubtreeOfAnotherTreePipeline.stories.tsx b/src/algorithms/trees/manipulation/subtree-of-another-tree/SubtreeOfAnotherTreePipeline.stories.tsx new file mode 100644 index 00000000..4d2b9b92 --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/SubtreeOfAnotherTreePipeline.stories.tsx @@ -0,0 +1,148 @@ +/** + * Storybook stories for the Subtree of Another Tree algorithm pipeline. + * Uses the real step generator with a 7-node main tree and a 3-node subtree, + * rendering the TreeVisualizer at key comparison states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSubtreeOfAnotherTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const mainTreeNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const subTreeNodes: TreeNode[] = [ + { + id: "s2", + value: 2, + parentId: null, + leftChildId: "s1", + rightChildId: "s3", + state: "default", + position: { x: 100, y: 60 }, + }, + { + id: "s1", + value: 1, + parentId: "s2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 160 }, + }, + { + id: "s3", + value: 3, + parentId: "s2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 160 }, + }, +]; + +const steps = generateSubtreeOfAnotherTreeSteps({ + nodes: mainTreeNodes, + rootId: "n4", + secondaryNodes: subTreeNodes, + secondaryRootId: "s2", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Subtree of Another Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — main tree and subtree before search begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-manipulation with some main tree nodes checked against subtree */ +export const MidManipulation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Manipulation complete — subtree found (or not found) in main tree */ +export const ManipulationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/educational.ts b/src/algorithms/trees/manipulation/subtree-of-another-tree/educational.ts new file mode 100644 index 00000000..01d046da --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const subtreeOfAnotherTreeEducational: EducationalContent = { + overview: + "**Subtree of Another Tree** determines whether one binary tree (subTree) appears as a contiguous subtree somewhere within another binary tree (mainTree). The subtree must match exactly — same structure and same values.\n\nThis problem combines tree traversal with tree equality checking as a subroutine.", + + howItWorks: + "The algorithm visits every node in the main tree and checks for a match:\n\n" + + "1. **Base cases** — if `subTree` is null, return `true` (empty tree is always a subtree); if `mainTree` is null, return `false`.\n" + + "2. **Check match** — at every node of `mainTree`, call `isSameTree(mainTree, subTree)`. If it returns `true`, the subtree is found.\n" + + "3. **Recurse** — if no match at the current node, recursively check `mainTree.left` and `mainTree.right`.\n\n" + + "The `isSameTree` helper uses the standard recursive same-tree comparison. For the default input (main tree: 1–7, subtree: left subtree rooted at 2), the algorithm finds a match at node 2.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n × m)`** where `n` is the main tree size and `m` is the subtree size\n\n" + + "In the worst case, `isSameTree` is called at every node of the main tree and runs in `O(m)` time each time.\n\n" + + "**Space Complexity: `O(h₁ + h₂)`** where `h₁` and `h₂` are the heights of the two trees\n\n" + + "The combined call stack depth is at most `h₁` (main tree recursion) + `h₂` (isSameTree recursion).", + + bestAndWorstCase: + "**Best case** — subTree root value matches the main tree root and both trees are identical — `O(m)` time (single match at root).\n\n" + + "**Worst case** — identical values throughout and subtree matches only at the last leaf position — `O(n × m)` time.", + + realWorldUses: [ + "**Code analysis** — Check whether a code pattern (AST subtree) appears in a larger AST.", + "**DOM inspection** — Detect if a specific HTML subtree appears within a document tree.", + "**Data validation** — Verify that a required schema subtree is present in a data tree.", + "**Duplicate detection** — Find repeated subtrees in parse trees or expression trees.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple recursive implementation reusing isSameTree as a helper.", + "Early termination on first match — often faster than worst case in practice.", + "Naturally extends to check for multiple subtrees or collect all matching positions.", + ], + limitations: [ + "`O(n × m)` worst-case time — a Merkle hash approach can reduce this to `O(n + m)`.", + "Recursive — may stack overflow for very deep trees.", + "Does not support partial matching — the subtree must match exactly.", + ], + }, + + whenToUseIt: + "Use subtree-of-another-tree for simple containment checks where the trees are not excessively large. For performance-critical applications, use a hashing-based approach to achieve `O(n + m)` time. The iterative variant is safer for deep trees.", +}; diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/index.ts b/src/algorithms/trees/manipulation/subtree-of-another-tree/index.ts new file mode 100644 index 00000000..037a3230 --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/index.ts @@ -0,0 +1,156 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { subtreeOfAnotherTree } from "./sources/subtree-of-another-tree.ts?fn"; +import { generateSubtreeOfAnotherTreeSteps } from "./step-generator"; +import type { SubtreeOfAnotherTreeInput } from "./step-generator"; +import { subtreeOfAnotherTreeEducational } from "./educational"; + +import typescriptSource from "./sources/subtree-of-another-tree.ts?raw"; +import pythonSource from "./sources/subtree-of-another-tree.py?raw"; +import javaSource from "./sources/SubtreeOfAnotherTree.java?raw"; + +/** Main tree: standard 7-node balanced BST */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Subtree: left subtree of main tree (rooted at node with value 2) */ +const secondaryNodes: TreeNode[] = [ + { + id: "s2", + value: 2, + parentId: null, + leftChildId: "s1", + rightChildId: "s3", + state: "default", + position: { x: 100, y: 60 }, + }, + { + id: "s1", + value: 1, + parentId: "s2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 160 }, + }, + { + id: "s3", + value: 3, + parentId: "s2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 160 }, + }, +]; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function executeSubtreeOfAnotherTree(input: SubtreeOfAnotherTreeInput): boolean { + function buildNode(nodeMap: Map, id: string | null): BinaryNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: buildNode(nodeMap, node.leftChildId), + right: buildNode(nodeMap, node.rightChildId), + }; + } + const mapA = new Map(input.nodes.map((node) => [node.id, node])); + const mapB = new Map(input.secondaryNodes.map((node) => [node.id, node])); + return subtreeOfAnotherTree( + buildNode(mapA, input.rootId), + buildNode(mapB, input.secondaryRootId), + ) as boolean; +} + +const subtreeOfAnotherTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SUBTREE_OF_ANOTHER_TREE!, + name: "Subtree of Another Tree", + category: CATEGORY.TREES!, + technique: "manipulation", + description: + "Recursively checks whether a smaller binary tree appears as an exact structural match anywhere within a larger binary tree", + timeComplexity: { best: "O(m)", average: "O(n×m)", worst: "O(n×m)" }, + spaceComplexity: "O(h1+h2)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", secondaryNodes, secondaryRootId: "s2" }, + }, + execute: executeSubtreeOfAnotherTree, + generateSteps: generateSubtreeOfAnotherTreeSteps, + educational: subtreeOfAnotherTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(subtreeOfAnotherTreeDefinition); diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/SubtreeOfAnotherTree.java b/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/SubtreeOfAnotherTree.java new file mode 100644 index 00000000..042c8f1c --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/SubtreeOfAnotherTree.java @@ -0,0 +1,28 @@ +// Subtree of Another Tree — recursive: for each node in main tree, check if subtree matches + +class BinaryNode { + int value; + BinaryNode left, right; + BinaryNode(int value) { this.value = value; } +} + +class SubtreeOfAnotherTree { + private boolean isSameTree(BinaryNode treeA, BinaryNode treeB) { + if (treeA == null && treeB == null) return true; + if (treeA == null || treeB == null) return false; + if (treeA.value != treeB.value) return false; + return isSameTree(treeA.left, treeB.left) && isSameTree(treeA.right, treeB.right); + } + + public boolean subtreeOfAnotherTree(BinaryNode mainTree, BinaryNode subTree) { + if (subTree == null) return true; // @step:initialize + if (mainTree == null) return false; // @step:initialize + + // Check if the tree rooted at mainTree matches subTree + if (isSameTree(mainTree, subTree)) return true; // @step:compare + + // Recursively check left and right subtrees + return subtreeOfAnotherTree(mainTree.left, subTree) || // @step:traverse-left + subtreeOfAnotherTree(mainTree.right, subTree); // @step:traverse-right + } +} diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/subtree-of-another-tree.py b/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/subtree-of-another-tree.py new file mode 100644 index 00000000..ebd7a8e7 --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/subtree-of-another-tree.py @@ -0,0 +1,30 @@ +# Subtree of Another Tree — recursive: for each node in main tree, check if subtree matches + +class BinaryNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def is_same_tree(tree_a, tree_b) -> bool: + if tree_a is None and tree_b is None: + return True + if tree_a is None or tree_b is None: + return False + if tree_a.value != tree_b.value: + return False + return is_same_tree(tree_a.left, tree_b.left) and is_same_tree(tree_a.right, tree_b.right) + +def subtree_of_another_tree(main_tree, sub_tree) -> bool: + if sub_tree is None: # @step:initialize + return True + if main_tree is None: # @step:initialize + return False + + # Check if the tree rooted at main_tree matches sub_tree + if is_same_tree(main_tree, sub_tree): # @step:compare + return True + + # Recursively check left and right subtrees + return (subtree_of_another_tree(main_tree.left, sub_tree) or # @step:traverse-left + subtree_of_another_tree(main_tree.right, sub_tree)) # @step:traverse-right diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/subtree-of-another-tree.ts b/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/subtree-of-another-tree.ts new file mode 100644 index 00000000..c3af4c49 --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/sources/subtree-of-another-tree.ts @@ -0,0 +1,28 @@ +// Subtree of Another Tree — recursive: for each node in main tree, check if subtree matches + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function isSameTree(treeA: BinaryNode | null, treeB: BinaryNode | null): boolean { + if (treeA === null && treeB === null) return true; + if (treeA === null || treeB === null) return false; + if (treeA.value !== treeB.value) return false; + return isSameTree(treeA.left, treeB.left) && isSameTree(treeA.right, treeB.right); +} + +function subtreeOfAnotherTree(mainTree: BinaryNode | null, subTree: BinaryNode | null): boolean { + if (subTree === null) return true; // @step:initialize + if (mainTree === null) return false; // @step:initialize + + // Check if the tree rooted at mainTree matches subTree + if (isSameTree(mainTree, subTree)) return true; // @step:compare + + // Recursively check left and right subtrees + return ( + subtreeOfAnotherTree(mainTree.left, subTree) || // @step:traverse-left + subtreeOfAnotherTree(mainTree.right, subTree) + ); // @step:traverse-right +} diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/step-generator.test.ts b/src/algorithms/trees/manipulation/subtree-of-another-tree/step-generator.test.ts new file mode 100644 index 00000000..983658ef --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/step-generator.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSubtreeOfAnotherTreeSteps } from "./step-generator"; + +const mainTreeNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const subTreeNodes: TreeNode[] = [ + { + id: "s2", + value: 2, + parentId: null, + leftChildId: "s1", + rightChildId: "s3", + state: "default", + position: { x: 100, y: 60 }, + }, + { + id: "s1", + value: 1, + parentId: "s2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 160 }, + }, + { + id: "s3", + value: 3, + parentId: "s2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 160 }, + }, +]; + +describe("generateSubtreeOfAnotherTreeSteps", () => { + it("produces steps for main tree and subtree", () => { + const steps = generateSubtreeOfAnotherTreeSteps({ + nodes: mainTreeNodes, + rootId: "n4", + secondaryNodes: subTreeNodes, + secondaryRootId: "s2", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSubtreeOfAnotherTreeSteps({ + nodes: mainTreeNodes, + rootId: "n4", + secondaryNodes: subTreeNodes, + secondaryRootId: "s2", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSubtreeOfAnotherTreeSteps({ + nodes: mainTreeNodes, + rootId: "n4", + secondaryNodes: subTreeNodes, + secondaryRootId: "s2", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSubtreeOfAnotherTreeSteps({ + nodes: mainTreeNodes, + rootId: "n4", + secondaryNodes: subTreeNodes, + secondaryRootId: "s2", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSubtreeOfAnotherTreeSteps({ + nodes: mainTreeNodes, + rootId: "n4", + secondaryNodes: subTreeNodes, + secondaryRootId: "s2", + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/step-generator.ts b/src/algorithms/trees/manipulation/subtree-of-another-tree/step-generator.ts new file mode 100644 index 00000000..07a71b80 --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/step-generator.ts @@ -0,0 +1,74 @@ +/** Step generator for Subtree of Another Tree — produces ExecutionStep[] using TreeManipulationTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeManipulationTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SUBTREE_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SUBTREE_OF_ANOTHER_TREE!); + +export interface SubtreeOfAnotherTreeInput { + nodes: TreeNode[]; + rootId: string; + secondaryNodes: TreeNode[]; + secondaryRootId: string; +} + +export function generateSubtreeOfAnotherTreeSteps( + input: SubtreeOfAnotherTreeInput, +): ExecutionStep[] { + const { nodes, rootId, secondaryNodes, secondaryRootId } = input; + const tracker = new TreeManipulationTracker( + nodes, + rootId, + SUBTREE_LINE_MAP, + secondaryNodes, + secondaryRootId, + ); + const nodeMapA = new Map(nodes.map((node) => [node.id, node])); + const nodeMapB = new Map(secondaryNodes.map((node) => [node.id, node])); + + tracker.initialize("Subtree of Another Tree", { rootId, secondaryRootId }); + + function isSameTree(idA: string | null, idB: string | null): boolean { + if (!idA && !idB) return true; + if (!idA || !idB) return false; + const nodeA = nodeMapA.get(idA) ?? nodeMapB.get(idA); + const nodeB = nodeMapB.get(idB); + if (!nodeA || !nodeB) return false; + if (nodeA.value !== nodeB.value) return false; + return ( + isSameTree(nodeA.leftChildId, nodeB.leftChildId) && + isSameTree(nodeA.rightChildId, nodeB.rightChildId) + ); + } + + let found = false; + + function search(mainId: string | null): boolean { + if (!mainId) return false; + const mainNode = nodeMapA.get(mainId); + if (!mainNode) return false; + + tracker.compareNodes(mainId, secondaryRootId, { + mainId, + secondaryRootId, + mainValue: mainNode.value, + }); + + if (isSameTree(mainId, secondaryRootId)) { + found = true; + tracker.markProcessed(mainId, { mainId, matched: true }); + return true; + } + + const leftResult = search(mainNode.leftChildId); + if (leftResult) return true; + return search(mainNode.rightChildId); + } + + search(rootId); + tracker.complete(found, { result: found }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/manipulation/subtree-of-another-tree/subtree-of-another-tree.test.ts b/src/algorithms/trees/manipulation/subtree-of-another-tree/subtree-of-another-tree.test.ts new file mode 100644 index 00000000..e6c27a1c --- /dev/null +++ b/src/algorithms/trees/manipulation/subtree-of-another-tree/subtree-of-another-tree.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { subtreeOfAnotherTree } from "./sources/subtree-of-another-tree.ts?fn"; + +interface BinaryNode { + value: number; + left: BinaryNode | null; + right: BinaryNode | null; +} + +function makeNode( + value: number, + left: BinaryNode | null = null, + right: BinaryNode | null = null, +): BinaryNode { + return { value, left, right }; +} + +describe("subtreeOfAnotherTree", () => { + it("returns true when subTree is null", () => { + expect(subtreeOfAnotherTree(makeNode(1), null)).toBe(true); + }); + + it("returns false when mainTree is null but subTree is not", () => { + expect(subtreeOfAnotherTree(null, makeNode(1))).toBe(false); + }); + + it("returns true when mainTree equals subTree exactly", () => { + const mainTree = makeNode(1, makeNode(2), makeNode(3)); + const subTree = makeNode(1, makeNode(2), makeNode(3)); + expect(subtreeOfAnotherTree(mainTree, subTree)).toBe(true); + }); + + it("returns true when subTree is the left subtree of mainTree", () => { + const mainTree = makeNode( + 4, + makeNode(2, makeNode(1), makeNode(3)), + makeNode(6, makeNode(5), makeNode(7)), + ); + const subTree = makeNode(2, makeNode(1), makeNode(3)); + expect(subtreeOfAnotherTree(mainTree, subTree)).toBe(true); + }); + + it("returns false when subTree is not in mainTree", () => { + const mainTree = makeNode(4, makeNode(2), makeNode(6)); + const subTree = makeNode(9); + expect(subtreeOfAnotherTree(mainTree, subTree)).toBe(false); + }); + + it("returns false when value matches but structure differs", () => { + const mainTree = makeNode(4, makeNode(2, makeNode(1)), null); + const subTree = makeNode(2, null, makeNode(1)); + expect(subtreeOfAnotherTree(mainTree, subTree)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/AllRootToLeafPathsIterativePipeline.stories.tsx b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/AllRootToLeafPathsIterativePipeline.stories.tsx new file mode 100644 index 00000000..3416702f --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/AllRootToLeafPathsIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for All Root to Leaf Paths Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateAllRootToLeafPathsIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateAllRootToLeafPathsIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/All Root to Leaf Paths Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/all-root-to-leaf-paths-iterative.test.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/all-root-to-leaf-paths-iterative.test.ts new file mode 100644 index 00000000..70c0207f --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/all-root-to-leaf-paths-iterative.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { allRootToLeafPathsIterative } from "./sources/all-root-to-leaf-paths-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("allRootToLeafPathsIterative", () => { + it("returns 4 paths for a 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const paths = allRootToLeafPathsIterative(root) as string[]; + expect(paths.length).toBe(4); + }); + + it("returns correct path strings", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const paths = allRootToLeafPathsIterative(root) as string[]; + expect(paths).toContain("4->2->1"); + }); + + it("returns empty array for null root", () => { + expect(allRootToLeafPathsIterative(null)).toEqual([]); + }); + + it("returns single path for a single node", () => { + expect(allRootToLeafPathsIterative(node(5))).toEqual(["5"]); + }); +}); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/educational.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/educational.ts new file mode 100644 index 00000000..4cffbd08 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/educational.ts @@ -0,0 +1,29 @@ +import type { EducationalContent } from "@/types"; + +export const allRootToLeafPathsIterativeEducational: EducationalContent = { + overview: + "**All Root-to-Leaf Paths (Iterative)** collects all root-to-leaf path strings using an explicit stack. " + + "Each stack entry carries a node and the path string accumulated to reach it.", + + howItWorks: + "1. Push `[root, String(root.value)]` onto the stack.\n" + + "2. Pop `[current, pathSoFar]`.\n" + + "3. At a leaf, push `pathSoFar` to results.\n" + + "4. Push right child with `pathSoFar + '->' + right.value`, then left child similarly.\n" + + "5. Continue until the stack empties.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n * h)`** — same as the recursive version.\n\n" + + "**Space Complexity: `O(L * h)`** — path strings stored per leaf.", + + bestAndWorstCase: "Both cases are the same as the recursive version.", + + realWorldUses: ["**Recursion-free path enumeration:** Safe for arbitrarily deep trees."], + + strengthsAndLimitations: { + strengths: ["No recursion stack overflow risk."], + limitations: ["More verbose; path string memory usage is the same."], + }, + + whenToUseIt: "Use the iterative version for large trees where recursion depth is a concern.", +}; diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/index.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/index.ts new file mode 100644 index 00000000..9ff8fdde --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/index.ts @@ -0,0 +1,121 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { allRootToLeafPathsIterative } from "./sources/all-root-to-leaf-paths-iterative.ts?fn"; +import { generateAllRootToLeafPathsIterativeSteps } from "./step-generator"; +import type { AllRootToLeafPathsIterativeInput } from "./step-generator"; +import { allRootToLeafPathsIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/all-root-to-leaf-paths-iterative.ts?raw"; +import pythonSource from "./sources/all-root-to-leaf-paths-iterative.py?raw"; +import javaSource from "./sources/AllRootToLeafPathsIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeAllRootToLeafPathsIterative(input: AllRootToLeafPathsIterativeInput): string[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return allRootToLeafPathsIterative(toBSTNode(input.rootId)) as string[]; +} + +const allRootToLeafPathsIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.ALL_ROOT_TO_LEAF_PATHS_ITERATIVE!, + name: "All Root to Leaf Paths (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Stack-based DFS path collection — each stack entry carries the node and the path string accumulated so far", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeAllRootToLeafPathsIterative, + generateSteps: generateAllRootToLeafPathsIterativeSteps, + educational: allRootToLeafPathsIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, + }; + +registry.register(allRootToLeafPathsIterativeDefinition); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/AllRootToLeafPathsIterative.java b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/AllRootToLeafPathsIterative.java new file mode 100644 index 00000000..6d8cf4a0 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/AllRootToLeafPathsIterative.java @@ -0,0 +1,42 @@ +// All Root-to-Leaf Paths (Iterative) — stack-based with path tracking +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +class PathsIterativeNode { + int value; + PathsIterativeNode left, right; + PathsIterativeNode(int value) { this.value = value; } +} + +class AllRootToLeafPathsIterative { + public List allRootToLeafPathsIterative(PathsIterativeNode root) { + List paths = new ArrayList<>(); // @step:initialize + if (root == null) return paths; // @step:initialize + + Deque stack = new ArrayDeque<>(); // @step:initialize + stack.push(new Object[]{root, String.valueOf(root.value)}); // @step:initialize + + while (!stack.isEmpty()) { // @step:visit + Object[] entry = stack.pop(); // @step:visit + PathsIterativeNode current = (PathsIterativeNode) entry[0]; // @step:visit + String pathSoFar = (String) entry[1]; // @step:visit + + // Leaf node — record complete path + if (current.left == null && current.right == null) { // @step:check-balance + paths.add(pathSoFar); // @step:add-to-result + } + + if (current.right != null) { // @step:traverse-right + stack.push(new Object[]{current.right, pathSoFar + "->" + current.right.value}); // @step:traverse-right + } + + if (current.left != null) { // @step:traverse-left + stack.push(new Object[]{current.left, pathSoFar + "->" + current.left.value}); // @step:traverse-left + } + } + + return paths; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/all-root-to-leaf-paths-iterative.py b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/all-root-to-leaf-paths-iterative.py new file mode 100644 index 00000000..37cc7311 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/all-root-to-leaf-paths-iterative.py @@ -0,0 +1,29 @@ +# All Root-to-Leaf Paths (Iterative) — stack-based with path tracking + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def all_root_to_leaf_paths_iterative(root) -> list: + if root is None: # @step:initialize + return [] + + paths = [] # @step:initialize + stack = [(root, str(root.value))] # @step:initialize + + while stack: # @step:visit + current, path_so_far = stack.pop() # @step:visit + + # Leaf node — record complete path + if current.left is None and current.right is None: # @step:check-balance + paths.append(path_so_far) # @step:add-to-result + + if current.right is not None: # @step:traverse-right + stack.append((current.right, f"{path_so_far}->{current.right.value}")) # @step:traverse-right + + if current.left is not None: # @step:traverse-left + stack.append((current.left, f"{path_so_far}->{current.left.value}")) # @step:traverse-left + + return paths # @step:complete diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/all-root-to-leaf-paths-iterative.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/all-root-to-leaf-paths-iterative.ts new file mode 100644 index 00000000..8ebaa894 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/sources/all-root-to-leaf-paths-iterative.ts @@ -0,0 +1,38 @@ +// All Root-to-Leaf Paths (Iterative) — stack-based with path tracking + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function allRootToLeafPathsIterative(root: TreeNode | null): string[] { + if (root === null) return []; // @step:initialize + + const paths: string[] = []; // @step:initialize + const stack: Array<[TreeNode, string]> = [[root, String(root.value)]]; // @step:initialize + + while (stack.length > 0) { + // @step:visit + const entry = stack.pop()!; // @step:visit + const [current, pathSoFar] = entry; // @step:visit + + // Leaf node — record complete path + if (current.left === null && current.right === null) { + // @step:check-balance + paths.push(pathSoFar); // @step:add-to-result + } + + if (current.right !== null) { + // @step:traverse-right + stack.push([current.right, `${pathSoFar}->${current.right.value}`]); // @step:traverse-right + } + + if (current.left !== null) { + // @step:traverse-left + stack.push([current.left, `${pathSoFar}->${current.left.value}`]); // @step:traverse-left + } + } + + return paths; // @step:complete +} diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/step-generator.test.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/step-generator.test.ts new file mode 100644 index 00000000..c8c81b29 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateAllRootToLeafPathsIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateAllRootToLeafPathsIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateAllRootToLeafPathsIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateAllRootToLeafPathsIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateAllRootToLeafPathsIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateAllRootToLeafPathsIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateAllRootToLeafPathsIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/step-generator.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/step-generator.ts new file mode 100644 index 00000000..b7895cb3 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths-iterative/step-generator.ts @@ -0,0 +1,60 @@ +/** Step generator for All Root to Leaf Paths (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.ALL_ROOT_TO_LEAF_PATHS_ITERATIVE!); + +export interface AllRootToLeafPathsIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateAllRootToLeafPathsIterativeSteps( + input: AllRootToLeafPathsIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const rootNode = nodeMap.get(rootId); + if (!rootNode) { + tracker.complete({ result: [] }); + return tracker.getSteps(); + } + + const stack: Array<[string, string]> = [[rootId, String(rootNode.value)]]; + const paths: string[] = []; + + while (stack.length > 0) { + const entry = stack.pop()!; + const [nodeId, pathSoFar] = entry; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, pathSoFar }); + + if (!node.leftChildId && !node.rightChildId) { + paths.push(pathSoFar); + tracker.markValid(nodeId, { currentNode: nodeId, completedPath: pathSoFar }); + } + + if (node.rightChildId) { + const rightNode = nodeMap.get(node.rightChildId); + if (rightNode) stack.push([node.rightChildId, `${pathSoFar}->${rightNode.value}`]); + } + if (node.leftChildId) { + const leftNode = nodeMap.get(node.leftChildId); + if (leftNode) stack.push([node.leftChildId, `${pathSoFar}->${leftNode.value}`]); + } + } + + tracker.recordResult(paths.length, { result: paths, count: paths.length }); + tracker.complete({ result: paths, count: paths.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/AllRootToLeafPathsPipeline.stories.tsx b/src/algorithms/trees/properties/all-root-to-leaf-paths/AllRootToLeafPathsPipeline.stories.tsx new file mode 100644 index 00000000..bbe113f6 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/AllRootToLeafPathsPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for All Root to Leaf Paths. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateAllRootToLeafPathsSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateAllRootToLeafPathsSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/All Root to Leaf Paths", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/all-root-to-leaf-paths.test.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths/all-root-to-leaf-paths.test.ts new file mode 100644 index 00000000..17375986 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/all-root-to-leaf-paths.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { allRootToLeafPaths } from "./sources/all-root-to-leaf-paths.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("allRootToLeafPaths", () => { + it("returns 4 paths for a 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const paths = allRootToLeafPaths(root) as string[]; + expect(paths.length).toBe(4); + }); + + it("returns correct path strings", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + const paths = allRootToLeafPaths(root) as string[]; + expect(paths).toContain("4->2->1"); + expect(paths).toContain("4->2->3"); + }); + + it("returns empty array for null root", () => { + expect(allRootToLeafPaths(null)).toEqual([]); + }); + + it("returns single path for a single node", () => { + expect(allRootToLeafPaths(node(5))).toEqual(["5"]); + }); +}); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/educational.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths/educational.ts new file mode 100644 index 00000000..b023682d --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/educational.ts @@ -0,0 +1,40 @@ +import type { EducationalContent } from "@/types"; + +export const allRootToLeafPathsEducational: EducationalContent = { + overview: + "**All Root-to-Leaf Paths** collects every complete path from the root to any leaf node, " + + 'formatted as arrow-separated strings (e.g., `"4->2->1"`).', + + howItWorks: + "A recursive DFS carries the current path string as it descends:\n\n" + + "1. Append the current node's value to the path string.\n" + + "2. At a leaf, push the completed path string into the result array.\n" + + "3. Recurse left and right with the updated path string.\n\n" + + "Since each recursive call creates a new string, there's no need to backtrack manually.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n * h)`** — path string construction is `O(h)` per leaf, and there are `O(n)` nodes.\n\n" + + "**Space Complexity: `O(L * h)`** where `L` is the number of leaf nodes.", + + bestAndWorstCase: + "**Best case** is a single node — one path of length 1.\n\n" + + "**Worst case** is a full binary tree — `n/2` leaves each with paths of length `O(log n)`.", + + realWorldUses: [ + "**Decision tree enumeration:** List all decision sequences from root to outcome.", + "**File system path listing:** All paths from root directory to files.", + "**Game walkthrough:** All possible game sequences from start to end states.", + ], + + strengthsAndLimitations: { + strengths: ["Returns human-readable path strings without requiring manual backtracking."], + limitations: [ + "String concatenation at each node creates O(h) intermediate strings per path.", + "Memory usage scales with number of leaves × path depth.", + ], + }, + + whenToUseIt: + "Use when you need to enumerate all root-to-leaf paths for display or validation. " + + "For just checking whether a target sum exists on any path, `pathSum` is more efficient.", +}; diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/index.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths/index.ts new file mode 100644 index 00000000..dc7f4945 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { allRootToLeafPaths } from "./sources/all-root-to-leaf-paths.ts?fn"; +import { generateAllRootToLeafPathsSteps } from "./step-generator"; +import type { AllRootToLeafPathsInput } from "./step-generator"; +import { allRootToLeafPathsEducational } from "./educational"; + +import typescriptSource from "./sources/all-root-to-leaf-paths.ts?raw"; +import pythonSource from "./sources/all-root-to-leaf-paths.py?raw"; +import javaSource from "./sources/AllRootToLeafPaths.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeAllRootToLeafPaths(input: AllRootToLeafPathsInput): string[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return allRootToLeafPaths(toBSTNode(input.rootId)) as string[]; +} + +const allRootToLeafPathsDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.ALL_ROOT_TO_LEAF_PATHS!, + name: "All Root to Leaf Paths", + category: CATEGORY.TREES!, + technique: "properties", + description: + 'Collects all root-to-leaf paths as strings (e.g., "4->2->1") using recursive DFS with path accumulation', + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeAllRootToLeafPaths, + generateSteps: generateAllRootToLeafPathsSteps, + educational: allRootToLeafPathsEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(allRootToLeafPathsDefinition); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/AllRootToLeafPaths.java b/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/AllRootToLeafPaths.java new file mode 100644 index 00000000..97e37ec3 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/AllRootToLeafPaths.java @@ -0,0 +1,32 @@ +// All Root-to-Leaf Paths — recursive DFS collecting all paths as strings +import java.util.ArrayList; +import java.util.List; + +class PathsNode { + int value; + PathsNode left, right; + PathsNode(int value) { this.value = value; } +} + +class AllRootToLeafPaths { + public List allRootToLeafPaths(PathsNode root) { + List paths = new ArrayList<>(); // @step:initialize + dfs(root, "", paths); // @step:initialize + return paths; // @step:complete + } + + private void dfs(PathsNode node, String currentPath, List paths) { + if (node == null) return; // @step:initialize + + String pathSoFar = currentPath.isEmpty() ? String.valueOf(node.value) : currentPath + "->" + node.value; // @step:visit + + // Leaf node — record this complete path + if (node.left == null && node.right == null) { // @step:visit + paths.add(pathSoFar); // @step:add-to-result + return; + } + + dfs(node.left, pathSoFar, paths); // @step:traverse-left + dfs(node.right, pathSoFar, paths); // @step:traverse-right + } +} diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/all-root-to-leaf-paths.py b/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/all-root-to-leaf-paths.py new file mode 100644 index 00000000..954c468f --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/all-root-to-leaf-paths.py @@ -0,0 +1,27 @@ +# All Root-to-Leaf Paths — recursive DFS collecting all paths as strings + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def all_root_to_leaf_paths(root) -> list: + paths = [] # @step:initialize + + def dfs(node, current_path: str) -> None: + if node is None: # @step:initialize + return + + path_so_far = f"{current_path}->{node.value}" if current_path else str(node.value) # @step:visit + + # Leaf node — record this complete path + if node.left is None and node.right is None: # @step:visit + paths.append(path_so_far) # @step:add-to-result + return + + dfs(node.left, path_so_far) # @step:traverse-left + dfs(node.right, path_so_far) # @step:traverse-right + + dfs(root, "") # @step:initialize + return paths # @step:complete diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/all-root-to-leaf-paths.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/all-root-to-leaf-paths.ts new file mode 100644 index 00000000..618c7b5c --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/sources/all-root-to-leaf-paths.ts @@ -0,0 +1,30 @@ +// All Root-to-Leaf Paths — recursive DFS collecting all paths as strings + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function allRootToLeafPaths(root: TreeNode | null): string[] { + const paths: string[] = []; // @step:initialize + + function dfs(node: TreeNode | null, currentPath: string): void { + if (node === null) return; // @step:initialize + + const pathSoFar = currentPath.length > 0 ? `${currentPath}->${node.value}` : String(node.value); // @step:visit + + // Leaf node — record this complete path + if (node.left === null && node.right === null) { + // @step:visit + paths.push(pathSoFar); // @step:add-to-result + return; + } + + dfs(node.left, pathSoFar); // @step:traverse-left + dfs(node.right, pathSoFar); // @step:traverse-right + } + + dfs(root, ""); // @step:initialize + return paths; // @step:complete +} diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/step-generator.test.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths/step-generator.test.ts new file mode 100644 index 00000000..abce89c8 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateAllRootToLeafPathsSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateAllRootToLeafPathsSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateAllRootToLeafPathsSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateAllRootToLeafPathsSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateAllRootToLeafPathsSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateAllRootToLeafPathsSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateAllRootToLeafPathsSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/all-root-to-leaf-paths/step-generator.ts b/src/algorithms/trees/properties/all-root-to-leaf-paths/step-generator.ts new file mode 100644 index 00000000..e8539867 --- /dev/null +++ b/src/algorithms/trees/properties/all-root-to-leaf-paths/step-generator.ts @@ -0,0 +1,54 @@ +/** Step generator for All Root to Leaf Paths — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.ALL_ROOT_TO_LEAF_PATHS!); + +export interface AllRootToLeafPathsInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateAllRootToLeafPathsSteps(input: AllRootToLeafPathsInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const paths: string[] = []; + + function dfs(nodeId: string | null, currentPath: string[], pathIds: string[]): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + currentPath.push(String(node.value)); + pathIds.push(nodeId); + + tracker.checkNode(nodeId, { + currentNode: nodeId, + value: node.value, + pathSoFar: currentPath.join("->"), + }); + tracker.trackPath(pathIds, { currentNode: nodeId, pathSoFar: currentPath.join("->") }); + + if (!node.leftChildId && !node.rightChildId) { + const pathStr = currentPath.join("->"); + paths.push(pathStr); + tracker.markValid(nodeId, { currentNode: nodeId, completedPath: pathStr }); + } + + dfs(node.leftChildId, [...currentPath], [...pathIds]); + dfs(node.rightChildId, [...currentPath], [...pathIds]); + } + + dfs(rootId, [], []); + tracker.recordResult(paths.length, { result: paths, count: paths.length }); + tracker.complete({ result: paths, count: paths.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/binary-tree-tilt/BinaryTreeTiltPipeline.stories.tsx b/src/algorithms/trees/properties/binary-tree-tilt/BinaryTreeTiltPipeline.stories.tsx new file mode 100644 index 00000000..430d99ca --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/BinaryTreeTiltPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Binary Tree Tilt. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBinaryTreeTiltSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBinaryTreeTiltSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Binary Tree Tilt", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/binary-tree-tilt/binary-tree-tilt.test.ts b/src/algorithms/trees/properties/binary-tree-tilt/binary-tree-tilt.test.ts new file mode 100644 index 00000000..6e3ee092 --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/binary-tree-tilt.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { binaryTreeTilt } from "./sources/binary-tree-tilt.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("binaryTreeTilt", () => { + it("returns 0 for a null root", () => { + expect(binaryTreeTilt(null)).toBe(0); + }); + + it("returns 0 for a single node", () => { + expect(binaryTreeTilt(node(1))).toBe(0); + }); + + it("returns correct tilt for a simple 3-node tree", () => { + // node(1, node(2), node(3)): tilt at root = |2-3| = 1, leaves 0; total = 1 + expect(binaryTreeTilt(node(1, node(2), node(3)))).toBe(1); + }); + + it("returns a non-negative number for any tree", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(binaryTreeTilt(root)).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/src/algorithms/trees/properties/binary-tree-tilt/educational.ts b/src/algorithms/trees/properties/binary-tree-tilt/educational.ts new file mode 100644 index 00000000..64dd2a57 --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/educational.ts @@ -0,0 +1,41 @@ +import type { EducationalContent } from "@/types"; + +export const binaryTreeTiltEducational: EducationalContent = { + overview: + "**Binary Tree Tilt** computes the sum of tilts across all nodes. " + + "The tilt of a node is `abs(left subtree sum - right subtree sum)`. " + + "Leaf nodes have tilt 0 because both subtree sums are 0.", + + howItWorks: + "A post-order DFS computes two things simultaneously:\n\n" + + "1. **Subtree sum** — sum of all values in the subtree rooted at this node (returned up the stack).\n" + + "2. **Node tilt** — `abs(leftSum - rightSum)`, accumulated into a running total.\n\n" + + "Post-order is essential because tilt requires knowing both children's sums before computing the parent's tilt.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node is visited once.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case** is a perfectly symmetric tree — all tilts are 0.\n\n" + + "**Worst case** is a heavily skewed tree — maximum tilt accumulation.", + + realWorldUses: [ + "**Tree balance metric:** Total tilt quantifies how unbalanced a tree is, useful for monitoring data structures.", + "**Statistical analysis:** Tilt-like asymmetry measures appear in tree-based statistical models.", + ], + + strengthsAndLimitations: { + strengths: [ + "Computes both subtree sums and tilts in a single O(n) pass.", + "Clean post-order structure makes the intent clear.", + ], + limitations: [ + "Total tilt is not a normalized balance metric — it grows with both size and skewness.", + ], + }, + + whenToUseIt: + "Use binary tree tilt when you need a scalar measure of asymmetry across all nodes. " + + "For per-node balance checking, use `isBalancedTree` instead.", +}; diff --git a/src/algorithms/trees/properties/binary-tree-tilt/index.ts b/src/algorithms/trees/properties/binary-tree-tilt/index.ts new file mode 100644 index 00000000..aabc6272 --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { binaryTreeTilt } from "./sources/binary-tree-tilt.ts?fn"; +import { generateBinaryTreeTiltSteps } from "./step-generator"; +import type { BinaryTreeTiltInput } from "./step-generator"; +import { binaryTreeTiltEducational } from "./educational"; + +import typescriptSource from "./sources/binary-tree-tilt.ts?raw"; +import pythonSource from "./sources/binary-tree-tilt.py?raw"; +import javaSource from "./sources/BinaryTreeTilt.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeBinaryTreeTilt(input: BinaryTreeTiltInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return binaryTreeTilt(toBSTNode(input.rootId)) as number; +} + +const binaryTreeTiltDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BINARY_TREE_TILT!, + name: "Binary Tree Tilt", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Computes the total tilt of all nodes. Tilt of a node = abs(left subtree sum - right subtree sum). Uses post-order traversal.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeBinaryTreeTilt, + generateSteps: generateBinaryTreeTiltSteps, + educational: binaryTreeTiltEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(binaryTreeTiltDefinition); diff --git a/src/algorithms/trees/properties/binary-tree-tilt/sources/BinaryTreeTilt.java b/src/algorithms/trees/properties/binary-tree-tilt/sources/BinaryTreeTilt.java new file mode 100644 index 00000000..3c74b788 --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/sources/BinaryTreeTilt.java @@ -0,0 +1,30 @@ +// Binary Tree Tilt — post-order: tilt = abs(left sum - right sum), accumulate total tilt + +class TiltNode { + int value; + TiltNode left, right; + TiltNode(int value) { this.value = value; } +} + +class BinaryTreeTilt { + private int totalTilt = 0; // @step:initialize + + public int binaryTreeTilt(TiltNode root) { + totalTilt = 0; // @step:initialize + subtreeSum(root); // @step:initialize + return totalTilt; // @step:complete + } + + private int subtreeSum(TiltNode node) { + if (node == null) return 0; // @step:initialize + + int leftSum = subtreeSum(node.left); // @step:traverse-left + int rightSum = subtreeSum(node.right); // @step:traverse-right + + // Tilt at this node is absolute difference of left and right sums + int nodeTilt = Math.abs(leftSum - rightSum); // @step:compute-value + totalTilt += nodeTilt; // @step:add-to-result + + return leftSum + rightSum + node.value; // @step:update-height + } +} diff --git a/src/algorithms/trees/properties/binary-tree-tilt/sources/binary-tree-tilt.py b/src/algorithms/trees/properties/binary-tree-tilt/sources/binary-tree-tilt.py new file mode 100644 index 00000000..1f88f862 --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/sources/binary-tree-tilt.py @@ -0,0 +1,26 @@ +# Binary Tree Tilt — post-order: tilt = abs(left sum - right sum), accumulate total tilt + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def binary_tree_tilt(root) -> int: + total_tilt = [0] # @step:initialize + + def subtree_sum(node) -> int: + if node is None: # @step:initialize + return 0 + + left_sum = subtree_sum(node.left) # @step:traverse-left + right_sum = subtree_sum(node.right) # @step:traverse-right + + # Tilt at this node is absolute difference of left and right sums + node_tilt = abs(left_sum - right_sum) # @step:compute-value + total_tilt[0] += node_tilt # @step:add-to-result + + return left_sum + right_sum + node.value # @step:update-height + + subtree_sum(root) # @step:initialize + return total_tilt[0] # @step:complete diff --git a/src/algorithms/trees/properties/binary-tree-tilt/sources/binary-tree-tilt.ts b/src/algorithms/trees/properties/binary-tree-tilt/sources/binary-tree-tilt.ts new file mode 100644 index 00000000..d1833f7c --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/sources/binary-tree-tilt.ts @@ -0,0 +1,27 @@ +// Binary Tree Tilt — post-order: tilt = abs(left sum - right sum), accumulate total tilt + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function binaryTreeTilt(root: TreeNode | null): number { + let totalTilt = 0; // @step:initialize + + function subtreeSum(node: TreeNode | null): number { + if (node === null) return 0; // @step:initialize + + const leftSum = subtreeSum(node.left); // @step:traverse-left + const rightSum = subtreeSum(node.right); // @step:traverse-right + + // Tilt at this node is absolute difference of left and right sums + const nodeTilt = Math.abs(leftSum - rightSum); // @step:compute-value + totalTilt += nodeTilt; // @step:add-to-result + + return leftSum + rightSum + node.value; // @step:update-height + } + + subtreeSum(root); // @step:initialize + return totalTilt; // @step:complete +} diff --git a/src/algorithms/trees/properties/binary-tree-tilt/step-generator.test.ts b/src/algorithms/trees/properties/binary-tree-tilt/step-generator.test.ts new file mode 100644 index 00000000..de4a409e --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBinaryTreeTiltSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBinaryTreeTiltSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBinaryTreeTiltSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBinaryTreeTiltSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBinaryTreeTiltSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBinaryTreeTiltSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateBinaryTreeTiltSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/binary-tree-tilt/step-generator.ts b/src/algorithms/trees/properties/binary-tree-tilt/step-generator.ts new file mode 100644 index 00000000..213a1f80 --- /dev/null +++ b/src/algorithms/trees/properties/binary-tree-tilt/step-generator.ts @@ -0,0 +1,52 @@ +/** Step generator for Binary Tree Tilt — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BINARY_TREE_TILT!); + +export interface BinaryTreeTiltInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBinaryTreeTiltSteps(input: BinaryTreeTiltInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + let totalTilt = 0; + + function subtreeSum(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + const leftSum = subtreeSum(node.leftChildId); + const rightSum = subtreeSum(node.rightChildId); + + const nodeTilt = Math.abs(leftSum - rightSum); + totalTilt += nodeTilt; + + tracker.updateHeight(nodeId, totalTilt, { + currentNode: nodeId, + leftSum, + rightSum, + nodeTilt, + totalTilt, + }); + return leftSum + rightSum + node.value; + } + + subtreeSum(rootId); + tracker.recordResult(totalTilt, { result: totalTilt }); + tracker.complete({ result: totalTilt }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/CountCompleteTreeNodesPipeline.stories.tsx b/src/algorithms/trees/properties/count-complete-tree-nodes/CountCompleteTreeNodesPipeline.stories.tsx new file mode 100644 index 00000000..fac38143 --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/CountCompleteTreeNodesPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Count Complete Tree Nodes. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateCountCompleteTreeNodesSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateCountCompleteTreeNodesSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Count Complete Tree Nodes", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/count-complete-tree-nodes.test.ts b/src/algorithms/trees/properties/count-complete-tree-nodes/count-complete-tree-nodes.test.ts new file mode 100644 index 00000000..8a108eef --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/count-complete-tree-nodes.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { countCompleteTreeNodes } from "./sources/count-complete-tree-nodes.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("countCompleteTreeNodes", () => { + it("returns 7 for a perfect 7-node tree", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(countCompleteTreeNodes(root)).toBe(7); + }); + + it("returns 0 for a null root", () => { + expect(countCompleteTreeNodes(null)).toBe(0); + }); + + it("returns 1 for a single node", () => { + expect(countCompleteTreeNodes(node(1))).toBe(1); + }); + + it("returns 3 for a 3-node perfect tree", () => { + expect(countCompleteTreeNodes(node(1, node(2), node(3)))).toBe(3); + }); +}); diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/educational.ts b/src/algorithms/trees/properties/count-complete-tree-nodes/educational.ts new file mode 100644 index 00000000..3f3238ad --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/educational.ts @@ -0,0 +1,45 @@ +import type { EducationalContent } from "@/types"; + +export const countCompleteTreeNodesEducational: EducationalContent = { + overview: + "**Count Complete Tree Nodes** exploits the structure of complete binary trees to count nodes " + + "faster than `O(n)`. In a complete binary tree, every level except possibly the last is fully filled, " + + "and all nodes are as far left as possible.", + + howItWorks: + "At each node:\n\n" + + "1. Compute the **leftmost height** (always follow left children).\n" + + "2. Compute the **rightmost height** (always follow right children).\n" + + "3. If equal, the subtree is a **perfect binary tree** with `2^h - 1` nodes — return immediately.\n" + + "4. If not equal, recurse on both subtrees and sum their counts.\n\n" + + "The key insight is that in a complete binary tree, at least one of the two subtrees is always perfect.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(log² n)`** — height computation is `O(log n)` and there are `O(log n)` recursive calls.\n\n" + + "**Space Complexity: `O(log n)`** — call stack depth equals tree height.", + + bestAndWorstCase: + "**Best case** is a perfect binary tree — returns `2^h - 1` after a single height comparison.\n\n" + + "**Worst case** is still `O(log² n)`, much better than a naive `O(n)` count.", + + realWorldUses: [ + "**Heap size computation:** Binary heaps are complete trees; counting nodes efficiently is useful in heap algorithms.", + "**Segment tree validation:** Verifying that a segment tree has the expected node count.", + "**Proof of completeness:** Confirming that insertion followed the complete-tree property.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(log² n) time — significantly faster than visiting every node.", + "Exploits the complete tree invariant for a guaranteed optimization.", + ], + limitations: [ + "Only works correctly on complete binary trees — gives wrong results for arbitrary trees.", + "More complex than a simple `O(n)` DFS count.", + ], + }, + + whenToUseIt: + "Use only when the input is guaranteed to be a complete binary tree, such as a binary heap. " + + "For arbitrary binary trees, a simple DFS count is correct and simpler.", +}; diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/index.ts b/src/algorithms/trees/properties/count-complete-tree-nodes/index.ts new file mode 100644 index 00000000..af791831 --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { countCompleteTreeNodes } from "./sources/count-complete-tree-nodes.ts?fn"; +import { generateCountCompleteTreeNodesSteps } from "./step-generator"; +import type { CountCompleteTreeNodesInput } from "./step-generator"; +import { countCompleteTreeNodesEducational } from "./educational"; + +import typescriptSource from "./sources/count-complete-tree-nodes.ts?raw"; +import pythonSource from "./sources/count-complete-tree-nodes.py?raw"; +import javaSource from "./sources/CountCompleteTreeNodes.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeCountCompleteTreeNodes(input: CountCompleteTreeNodesInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return countCompleteTreeNodes(toBSTNode(input.rootId)) as number; +} + +const countCompleteTreeNodesDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.COUNT_COMPLETE_TREE_NODES!, + name: "Count Complete Tree Nodes", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Counts nodes in a complete binary tree in O(log²n) by exploiting the property that a subtree with equal left and right heights is a perfect binary tree", + timeComplexity: { best: "O(log²n)", average: "O(log²n)", worst: "O(log²n)" }, + spaceComplexity: "O(log n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeCountCompleteTreeNodes, + generateSteps: generateCountCompleteTreeNodesSteps, + educational: countCompleteTreeNodesEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(countCompleteTreeNodesDefinition); diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/sources/CountCompleteTreeNodes.java b/src/algorithms/trees/properties/count-complete-tree-nodes/sources/CountCompleteTreeNodes.java new file mode 100644 index 00000000..c8afd657 --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/sources/CountCompleteTreeNodes.java @@ -0,0 +1,39 @@ +// Count Complete Tree Nodes — if left height equals right height, nodes = 2^h - 1, else recurse + +class CompleteTreeNode { + int value; + CompleteTreeNode left, right; + CompleteTreeNode(int value) { this.value = value; } +} + +class CountCompleteTreeNodes { + public int countCompleteTreeNodes(CompleteTreeNode root) { + if (root == null) return 0; // @step:initialize + + // Compute left-most height and right-most height + int leftHeight = 0; // @step:initialize + int rightHeight = 0; // @step:initialize + + CompleteTreeNode leftCursor = root; // @step:traverse-left + while (leftCursor != null) { // @step:traverse-left + leftHeight++; // @step:update-height + leftCursor = leftCursor.left; // @step:traverse-left + } + + CompleteTreeNode rightCursor = root; // @step:traverse-right + while (rightCursor != null) { // @step:traverse-right + rightHeight++; // @step:update-height + rightCursor = rightCursor.right; // @step:traverse-right + } + + // If heights match, the tree is a perfect binary tree + if (leftHeight == rightHeight) { // @step:check-balance + return (int) Math.pow(2, leftHeight) - 1; // @step:add-to-result + } + + // Otherwise recurse on both subtrees + int leftCount = countCompleteTreeNodes(root.left); // @step:traverse-left + int rightCount = countCompleteTreeNodes(root.right); // @step:traverse-right + return leftCount + rightCount + 1; // @step:add-to-result + } +} diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/sources/count-complete-tree-nodes.py b/src/algorithms/trees/properties/count-complete-tree-nodes/sources/count-complete-tree-nodes.py new file mode 100644 index 00000000..393a9932 --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/sources/count-complete-tree-nodes.py @@ -0,0 +1,34 @@ +# Count Complete Tree Nodes — if left height equals right height, nodes = 2^h - 1, else recurse + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def count_complete_tree_nodes(root) -> int: + if root is None: # @step:initialize + return 0 + + # Compute left-most height and right-most height + left_height = 0 # @step:initialize + right_height = 0 # @step:initialize + + left_cursor = root # @step:traverse-left + while left_cursor is not None: # @step:traverse-left + left_height += 1 # @step:update-height + left_cursor = left_cursor.left # @step:traverse-left + + right_cursor = root # @step:traverse-right + while right_cursor is not None: # @step:traverse-right + right_height += 1 # @step:update-height + right_cursor = right_cursor.right # @step:traverse-right + + # If heights match, the tree is a perfect binary tree + if left_height == right_height: # @step:check-balance + return (2 ** left_height) - 1 # @step:add-to-result + + # Otherwise recurse on both subtrees + left_count = count_complete_tree_nodes(root.left) # @step:traverse-left + right_count = count_complete_tree_nodes(root.right) # @step:traverse-right + return left_count + right_count + 1 # @step:add-to-result diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/sources/count-complete-tree-nodes.ts b/src/algorithms/trees/properties/count-complete-tree-nodes/sources/count-complete-tree-nodes.ts new file mode 100644 index 00000000..47509879 --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/sources/count-complete-tree-nodes.ts @@ -0,0 +1,40 @@ +// Count Complete Tree Nodes — if left height equals right height, nodes = 2^h - 1, else recurse + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function countCompleteTreeNodes(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + // Compute left-most height and right-most height + let leftHeight = 0; // @step:initialize + let rightHeight = 0; // @step:initialize + + let leftCursor: TreeNode | null = root; // @step:traverse-left + while (leftCursor !== null) { + // @step:traverse-left + leftHeight += 1; // @step:update-height + leftCursor = leftCursor.left; // @step:traverse-left + } + + let rightCursor: TreeNode | null = root; // @step:traverse-right + while (rightCursor !== null) { + // @step:traverse-right + rightHeight += 1; // @step:update-height + rightCursor = rightCursor.right; // @step:traverse-right + } + + // If heights match, the tree is a perfect binary tree + if (leftHeight === rightHeight) { + // @step:check-balance + return Math.pow(2, leftHeight) - 1; // @step:add-to-result + } + + // Otherwise recurse on both subtrees + const leftCount = countCompleteTreeNodes(root.left); // @step:traverse-left + const rightCount = countCompleteTreeNodes(root.right); // @step:traverse-right + return leftCount + rightCount + 1; // @step:add-to-result +} diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/step-generator.test.ts b/src/algorithms/trees/properties/count-complete-tree-nodes/step-generator.test.ts new file mode 100644 index 00000000..a538f4d9 --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateCountCompleteTreeNodesSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateCountCompleteTreeNodesSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateCountCompleteTreeNodesSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateCountCompleteTreeNodesSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateCountCompleteTreeNodesSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateCountCompleteTreeNodesSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateCountCompleteTreeNodesSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/count-complete-tree-nodes/step-generator.ts b/src/algorithms/trees/properties/count-complete-tree-nodes/step-generator.ts new file mode 100644 index 00000000..50edbe5f --- /dev/null +++ b/src/algorithms/trees/properties/count-complete-tree-nodes/step-generator.ts @@ -0,0 +1,68 @@ +/** Step generator for Count Complete Tree Nodes — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.COUNT_COMPLETE_TREE_NODES!); + +export interface CountCompleteTreeNodesInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateCountCompleteTreeNodesSteps( + input: CountCompleteTreeNodesInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + function countNodes(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + // Compute leftmost height + let leftHeight = 0; + let leftCursorId: string | null = nodeId; + while (leftCursorId) { + leftHeight += 1; + leftCursorId = nodeMap.get(leftCursorId)?.leftChildId ?? null; + } + + // Compute rightmost height + let rightHeight = 0; + let rightCursorId: string | null = nodeId; + while (rightCursorId) { + rightHeight += 1; + rightCursorId = nodeMap.get(rightCursorId)?.rightChildId ?? null; + } + + if (leftHeight === rightHeight) { + const perfectCount = Math.pow(2, leftHeight) - 1; + tracker.recordResult(perfectCount, { + currentNode: nodeId, + leftHeight, + rightHeight, + perfectCount, + }); + return perfectCount; + } + + const leftCount = countNodes(node.leftChildId); + const rightCount = countNodes(node.rightChildId); + return leftCount + rightCount + 1; + } + + const result = countNodes(rootId); + tracker.recordResult(result, { result }); + tracker.complete({ result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/CousinsInBinaryTreePipeline.stories.tsx b/src/algorithms/trees/properties/cousins-in-binary-tree/CousinsInBinaryTreePipeline.stories.tsx new file mode 100644 index 00000000..c49b1118 --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/CousinsInBinaryTreePipeline.stories.tsx @@ -0,0 +1,116 @@ +/** + * Storybook pipeline stories for Cousins in Binary Tree. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateCousinsInBinaryTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateCousinsInBinaryTreeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 5, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Cousins in Binary Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/cousins-in-binary-tree.test.ts b/src/algorithms/trees/properties/cousins-in-binary-tree/cousins-in-binary-tree.test.ts new file mode 100644 index 00000000..4cabc0f9 --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/cousins-in-binary-tree.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { cousinsInBinaryTree } from "./sources/cousins-in-binary-tree.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("cousinsInBinaryTree", () => { + it("returns true for nodes 1 and 5 (cousins in 7-node BST)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(cousinsInBinaryTree(root, 1, 5)).toBe(true); + }); + + it("returns false for siblings (same parent)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(cousinsInBinaryTree(root, 1, 3)).toBe(false); + }); + + it("returns false for nodes at different depths", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(cousinsInBinaryTree(root, 2, 1)).toBe(false); + }); + + it("returns false for null root", () => { + expect(cousinsInBinaryTree(null, 1, 2)).toBe(false); + }); + + it("returns true for another valid cousin pair (3 and 7)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(cousinsInBinaryTree(root, 3, 7)).toBe(true); + }); +}); diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/educational.ts b/src/algorithms/trees/properties/cousins-in-binary-tree/educational.ts new file mode 100644 index 00000000..4ed55b43 --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/educational.ts @@ -0,0 +1,42 @@ +import type { EducationalContent } from "@/types"; + +export const cousinsInBinaryTreeEducational: EducationalContent = { + overview: + "**Cousins in Binary Tree** determines whether two nodes are cousins — meaning they are at the same depth " + + "in the tree but have different parents. Siblings (same parent) are NOT cousins.", + + howItWorks: + "BFS traversal tracks two facts for both target nodes:\n\n" + + "1. **Depth** — the BFS level at which the node was found.\n" + + "2. **Parent** — the parent node object (for identity comparison).\n\n" + + "After traversal, the two nodes are cousins if and only if `depthA === depthB && parentA !== parentB`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — worst case visits all nodes.\n\n" + + "**Space Complexity: `O(w)`** — BFS queue width.", + + bestAndWorstCase: + "**Best case** is when both targets are found near the root — BFS terminates early.\n\n" + + "**Worst case** is when both targets are at the deepest level — all nodes are visited.", + + realWorldUses: [ + "**Network topology:** Identifying nodes at the same hop count from the root with different upstream parents.", + "**Organizational hierarchy:** Finding employees at the same level but different manager chains.", + "**Gene relationships:** Identifying genes that diverged at the same evolutionary distance from a common ancestor.", + ], + + strengthsAndLimitations: { + strengths: [ + "BFS naturally produces level and parent information without extra passes.", + "Clean termination condition: same depth, different parent.", + ], + limitations: [ + "Uses O(w) queue memory.", + "Does not handle duplicate node values — assumes each value is unique.", + ], + }, + + whenToUseIt: + "Use cousins-in-binary-tree when checking level-relative sibling relationships. " + + "If performance is critical and both targets are expected to be near the root, BFS is optimal due to early termination.", +}; diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/index.ts b/src/algorithms/trees/properties/cousins-in-binary-tree/index.ts new file mode 100644 index 00000000..3e4b4170 --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/index.ts @@ -0,0 +1,124 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { cousinsInBinaryTree } from "./sources/cousins-in-binary-tree.ts?fn"; +import { generateCousinsInBinaryTreeSteps } from "./step-generator"; +import type { CousinsInBinaryTreeInput } from "./step-generator"; +import { cousinsInBinaryTreeEducational } from "./educational"; + +import typescriptSource from "./sources/cousins-in-binary-tree.ts?raw"; +import pythonSource from "./sources/cousins-in-binary-tree.py?raw"; +import javaSource from "./sources/CousinsInBinaryTree.java?raw"; + +/** Balanced 7-node BST: root=4, nodes 1 and 5 are cousins (depth 2, different parents 2 and 6) */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeCousinsInBinaryTree(input: CousinsInBinaryTreeInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return cousinsInBinaryTree( + toBSTNode(input.rootId), + input.nodeValueA, + input.nodeValueB, + ) as boolean; +} + +const cousinsInBinaryTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.COUSINS_IN_BINARY_TREE!, + name: "Cousins in Binary Tree", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Determines if two nodes are cousins — same depth in the tree but with different parents. Uses BFS to track depth and parent for both target nodes.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", nodeValueA: 1, nodeValueB: 5 }, + }, + execute: executeCousinsInBinaryTree, + generateSteps: generateCousinsInBinaryTreeSteps, + educational: cousinsInBinaryTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(cousinsInBinaryTreeDefinition); diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/sources/CousinsInBinaryTree.java b/src/algorithms/trees/properties/cousins-in-binary-tree/sources/CousinsInBinaryTree.java new file mode 100644 index 00000000..73109538 --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/sources/CousinsInBinaryTree.java @@ -0,0 +1,46 @@ +// Cousins in Binary Tree — BFS: check if two nodes are at same depth with different parents +import java.util.LinkedList; +import java.util.Queue; + +class CousinsNode { + int value; + CousinsNode left, right; + CousinsNode(int value) { this.value = value; } +} + +class CousinsInBinaryTree { + public boolean cousinsInBinaryTree(CousinsNode root, int nodeValueA, int nodeValueB) { + if (root == null) return false; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(new Object[]{root, null, 0}); // @step:initialize + + CousinsNode parentA = null; // @step:initialize + CousinsNode parentB = null; // @step:initialize + int depthA = -1; // @step:initialize + int depthB = -1; // @step:initialize + + while (!queue.isEmpty()) { // @step:visit + Object[] entry = queue.poll(); // @step:visit + CousinsNode current = (CousinsNode) entry[0]; // @step:visit + CousinsNode parent = (CousinsNode) entry[1]; // @step:visit + int currentDepth = (int) entry[2]; // @step:visit + + if (current.value == nodeValueA) { // @step:check-balance + parentA = parent; // @step:check-balance + depthA = currentDepth; // @step:update-height + } + + if (current.value == nodeValueB) { // @step:check-balance + parentB = parent; // @step:check-balance + depthB = currentDepth; // @step:update-height + } + + if (current.left != null) queue.offer(new Object[]{current.left, current, currentDepth + 1}); // @step:traverse-left + if (current.right != null) queue.offer(new Object[]{current.right, current, currentDepth + 1}); // @step:traverse-right + } + + // Cousins: same depth, different parents + return depthA == depthB && parentA != parentB; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/sources/cousins-in-binary-tree.py b/src/algorithms/trees/properties/cousins-in-binary-tree/sources/cousins-in-binary-tree.py new file mode 100644 index 00000000..c34d14fd --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/sources/cousins-in-binary-tree.py @@ -0,0 +1,37 @@ +# Cousins in Binary Tree — BFS: check if two nodes are at same depth with different parents +from collections import deque + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def cousins_in_binary_tree(root, node_value_a: int, node_value_b: int) -> bool: + if root is None: # @step:initialize + return False + + queue = deque([(root, None, 0)]) # @step:initialize + parent_a = None # @step:initialize + parent_b = None # @step:initialize + depth_a = -1 # @step:initialize + depth_b = -1 # @step:initialize + + while queue: # @step:visit + current, parent, current_depth = queue.popleft() # @step:visit + + if current.value == node_value_a: # @step:check-balance + parent_a = parent # @step:check-balance + depth_a = current_depth # @step:update-height + + if current.value == node_value_b: # @step:check-balance + parent_b = parent # @step:check-balance + depth_b = current_depth # @step:update-height + + if current.left is not None: # @step:traverse-left + queue.append((current.left, current, current_depth + 1)) # @step:traverse-left + if current.right is not None: # @step:traverse-right + queue.append((current.right, current, current_depth + 1)) # @step:traverse-right + + # Cousins: same depth, different parents + return depth_a == depth_b and parent_a is not parent_b # @step:complete diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/sources/cousins-in-binary-tree.ts b/src/algorithms/trees/properties/cousins-in-binary-tree/sources/cousins-in-binary-tree.ts new file mode 100644 index 00000000..26b872fd --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/sources/cousins-in-binary-tree.ts @@ -0,0 +1,46 @@ +// Cousins in Binary Tree — BFS: check if two nodes are at same depth with different parents + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function cousinsInBinaryTree( + root: TreeNode | null, + nodeValueA: number, + nodeValueB: number, +): boolean { + if (root === null) return false; // @step:initialize + + const queue: Array<[TreeNode, TreeNode | null, number]> = [[root, null, 0]]; // @step:initialize + + let parentA: TreeNode | null = null; // @step:initialize + let parentB: TreeNode | null = null; // @step:initialize + let depthA = -1; // @step:initialize + let depthB = -1; // @step:initialize + + while (queue.length > 0) { + // @step:visit + const entry = queue.shift()!; // @step:visit + const [current, parent, currentDepth] = entry; // @step:visit + + if (current.value === nodeValueA) { + // @step:check-balance + parentA = parent; // @step:check-balance + depthA = currentDepth; // @step:update-height + } + + if (current.value === nodeValueB) { + // @step:check-balance + parentB = parent; // @step:check-balance + depthB = currentDepth; // @step:update-height + } + + if (current.left !== null) queue.push([current.left, current, currentDepth + 1]); // @step:traverse-left + if (current.right !== null) queue.push([current.right, current, currentDepth + 1]); // @step:traverse-right + } + + // Cousins: same depth, different parents + return depthA === depthB && parentA !== parentB; // @step:complete +} diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/step-generator.test.ts b/src/algorithms/trees/properties/cousins-in-binary-tree/step-generator.test.ts new file mode 100644 index 00000000..d732c516 --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/step-generator.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateCousinsInBinaryTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateCousinsInBinaryTreeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateCousinsInBinaryTreeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 5, + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateCousinsInBinaryTreeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 5, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateCousinsInBinaryTreeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 5, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateCousinsInBinaryTreeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 5, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateCousinsInBinaryTreeSteps({ + nodes: defaultNodes, + rootId: "n4", + nodeValueA: 1, + nodeValueB: 5, + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/cousins-in-binary-tree/step-generator.ts b/src/algorithms/trees/properties/cousins-in-binary-tree/step-generator.ts new file mode 100644 index 00000000..86bfa5fb --- /dev/null +++ b/src/algorithms/trees/properties/cousins-in-binary-tree/step-generator.ts @@ -0,0 +1,69 @@ +/** Step generator for Cousins in Binary Tree — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.COUSINS_IN_BINARY_TREE!); + +export interface CousinsInBinaryTreeInput { + nodes: TreeNode[]; + rootId: string; + nodeValueA: number; + nodeValueB: number; +} + +export function generateCousinsInBinaryTreeSteps(input: CousinsInBinaryTreeInput): ExecutionStep[] { + const { nodes, rootId, nodeValueA, nodeValueB } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, nodeValueA, nodeValueB }); + + const queue: Array<[string, string | null, number]> = [[rootId, null, 0]]; + let parentAId: string | null = null; + let parentBId: string | null = null; + let depthA = -1; + let depthB = -1; + + while (queue.length > 0) { + const entry = queue.shift()!; + const [nodeId, parentId, depth] = entry; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, depth, parent: parentId }); + + if (node.value === nodeValueA) { + parentAId = parentId; + depthA = depth; + tracker.updateDepth(nodeId, depth, { + currentNode: nodeId, + foundNodeA: nodeValueA, + depth, + parent: parentId, + }); + } + + if (node.value === nodeValueB) { + parentBId = parentId; + depthB = depth; + tracker.updateDepth(nodeId, depth, { + currentNode: nodeId, + foundNodeB: nodeValueB, + depth, + parent: parentId, + }); + } + + if (node.leftChildId) queue.push([node.leftChildId, nodeId, depth + 1]); + if (node.rightChildId) queue.push([node.rightChildId, nodeId, depth + 1]); + } + + const areCousins = depthA === depthB && parentAId !== parentBId; + tracker.recordResult(areCousins, { result: areCousins, depthA, depthB, parentAId, parentBId }); + tracker.complete({ result: areCousins }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/DiameterOfBinaryTreePipeline.stories.tsx b/src/algorithms/trees/properties/diameter-of-binary-tree/DiameterOfBinaryTreePipeline.stories.tsx new file mode 100644 index 00000000..8c184d39 --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/DiameterOfBinaryTreePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Diameter of Binary Tree. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateDiameterOfBinaryTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateDiameterOfBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Diameter of Binary Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/diameter-of-binary-tree.test.ts b/src/algorithms/trees/properties/diameter-of-binary-tree/diameter-of-binary-tree.test.ts new file mode 100644 index 00000000..21dde448 --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/diameter-of-binary-tree.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { diameterOfBinaryTree } from "./sources/diameter-of-binary-tree.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("diameterOfBinaryTree", () => { + it("returns 4 for a balanced 7-node BST (leaf to leaf through root)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(diameterOfBinaryTree(root)).toBe(4); + }); + + it("returns 0 for a null root", () => { + expect(diameterOfBinaryTree(null)).toBe(0); + }); + + it("returns 0 for a single node", () => { + expect(diameterOfBinaryTree(node(1))).toBe(0); + }); + + it("returns 1 for a two-node tree", () => { + expect(diameterOfBinaryTree(node(1, node(2)))).toBe(1); + }); + + it("returns correct diameter for a skewed tree", () => { + const root = node(1, node(2, node(3, node(4)))); + expect(diameterOfBinaryTree(root)).toBe(3); + }); +}); diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/educational.ts b/src/algorithms/trees/properties/diameter-of-binary-tree/educational.ts new file mode 100644 index 00000000..1b2b2c6e --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/educational.ts @@ -0,0 +1,43 @@ +import type { EducationalContent } from "@/types"; + +export const diameterOfBinaryTreeEducational: EducationalContent = { + overview: + "**Diameter of a Binary Tree** is the length of the longest path between any two nodes. " + + "This path may or may not pass through the root. The length is measured in number of edges.", + + howItWorks: + "A recursive post-order DFS computes two values simultaneously at each node:\n\n" + + "1. **Height** — the height of this node's subtree (returned up the call stack).\n" + + "2. **Local diameter** — `leftHeight + rightHeight` represents the longest path through this node.\n\n" + + "A global variable tracks the maximum local diameter seen across all nodes. " + + "The final diameter is recorded when the recursion completes.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth equals tree height.", + + bestAndWorstCase: + "**Best case** is a balanced tree where the diameter passes through the root and equals `2h - 2`.\n\n" + + "**Worst case** in terms of space is a degenerate tree with `O(n)` stack depth.", + + realWorldUses: [ + "**Network diameter:** The maximum latency path in a hierarchical network topology modeled as a tree.", + "**Phylogenetic tree analysis:** The evolutionary distance between two most-distant species.", + "**Social network depth:** Maximum degrees of separation in a tree-shaped contact graph.", + ], + + strengthsAndLimitations: { + strengths: [ + "Computes height and diameter in a single DFS pass — O(n) time with no redundant work.", + "Simple to implement once the key insight (local path = leftHeight + rightHeight) is understood.", + ], + limitations: [ + "Requires careful handling of the global state (max diameter) in recursive implementations.", + "Recursive version risks stack overflow on degenerate trees.", + ], + }, + + whenToUseIt: + "Use diameter when you need to find the longest inter-node path in a tree. " + + "If the tree can be very deep, consider an iterative post-order approach to avoid stack overflow.", +}; diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/index.ts b/src/algorithms/trees/properties/diameter-of-binary-tree/index.ts new file mode 100644 index 00000000..fea64684 --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { diameterOfBinaryTree } from "./sources/diameter-of-binary-tree.ts?fn"; +import { generateDiameterOfBinaryTreeSteps } from "./step-generator"; +import type { DiameterOfBinaryTreeInput } from "./step-generator"; +import { diameterOfBinaryTreeEducational } from "./educational"; + +import typescriptSource from "./sources/diameter-of-binary-tree.ts?raw"; +import pythonSource from "./sources/diameter-of-binary-tree.py?raw"; +import javaSource from "./sources/DiameterOfBinaryTree.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeDiameterOfBinaryTree(input: DiameterOfBinaryTreeInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return diameterOfBinaryTree(toBSTNode(input.rootId)) as number; +} + +const diameterOfBinaryTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.DIAMETER_OF_BINARY_TREE!, + name: "Diameter of Binary Tree", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Finds the longest path between any two nodes in the tree (the diameter). At each node, the local path length is leftHeight + rightHeight.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeDiameterOfBinaryTree, + generateSteps: generateDiameterOfBinaryTreeSteps, + educational: diameterOfBinaryTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(diameterOfBinaryTreeDefinition); diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/sources/DiameterOfBinaryTree.java b/src/algorithms/trees/properties/diameter-of-binary-tree/sources/DiameterOfBinaryTree.java new file mode 100644 index 00000000..9cc2a84a --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/sources/DiameterOfBinaryTree.java @@ -0,0 +1,29 @@ +// Diameter of Binary Tree — track max of (leftHeight + rightHeight) at each node + +class DiameterNode { + int value; + DiameterNode left, right; + DiameterNode(int value) { this.value = value; } +} + +class DiameterOfBinaryTree { + private int maxDiameter = 0; // @step:initialize + + public int diameterOfBinaryTree(DiameterNode root) { + maxDiameter = 0; // @step:initialize + computeHeight(root); // @step:initialize + return maxDiameter; // @step:complete + } + + private int computeHeight(DiameterNode node) { + if (node == null) return 0; // @step:initialize + + int leftHeight = computeHeight(node.left); // @step:traverse-left + int rightHeight = computeHeight(node.right); // @step:traverse-right + + // Update global max diameter + maxDiameter = Math.max(maxDiameter, leftHeight + rightHeight); // @step:update-height + + return Math.max(leftHeight, rightHeight) + 1; // @step:update-height + } +} diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/sources/diameter-of-binary-tree.py b/src/algorithms/trees/properties/diameter-of-binary-tree/sources/diameter-of-binary-tree.py new file mode 100644 index 00000000..ef8ee773 --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/sources/diameter-of-binary-tree.py @@ -0,0 +1,25 @@ +# Diameter of Binary Tree — track max of (leftHeight + rightHeight) at each node + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def diameter_of_binary_tree(root) -> int: + max_diameter = [0] # @step:initialize + + def compute_height(node) -> int: + if node is None: # @step:initialize + return 0 + + left_height = compute_height(node.left) # @step:traverse-left + right_height = compute_height(node.right) # @step:traverse-right + + # Update global max diameter + max_diameter[0] = max(max_diameter[0], left_height + right_height) # @step:update-height + + return max(left_height, right_height) + 1 # @step:update-height + + compute_height(root) # @step:initialize + return max_diameter[0] # @step:complete diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/sources/diameter-of-binary-tree.ts b/src/algorithms/trees/properties/diameter-of-binary-tree/sources/diameter-of-binary-tree.ts new file mode 100644 index 00000000..38f482d0 --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/sources/diameter-of-binary-tree.ts @@ -0,0 +1,26 @@ +// Diameter of Binary Tree — track max of (leftHeight + rightHeight) at each node + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function diameterOfBinaryTree(root: TreeNode | null): number { + let maxDiameter = 0; // @step:initialize + + function computeHeight(node: TreeNode | null): number { + if (node === null) return 0; // @step:initialize + + const leftHeight = computeHeight(node.left); // @step:traverse-left + const rightHeight = computeHeight(node.right); // @step:traverse-right + + // Update global max diameter — path through this node spans leftHeight + rightHeight edges + maxDiameter = Math.max(maxDiameter, leftHeight + rightHeight); // @step:update-height + + return Math.max(leftHeight, rightHeight) + 1; // @step:update-height + } + + computeHeight(root); // @step:initialize + return maxDiameter; // @step:complete +} diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/step-generator.test.ts b/src/algorithms/trees/properties/diameter-of-binary-tree/step-generator.test.ts new file mode 100644 index 00000000..7e0c38c8 --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateDiameterOfBinaryTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateDiameterOfBinaryTreeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateDiameterOfBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDiameterOfBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDiameterOfBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateDiameterOfBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateDiameterOfBinaryTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/diameter-of-binary-tree/step-generator.ts b/src/algorithms/trees/properties/diameter-of-binary-tree/step-generator.ts new file mode 100644 index 00000000..b1c4c05d --- /dev/null +++ b/src/algorithms/trees/properties/diameter-of-binary-tree/step-generator.ts @@ -0,0 +1,51 @@ +/** Step generator for Diameter of Binary Tree — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.DIAMETER_OF_BINARY_TREE!); + +export interface DiameterOfBinaryTreeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateDiameterOfBinaryTreeSteps( + input: DiameterOfBinaryTreeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + let maxDiameter = 0; + + function computeHeight(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + const leftHeight = computeHeight(node.leftChildId); + const rightHeight = computeHeight(node.rightChildId); + + const localDiameter = leftHeight + rightHeight; + if (localDiameter > maxDiameter) { + maxDiameter = localDiameter; + } + + const height = Math.max(leftHeight, rightHeight) + 1; + tracker.updateHeight(nodeId, height, { currentNode: nodeId, height, diameter: maxDiameter }); + return height; + } + + computeHeight(rootId); + tracker.recordResult(maxDiameter, { result: maxDiameter }); + tracker.complete({ result: maxDiameter }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/IsBalancedTreeIterativePipeline.stories.tsx b/src/algorithms/trees/properties/is-balanced-tree-iterative/IsBalancedTreeIterativePipeline.stories.tsx new file mode 100644 index 00000000..51e5dad8 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/IsBalancedTreeIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Is Balanced Tree Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateIsBalancedTreeIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateIsBalancedTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Is Balanced Tree Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/educational.ts b/src/algorithms/trees/properties/is-balanced-tree-iterative/educational.ts new file mode 100644 index 00000000..fe72a719 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/educational.ts @@ -0,0 +1,43 @@ +import type { EducationalContent } from "@/types"; + +export const isBalancedTreeIterativeEducational: EducationalContent = { + overview: + "**Is Balanced Tree (Iterative)** performs the same balance check as the recursive version " + + "but uses an explicit stack to simulate post-order traversal. " + + "A `Map` stores computed heights so they can be looked up during the post-processing phase.", + + howItWorks: + "The algorithm simulates the three phases of recursive post-order DFS using a phase counter:\n\n" + + "- **Phase 0:** First visit — push left child if it exists.\n" + + "- **Phase 1:** Left done — push right child if it exists.\n" + + "- **Phase 2:** Both children done — compute balance and record height.\n\n" + + "Heights are stored in a `Map` and retrieved when processing a parent node.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node is processed once.\n\n" + + "**Space Complexity: `O(n)`** — the height map stores an entry for every node.", + + bestAndWorstCase: + "**Best case** is an imbalanced tree found near the leaves — returns `false` quickly.\n\n" + + "**Worst case** is a fully balanced tree — all `n` nodes are processed.", + + realWorldUses: [ + "**Production-safe balance checks:** No risk of stack overflow for arbitrarily deep trees.", + "**Integration with iterative tree operations:** Consistent with iterative insert/delete that also avoid recursion.", + ], + + strengthsAndLimitations: { + strengths: [ + "Stack-overflow-safe for arbitrarily deep trees.", + "Equivalent correctness to the recursive version.", + ], + limitations: [ + "More complex implementation than the recursive approach.", + "Requires O(n) extra memory for the height map.", + ], + }, + + whenToUseIt: + "Use the iterative version when tree depth is unbounded or when working in environments with recursion limits. " + + "For most practical balanced trees, the simpler recursive version is preferred.", +}; diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/index.ts b/src/algorithms/trees/properties/is-balanced-tree-iterative/index.ts new file mode 100644 index 00000000..d61cd2bf --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { isBalancedTreeIterative } from "./sources/is-balanced-tree-iterative.ts?fn"; +import { generateIsBalancedTreeIterativeSteps } from "./step-generator"; +import type { IsBalancedTreeIterativeInput } from "./step-generator"; +import { isBalancedTreeIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/is-balanced-tree-iterative.ts?raw"; +import pythonSource from "./sources/is-balanced-tree-iterative.py?raw"; +import javaSource from "./sources/IsBalancedTreeIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeIsBalancedTreeIterative(input: IsBalancedTreeIterativeInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return isBalancedTreeIterative(toBSTNode(input.rootId)) as boolean; +} + +const isBalancedTreeIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.IS_BALANCED_TREE_ITERATIVE!, + name: "Is Balanced Tree (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Iterative balance check using a simulated post-order traversal with explicit stack and a height-tracking map", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeIsBalancedTreeIterative, + generateSteps: generateIsBalancedTreeIterativeSteps, + educational: isBalancedTreeIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(isBalancedTreeIterativeDefinition); diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/is-balanced-tree-iterative.test.ts b/src/algorithms/trees/properties/is-balanced-tree-iterative/is-balanced-tree-iterative.test.ts new file mode 100644 index 00000000..32f26043 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/is-balanced-tree-iterative.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { isBalancedTreeIterative } from "./sources/is-balanced-tree-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("isBalancedTreeIterative", () => { + it("returns true for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(isBalancedTreeIterative(root)).toBe(true); + }); + + it("returns true for null root", () => { + expect(isBalancedTreeIterative(null)).toBe(true); + }); + + it("returns true for a single node", () => { + expect(isBalancedTreeIterative(node(1))).toBe(true); + }); + + it("returns false for an unbalanced tree", () => { + const root = node(1, node(2, node(3, node(4)))); + expect(isBalancedTreeIterative(root)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/IsBalancedTreeIterative.java b/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/IsBalancedTreeIterative.java new file mode 100644 index 00000000..0fcc3a7d --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/IsBalancedTreeIterative.java @@ -0,0 +1,55 @@ +// Is Balanced Tree (Iterative) — bottom-up post-order using stack with height tracking +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; + +class BalancedTreeIterativeNode { + int value; + BalancedTreeIterativeNode left, right; + BalancedTreeIterativeNode(int value) { this.value = value; } +} + +class IsBalancedTreeIterative { + public boolean isBalancedTreeIterative(BalancedTreeIterativeNode root) { + if (root == null) return true; // @step:initialize + + Deque stack = new ArrayDeque<>(); // @step:initialize + Map heights = new HashMap<>(); // @step:initialize + Deque nodeStack = new ArrayDeque<>(); // @step:initialize + + nodeStack.push(new BalancedTreeIterativeNode[]{root}); + stack.push(new int[]{0}); // phase 0 + + while (!nodeStack.isEmpty()) { // @step:visit + BalancedTreeIterativeNode[] nodeWrapper = nodeStack.peek(); // @step:visit + int[] phaseWrapper = stack.peek(); // @step:visit + BalancedTreeIterativeNode node = nodeWrapper[0]; // @step:visit + + if (phaseWrapper[0] == 0) { + phaseWrapper[0] = 1; // @step:visit + if (node.left != null) { // @step:traverse-left + nodeStack.push(new BalancedTreeIterativeNode[]{node.left}); + stack.push(new int[]{0}); + } + } else if (phaseWrapper[0] == 1) { + phaseWrapper[0] = 2; // @step:visit + if (node.right != null) { // @step:traverse-right + nodeStack.push(new BalancedTreeIterativeNode[]{node.right}); + stack.push(new int[]{0}); + } + } else { + nodeStack.pop(); // @step:visit + stack.pop(); + int leftHeight = node.left != null ? heights.getOrDefault(node.left, 0) : 0; // @step:check-balance + int rightHeight = node.right != null ? heights.getOrDefault(node.right, 0) : 0; // @step:check-balance + + if (Math.abs(leftHeight - rightHeight) > 1) return false; // @step:check-balance + + heights.put(node, Math.max(leftHeight, rightHeight) + 1); // @step:update-height + } + } + + return true; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/is-balanced-tree-iterative.py b/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/is-balanced-tree-iterative.py new file mode 100644 index 00000000..3118b685 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/is-balanced-tree-iterative.py @@ -0,0 +1,38 @@ +# Is Balanced Tree (Iterative) — bottom-up post-order using stack with height tracking + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def is_balanced_tree_iterative(root) -> bool: + if root is None: # @step:initialize + return True + + stack = [{"node": root, "phase": 0}] # @step:initialize + heights = {} # @step:initialize + + while stack: # @step:visit + entry = stack[-1] # @step:visit + node = entry["node"] # @step:visit + + if entry["phase"] == 0: + entry["phase"] = 1 # @step:visit + if node.left is not None: # @step:traverse-left + stack.append({"node": node.left, "phase": 0}) # @step:traverse-left + elif entry["phase"] == 1: + entry["phase"] = 2 # @step:visit + if node.right is not None: # @step:traverse-right + stack.append({"node": node.right, "phase": 0}) # @step:traverse-right + else: + stack.pop() # @step:visit + left_height = heights.get(id(node.left), 0) if node.left else 0 # @step:check-balance + right_height = heights.get(id(node.right), 0) if node.right else 0 # @step:check-balance + + if abs(left_height - right_height) > 1: # @step:check-balance + return False + + heights[id(node)] = max(left_height, right_height) + 1 # @step:update-height + + return True # @step:complete diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/is-balanced-tree-iterative.ts b/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/is-balanced-tree-iterative.ts new file mode 100644 index 00000000..c44bcbe5 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/sources/is-balanced-tree-iterative.ts @@ -0,0 +1,40 @@ +// Is Balanced Tree (Iterative) — bottom-up post-order using stack with height tracking + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function isBalancedTreeIterative(root: TreeNode | null): boolean { + if (root === null) return true; // @step:initialize + + const stack: Array<{ node: TreeNode; phase: number }> = []; // @step:initialize + const heights = new Map(); // @step:initialize + + stack.push({ node: root, phase: 0 }); // @step:initialize + + while (stack.length > 0) { + // @step:visit + const entry = stack[stack.length - 1]!; // @step:visit + const { node } = entry; // @step:visit + + if (entry.phase === 0) { + entry.phase = 1; // @step:visit + if (node.left !== null) stack.push({ node: node.left, phase: 0 }); // @step:traverse-left + } else if (entry.phase === 1) { + entry.phase = 2; // @step:visit + if (node.right !== null) stack.push({ node: node.right, phase: 0 }); // @step:traverse-right + } else { + stack.pop(); // @step:visit + const leftHeight = node.left !== null ? (heights.get(node.left) ?? 0) : 0; // @step:check-balance + const rightHeight = node.right !== null ? (heights.get(node.right) ?? 0) : 0; // @step:check-balance + + if (Math.abs(leftHeight - rightHeight) > 1) return false; // @step:check-balance + + heights.set(node, Math.max(leftHeight, rightHeight) + 1); // @step:update-height + } + } + + return true; // @step:complete +} diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/step-generator.test.ts b/src/algorithms/trees/properties/is-balanced-tree-iterative/step-generator.test.ts new file mode 100644 index 00000000..8cf2b8ce --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateIsBalancedTreeIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateIsBalancedTreeIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateIsBalancedTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateIsBalancedTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateIsBalancedTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateIsBalancedTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateIsBalancedTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/is-balanced-tree-iterative/step-generator.ts b/src/algorithms/trees/properties/is-balanced-tree-iterative/step-generator.ts new file mode 100644 index 00000000..60600b50 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree-iterative/step-generator.ts @@ -0,0 +1,68 @@ +/** Step generator for Is Balanced Tree (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.IS_BALANCED_TREE_ITERATIVE!); + +export interface IsBalancedTreeIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateIsBalancedTreeIterativeSteps( + input: IsBalancedTreeIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const stack: Array<{ nodeId: string; phase: number }> = [{ nodeId: rootId, phase: 0 }]; + const heights = new Map(); + let isBalanced = true; + + while (stack.length > 0 && isBalanced) { + const entry = stack[stack.length - 1]!; + const node = nodeMap.get(entry.nodeId); + if (!node) { + stack.pop(); + continue; + } + + if (entry.phase === 0) { + tracker.checkNode(entry.nodeId, { currentNode: entry.nodeId, value: node.value }); + entry.phase = 1; + if (node.leftChildId) stack.push({ nodeId: node.leftChildId, phase: 0 }); + } else if (entry.phase === 1) { + entry.phase = 2; + if (node.rightChildId) stack.push({ nodeId: node.rightChildId, phase: 0 }); + } else { + stack.pop(); + const leftHeight = node.leftChildId ? (heights.get(node.leftChildId) ?? 0) : 0; + const rightHeight = node.rightChildId ? (heights.get(node.rightChildId) ?? 0) : 0; + + if (Math.abs(leftHeight - rightHeight) > 1) { + tracker.markInvalid(entry.nodeId, { currentNode: entry.nodeId, leftHeight, rightHeight }); + isBalanced = false; + } else { + const height = Math.max(leftHeight, rightHeight) + 1; + heights.set(entry.nodeId, height); + tracker.markValid(entry.nodeId, { + currentNode: entry.nodeId, + leftHeight, + rightHeight, + height, + }); + } + } + } + + tracker.recordResult(isBalanced, { result: isBalanced }); + tracker.complete({ result: isBalanced }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/is-balanced-tree/IsBalancedTreePipeline.stories.tsx b/src/algorithms/trees/properties/is-balanced-tree/IsBalancedTreePipeline.stories.tsx new file mode 100644 index 00000000..f8ba650a --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/IsBalancedTreePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Is Balanced Tree. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateIsBalancedTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateIsBalancedTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Is Balanced Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/is-balanced-tree/educational.ts b/src/algorithms/trees/properties/is-balanced-tree/educational.ts new file mode 100644 index 00000000..330748d7 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const isBalancedTreeEducational: EducationalContent = { + overview: + "**Is Balanced Tree** checks whether a binary tree is height-balanced. " + + "A height-balanced tree requires that for every node, the heights of its left and right subtrees differ by at most 1. " + + "AVL trees enforce this invariant to guarantee O(log n) search time.", + + howItWorks: + "The algorithm uses a modified `checkHeight` helper that returns `-1` for unbalanced subtrees " + + "and the actual height for balanced ones:\n\n" + + "1. Recurse left — if `-1` propagates up, return `-1` immediately (short-circuit).\n" + + "2. Recurse right — same short-circuit.\n" + + "3. Check `abs(leftHeight - rightHeight) <= 1`.\n" + + "4. If balanced, return `max(leftHeight, rightHeight) + 1`.\n\n" + + "This avoids recomputing heights in a separate pass, achieving O(n) time.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node is checked once with short-circuit on imbalance.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case** is an imbalanced root — returns `-1` immediately after checking the two subtrees.\n\n" + + "**Worst case** is a balanced tree — all `n` nodes must be checked.", + + realWorldUses: [ + "**AVL tree maintenance:** After every insert or delete, balance must be verified and rotations applied.", + "**Database index validation:** B-tree variants require balance to guarantee O(log n) lookups.", + "**Game tree optimization:** Balanced decision trees ensure bounded search depth.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time with no extra space beyond the call stack.", + "Early termination on the first imbalanced node found.", + ], + limitations: [ + "Cannot short-circuit if the imbalanced node is a leaf deep in a large tree.", + "Recursive approach risks stack overflow for degenerate inputs.", + ], + }, + + whenToUseIt: + "Use this to validate AVL tree invariants or verify that a tree is balanced after construction. " + + "For iterative balance checking (safer for large trees), use the stack-based variant.", +}; diff --git a/src/algorithms/trees/properties/is-balanced-tree/index.ts b/src/algorithms/trees/properties/is-balanced-tree/index.ts new file mode 100644 index 00000000..5967a66f --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { isBalancedTree } from "./sources/is-balanced-tree.ts?fn"; +import { generateIsBalancedTreeSteps } from "./step-generator"; +import type { IsBalancedTreeInput } from "./step-generator"; +import { isBalancedTreeEducational } from "./educational"; + +import typescriptSource from "./sources/is-balanced-tree.ts?raw"; +import pythonSource from "./sources/is-balanced-tree.py?raw"; +import javaSource from "./sources/IsBalancedTree.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeIsBalancedTree(input: IsBalancedTreeInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return isBalancedTree(toBSTNode(input.rootId)) as boolean; +} + +const isBalancedTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.IS_BALANCED_TREE!, + name: "Is Balanced Tree", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Checks if a binary tree is height-balanced — every node must have left and right subtrees differing in height by at most 1", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeIsBalancedTree, + generateSteps: generateIsBalancedTreeSteps, + educational: isBalancedTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(isBalancedTreeDefinition); diff --git a/src/algorithms/trees/properties/is-balanced-tree/is-balanced-tree.test.ts b/src/algorithms/trees/properties/is-balanced-tree/is-balanced-tree.test.ts new file mode 100644 index 00000000..d6cefc15 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/is-balanced-tree.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { isBalancedTree } from "./sources/is-balanced-tree.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("isBalancedTree", () => { + it("returns true for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(isBalancedTree(root)).toBe(true); + }); + + it("returns true for null root", () => { + expect(isBalancedTree(null)).toBe(true); + }); + + it("returns true for a single node", () => { + expect(isBalancedTree(node(1))).toBe(true); + }); + + it("returns false for an unbalanced tree", () => { + const root = node(1, node(2, node(3, node(4)))); + expect(isBalancedTree(root)).toBe(false); + }); + + it("returns true for a two-node tree", () => { + expect(isBalancedTree(node(1, node(2)))).toBe(true); + }); +}); diff --git a/src/algorithms/trees/properties/is-balanced-tree/sources/IsBalancedTree.java b/src/algorithms/trees/properties/is-balanced-tree/sources/IsBalancedTree.java new file mode 100644 index 00000000..f36027f1 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/sources/IsBalancedTree.java @@ -0,0 +1,29 @@ +// Is Balanced Tree — recursive DFS checking abs(leftHeight - rightHeight) ≤ 1 at every node + +class BalancedTreeNode { + int value; + BalancedTreeNode left, right; + BalancedTreeNode(int value) { this.value = value; } +} + +class IsBalancedTree { + public boolean isBalancedTree(BalancedTreeNode root) { + return checkHeight(root) != -1; // @step:complete + } + + // Returns -1 if unbalanced, otherwise returns height of the subtree + private int checkHeight(BalancedTreeNode node) { + if (node == null) return 0; // @step:initialize + + int leftHeight = checkHeight(node.left); // @step:traverse-left + if (leftHeight == -1) return -1; // @step:check-balance + + int rightHeight = checkHeight(node.right); // @step:traverse-right + if (rightHeight == -1) return -1; // @step:check-balance + + // Unbalanced if height difference exceeds 1 + if (Math.abs(leftHeight - rightHeight) > 1) return -1; // @step:check-balance + + return Math.max(leftHeight, rightHeight) + 1; // @step:update-height + } +} diff --git a/src/algorithms/trees/properties/is-balanced-tree/sources/is-balanced-tree.py b/src/algorithms/trees/properties/is-balanced-tree/sources/is-balanced-tree.py new file mode 100644 index 00000000..d174fae5 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/sources/is-balanced-tree.py @@ -0,0 +1,29 @@ +# Is Balanced Tree — recursive DFS checking abs(leftHeight - rightHeight) ≤ 1 at every node + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def is_balanced_tree(root) -> bool: + # Returns -1 if unbalanced, otherwise returns height of the subtree + def check_height(node) -> int: + if node is None: # @step:initialize + return 0 + + left_height = check_height(node.left) # @step:traverse-left + if left_height == -1: # @step:check-balance + return -1 + + right_height = check_height(node.right) # @step:traverse-right + if right_height == -1: # @step:check-balance + return -1 + + # Unbalanced if height difference exceeds 1 + if abs(left_height - right_height) > 1: # @step:check-balance + return -1 + + return max(left_height, right_height) + 1 # @step:update-height + + return check_height(root) != -1 # @step:complete diff --git a/src/algorithms/trees/properties/is-balanced-tree/sources/is-balanced-tree.ts b/src/algorithms/trees/properties/is-balanced-tree/sources/is-balanced-tree.ts new file mode 100644 index 00000000..34e9bf52 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/sources/is-balanced-tree.ts @@ -0,0 +1,27 @@ +// Is Balanced Tree — recursive DFS checking abs(leftHeight - rightHeight) ≤ 1 at every node + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function isBalancedTree(root: TreeNode | null): boolean { + // Returns -1 if unbalanced, otherwise returns height of the subtree + function checkHeight(node: TreeNode | null): number { + if (node === null) return 0; // @step:initialize + + const leftHeight = checkHeight(node.left); // @step:traverse-left + if (leftHeight === -1) return -1; // @step:check-balance + + const rightHeight = checkHeight(node.right); // @step:traverse-right + if (rightHeight === -1) return -1; // @step:check-balance + + // Unbalanced if height difference exceeds 1 + if (Math.abs(leftHeight - rightHeight) > 1) return -1; // @step:check-balance + + return Math.max(leftHeight, rightHeight) + 1; // @step:update-height + } + + return checkHeight(root) !== -1; // @step:complete +} diff --git a/src/algorithms/trees/properties/is-balanced-tree/step-generator.test.ts b/src/algorithms/trees/properties/is-balanced-tree/step-generator.test.ts new file mode 100644 index 00000000..6b651157 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateIsBalancedTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateIsBalancedTreeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateIsBalancedTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateIsBalancedTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateIsBalancedTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateIsBalancedTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateIsBalancedTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/is-balanced-tree/step-generator.ts b/src/algorithms/trees/properties/is-balanced-tree/step-generator.ts new file mode 100644 index 00000000..3db0cef4 --- /dev/null +++ b/src/algorithms/trees/properties/is-balanced-tree/step-generator.ts @@ -0,0 +1,63 @@ +/** Step generator for Is Balanced Tree — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.IS_BALANCED_TREE!); + +export interface IsBalancedTreeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateIsBalancedTreeSteps(input: IsBalancedTreeInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + // Returns -1 if unbalanced, height otherwise + function checkHeight(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + const leftHeight = checkHeight(node.leftChildId); + if (leftHeight === -1) { + tracker.markInvalid(nodeId, { currentNode: nodeId, reason: "left subtree unbalanced" }); + return -1; + } + + const rightHeight = checkHeight(node.rightChildId); + if (rightHeight === -1) { + tracker.markInvalid(nodeId, { currentNode: nodeId, reason: "right subtree unbalanced" }); + return -1; + } + + if (Math.abs(leftHeight - rightHeight) > 1) { + tracker.markInvalid(nodeId, { + currentNode: nodeId, + leftHeight, + rightHeight, + diff: Math.abs(leftHeight - rightHeight), + }); + return -1; + } + + const height = Math.max(leftHeight, rightHeight) + 1; + tracker.markValid(nodeId, { currentNode: nodeId, leftHeight, rightHeight, height }); + return height; + } + + const heightResult = checkHeight(rootId); + const isBalanced = heightResult !== -1; + tracker.recordResult(isBalanced, { result: isBalanced }); + tracker.complete({ result: isBalanced }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/IsSymmetricTreeIterativePipeline.stories.tsx b/src/algorithms/trees/properties/is-symmetric-tree-iterative/IsSymmetricTreeIterativePipeline.stories.tsx new file mode 100644 index 00000000..8909f3df --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/IsSymmetricTreeIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Is Symmetric Tree Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateIsSymmetricTreeIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateIsSymmetricTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Is Symmetric Tree Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/educational.ts b/src/algorithms/trees/properties/is-symmetric-tree-iterative/educational.ts new file mode 100644 index 00000000..c7f55646 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/educational.ts @@ -0,0 +1,40 @@ +import type { EducationalContent } from "@/types"; + +export const isSymmetricTreeIterativeEducational: EducationalContent = { + overview: + "**Is Symmetric Tree (Iterative)** checks tree symmetry using a queue of node pairs. " + + "Instead of recursive calls, it processes mirror pairs level by level, " + + "comparing outer and inner pairs at each step.", + + howItWorks: + "1. Initialize the queue with `[root.left, root.right]`.\n" + + "2. Dequeue a pair `[left, right]`.\n" + + "3. If both null — symmetric, continue.\n" + + "4. If one null or values differ — return `false`.\n" + + "5. Enqueue outer pair `[left.left, right.right]` and inner pair `[left.right, right.left]`.\n" + + "6. If the queue empties without failure — return `true`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — all node pairs are processed.\n\n" + + "**Space Complexity: `O(w)`** — the queue holds at most one level of node pairs.", + + bestAndWorstCase: + "**Best case** is a root with mismatching children — terminates after the first pair check.\n\n" + + "**Worst case** is a symmetric tree — all pairs must be enqueued and processed.", + + realWorldUses: [ + "**Stack-overflow-safe symmetry checking:** Works on arbitrarily deep trees.", + "**BFS-parallel symmetry:** Naturally processes symmetric levels together.", + ], + + strengthsAndLimitations: { + strengths: [ + "No stack overflow risk from deep recursion.", + "BFS ordering makes it easy to add per-level symmetry diagnostics.", + ], + limitations: ["Queue uses O(w) memory, which can be significant for wide trees."], + }, + + whenToUseIt: + "Use the queue-based iterative version for very deep trees or in environments with recursion limits.", +}; diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/index.ts b/src/algorithms/trees/properties/is-symmetric-tree-iterative/index.ts new file mode 100644 index 00000000..3fca0f65 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { isSymmetricTreeIterative } from "./sources/is-symmetric-tree-iterative.ts?fn"; +import { generateIsSymmetricTreeIterativeSteps } from "./step-generator"; +import type { IsSymmetricTreeIterativeInput } from "./step-generator"; +import { isSymmetricTreeIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/is-symmetric-tree-iterative.ts?raw"; +import pythonSource from "./sources/is-symmetric-tree-iterative.py?raw"; +import javaSource from "./sources/IsSymmetricTreeIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeIsSymmetricTreeIterative(input: IsSymmetricTreeIterativeInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return isSymmetricTreeIterative(toBSTNode(input.rootId)) as boolean; +} + +const isSymmetricTreeIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.IS_SYMMETRIC_TREE_ITERATIVE!, + name: "Is Symmetric Tree (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Queue-based symmetry check — enqueues mirror pairs and compares them level by level", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeIsSymmetricTreeIterative, + generateSteps: generateIsSymmetricTreeIterativeSteps, + educational: isSymmetricTreeIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(isSymmetricTreeIterativeDefinition); diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/is-symmetric-tree-iterative.test.ts b/src/algorithms/trees/properties/is-symmetric-tree-iterative/is-symmetric-tree-iterative.test.ts new file mode 100644 index 00000000..109b83d5 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/is-symmetric-tree-iterative.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { isSymmetricTreeIterative } from "./sources/is-symmetric-tree-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("isSymmetricTreeIterative", () => { + it("returns true for a symmetric tree", () => { + const root = node(1, node(2, node(3), node(4)), node(2, node(4), node(3))); + expect(isSymmetricTreeIterative(root)).toBe(true); + }); + + it("returns true for null root", () => { + expect(isSymmetricTreeIterative(null)).toBe(true); + }); + + it("returns true for a single node", () => { + expect(isSymmetricTreeIterative(node(1))).toBe(true); + }); + + it("returns false for an asymmetric tree", () => { + const root = node(1, node(2, null, node(3)), node(2, null, node(3))); + expect(isSymmetricTreeIterative(root)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/IsSymmetricTreeIterative.java b/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/IsSymmetricTreeIterative.java new file mode 100644 index 00000000..3cca4a63 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/IsSymmetricTreeIterative.java @@ -0,0 +1,36 @@ +// Is Symmetric Tree (Iterative) — queue-based: enqueue pairs and compare +import java.util.ArrayDeque; +import java.util.Queue; + +class SymmetricTreeIterativeNode { + int value; + SymmetricTreeIterativeNode left, right; + SymmetricTreeIterativeNode(int value) { this.value = value; } +} + +class IsSymmetricTreeIterative { + public boolean isSymmetricTreeIterative(SymmetricTreeIterativeNode root) { + if (root == null) return true; // @step:initialize + + Queue queue = new ArrayDeque<>(); // @step:initialize + queue.offer(root.left); // @step:initialize + queue.offer(root.right); // @step:initialize + + while (!queue.isEmpty()) { // @step:visit + SymmetricTreeIterativeNode leftNode = queue.poll(); // @step:visit + SymmetricTreeIterativeNode rightNode = queue.poll(); // @step:visit + + if (leftNode == null && rightNode == null) continue; // @step:check-balance + if (leftNode == null || rightNode == null) return false; // @step:check-balance + if (leftNode.value != rightNode.value) return false; // @step:check-balance + + // Enqueue outer pair and inner pair + queue.offer(leftNode.left); // @step:traverse-left + queue.offer(rightNode.right); // @step:traverse-left + queue.offer(leftNode.right); // @step:traverse-right + queue.offer(rightNode.left); // @step:traverse-right + } + + return true; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/is-symmetric-tree-iterative.py b/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/is-symmetric-tree-iterative.py new file mode 100644 index 00000000..c930248d --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/is-symmetric-tree-iterative.py @@ -0,0 +1,30 @@ +# Is Symmetric Tree (Iterative) — queue-based: enqueue pairs and compare +from collections import deque + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def is_symmetric_tree_iterative(root) -> bool: + if root is None: # @step:initialize + return True + + queue = deque([(root.left, root.right)]) # @step:initialize + + while queue: # @step:visit + left_node, right_node = queue.popleft() # @step:visit + + if left_node is None and right_node is None: # @step:check-balance + continue + if left_node is None or right_node is None: # @step:check-balance + return False + if left_node.value != right_node.value: # @step:check-balance + return False + + # Enqueue outer pair and inner pair + queue.append((left_node.left, right_node.right)) # @step:traverse-left + queue.append((left_node.right, right_node.left)) # @step:traverse-right + + return True # @step:complete diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/is-symmetric-tree-iterative.ts b/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/is-symmetric-tree-iterative.ts new file mode 100644 index 00000000..105b8e91 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/sources/is-symmetric-tree-iterative.ts @@ -0,0 +1,30 @@ +// Is Symmetric Tree (Iterative) — queue-based: enqueue pairs and compare + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function isSymmetricTreeIterative(root: TreeNode | null): boolean { + if (root === null) return true; // @step:initialize + + const queue: Array<[TreeNode | null, TreeNode | null]> = []; // @step:initialize + queue.push([root.left, root.right]); // @step:initialize + + while (queue.length > 0) { + // @step:visit + const pair = queue.shift()!; // @step:visit + const [leftNode, rightNode] = pair; // @step:visit + + if (leftNode === null && rightNode === null) continue; // @step:check-balance + if (leftNode === null || rightNode === null) return false; // @step:check-balance + if (leftNode.value !== rightNode.value) return false; // @step:check-balance + + // Enqueue outer pair and inner pair + queue.push([leftNode.left, rightNode.right]); // @step:traverse-left + queue.push([leftNode.right, rightNode.left]); // @step:traverse-right + } + + return true; // @step:complete +} diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/step-generator.test.ts b/src/algorithms/trees/properties/is-symmetric-tree-iterative/step-generator.test.ts new file mode 100644 index 00000000..89a12c09 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateIsSymmetricTreeIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateIsSymmetricTreeIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateIsSymmetricTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateIsSymmetricTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateIsSymmetricTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateIsSymmetricTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateIsSymmetricTreeIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/is-symmetric-tree-iterative/step-generator.ts b/src/algorithms/trees/properties/is-symmetric-tree-iterative/step-generator.ts new file mode 100644 index 00000000..0be2b455 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree-iterative/step-generator.ts @@ -0,0 +1,82 @@ +/** Step generator for Is Symmetric Tree (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.IS_SYMMETRIC_TREE_ITERATIVE!); + +export interface IsSymmetricTreeIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateIsSymmetricTreeIterativeSteps( + input: IsSymmetricTreeIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const rootNode = nodeMap.get(rootId); + if (!rootNode) { + tracker.recordResult(true, { result: true }); + tracker.complete({ result: true }); + return tracker.getSteps(); + } + + const queue: Array<[string | null, string | null]> = [ + [rootNode.leftChildId, rootNode.rightChildId], + ]; + let isSymmetric = true; + + while (queue.length > 0 && isSymmetric) { + const pair = queue.shift()!; + const [leftId, rightId] = pair; + + if (!leftId && !rightId) continue; + + if (!leftId || !rightId) { + isSymmetric = false; + break; + } + + const leftNode = nodeMap.get(leftId); + const rightNode = nodeMap.get(rightId); + if (!leftNode || !rightNode) { + isSymmetric = false; + break; + } + + tracker.checkNode(leftId, { + currentNode: leftId, + mirrorNode: rightId, + leftValue: leftNode.value, + rightValue: rightNode.value, + }); + + if (leftNode.value !== rightNode.value) { + tracker.markInvalid(leftId, { + currentNode: leftId, + mirrorNode: rightId, + leftValue: leftNode.value, + rightValue: rightNode.value, + }); + isSymmetric = false; + break; + } + + tracker.markValid(leftId, { currentNode: leftId, mirrorNode: rightId, value: leftNode.value }); + + queue.push([leftNode.leftChildId, rightNode.rightChildId]); + queue.push([leftNode.rightChildId, rightNode.leftChildId]); + } + + tracker.recordResult(isSymmetric, { result: isSymmetric }); + tracker.complete({ result: isSymmetric }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/is-symmetric-tree/IsSymmetricTreePipeline.stories.tsx b/src/algorithms/trees/properties/is-symmetric-tree/IsSymmetricTreePipeline.stories.tsx new file mode 100644 index 00000000..23e4cd1b --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/IsSymmetricTreePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Is Symmetric Tree. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateIsSymmetricTreeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateIsSymmetricTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Is Symmetric Tree", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/is-symmetric-tree/educational.ts b/src/algorithms/trees/properties/is-symmetric-tree/educational.ts new file mode 100644 index 00000000..adc10d2f --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/educational.ts @@ -0,0 +1,44 @@ +import type { EducationalContent } from "@/types"; + +export const isSymmetricTreeEducational: EducationalContent = { + overview: + "**Is Symmetric Tree** checks whether a binary tree is a mirror image of itself around its center. " + + "A symmetric tree has matching node values when read from the outside inward on both sides.", + + howItWorks: + "A helper `isMirror(left, right)` recursively checks pairs of nodes:\n\n" + + "1. **Both null** — symmetric by definition, return `true`.\n" + + "2. **One null** — asymmetric, return `false`.\n" + + "3. **Values differ** — return `false`.\n" + + "4. **Recurse:** outer pair `(left.left, right.right)` AND inner pair `(left.right, right.left)` must both be mirrors.\n\n" + + "Start by calling `isMirror(root.left, root.right)`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node pair is compared once.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case** is when the root's children have different values — returns `false` after checking just two nodes.\n\n" + + "**Worst case** is a symmetric tree — all `n` nodes must be compared.", + + realWorldUses: [ + "**Palindrome structures:** Symmetric trees model palindrome-like data arrangements.", + "**UI layout verification:** Checking that a hierarchical layout is symmetric around a center axis.", + "**Molecule symmetry:** Chemical graph symmetry checking in computational chemistry.", + ], + + strengthsAndLimitations: { + strengths: [ + "Elegant pair-based recursion that directly models the definition.", + "Short-circuits early on the first asymmetric pair.", + ], + limitations: [ + "Recursive version risks stack overflow on very deep trees.", + "Does not tell you where the asymmetry is, only whether it exists.", + ], + }, + + whenToUseIt: + "Use when checking if a tree is a mirror of itself. For iterative symmetry checking without recursion risk, " + + "use the queue-based variant.", +}; diff --git a/src/algorithms/trees/properties/is-symmetric-tree/index.ts b/src/algorithms/trees/properties/is-symmetric-tree/index.ts new file mode 100644 index 00000000..34419eb2 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { isSymmetricTree } from "./sources/is-symmetric-tree.ts?fn"; +import { generateIsSymmetricTreeSteps } from "./step-generator"; +import type { IsSymmetricTreeInput } from "./step-generator"; +import { isSymmetricTreeEducational } from "./educational"; + +import typescriptSource from "./sources/is-symmetric-tree.ts?raw"; +import pythonSource from "./sources/is-symmetric-tree.py?raw"; +import javaSource from "./sources/IsSymmetricTree.java?raw"; + +/** Symmetric 7-node BST: root=4, mirrors [2,6], [1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeIsSymmetricTree(input: IsSymmetricTreeInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return isSymmetricTree(toBSTNode(input.rootId)) as boolean; +} + +const isSymmetricTreeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.IS_SYMMETRIC_TREE!, + name: "Is Symmetric Tree", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Checks if a binary tree is a mirror of itself around its center. Recursively compares outer and inner pairs.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeIsSymmetricTree, + generateSteps: generateIsSymmetricTreeSteps, + educational: isSymmetricTreeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(isSymmetricTreeDefinition); diff --git a/src/algorithms/trees/properties/is-symmetric-tree/is-symmetric-tree.test.ts b/src/algorithms/trees/properties/is-symmetric-tree/is-symmetric-tree.test.ts new file mode 100644 index 00000000..3d226dca --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/is-symmetric-tree.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { isSymmetricTree } from "./sources/is-symmetric-tree.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("isSymmetricTree", () => { + it("returns false for a non-symmetric BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(isSymmetricTree(root)).toBe(false); + }); + + it("returns true for a symmetric tree", () => { + const root = node(1, node(2, node(3), node(4)), node(2, node(4), node(3))); + expect(isSymmetricTree(root)).toBe(true); + }); + + it("returns true for null root", () => { + expect(isSymmetricTree(null)).toBe(true); + }); + + it("returns true for a single node", () => { + expect(isSymmetricTree(node(1))).toBe(true); + }); + + it("returns false for an asymmetric tree", () => { + const root = node(1, node(2, null, node(3)), node(2, null, node(3))); + expect(isSymmetricTree(root)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/properties/is-symmetric-tree/sources/IsSymmetricTree.java b/src/algorithms/trees/properties/is-symmetric-tree/sources/IsSymmetricTree.java new file mode 100644 index 00000000..8e7b2646 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/sources/IsSymmetricTree.java @@ -0,0 +1,25 @@ +// Is Symmetric Tree — recursive: compare left.left with right.right and left.right with right.left + +class SymmetricTreeNode { + int value; + SymmetricTreeNode left, right; + SymmetricTreeNode(int value) { this.value = value; } +} + +class IsSymmetricTree { + public boolean isSymmetricTree(SymmetricTreeNode root) { + if (root == null) return true; // @step:initialize + return isMirror(root.left, root.right); // @step:complete + } + + private boolean isMirror(SymmetricTreeNode leftNode, SymmetricTreeNode rightNode) { + if (leftNode == null && rightNode == null) return true; // @step:check-balance + if (leftNode == null || rightNode == null) return false; // @step:check-balance + if (leftNode.value != rightNode.value) return false; // @step:check-balance + + // Outer pair and inner pair must both be mirrors + boolean outerMatch = isMirror(leftNode.left, rightNode.right); // @step:traverse-left + boolean innerMatch = isMirror(leftNode.right, rightNode.left); // @step:traverse-right + return outerMatch && innerMatch; // @step:check-balance + } +} diff --git a/src/algorithms/trees/properties/is-symmetric-tree/sources/is-symmetric-tree.py b/src/algorithms/trees/properties/is-symmetric-tree/sources/is-symmetric-tree.py new file mode 100644 index 00000000..d7e9acbd --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/sources/is-symmetric-tree.py @@ -0,0 +1,26 @@ +# Is Symmetric Tree — recursive: compare left.left with right.right and left.right with right.left + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def is_symmetric_tree(root) -> bool: + if root is None: # @step:initialize + return True + + def is_mirror(left_node, right_node) -> bool: + if left_node is None and right_node is None: # @step:check-balance + return True + if left_node is None or right_node is None: # @step:check-balance + return False + if left_node.value != right_node.value: # @step:check-balance + return False + + # Outer pair and inner pair must both be mirrors + outer_match = is_mirror(left_node.left, right_node.right) # @step:traverse-left + inner_match = is_mirror(left_node.right, right_node.left) # @step:traverse-right + return outer_match and inner_match # @step:check-balance + + return is_mirror(root.left, root.right) # @step:complete diff --git a/src/algorithms/trees/properties/is-symmetric-tree/sources/is-symmetric-tree.ts b/src/algorithms/trees/properties/is-symmetric-tree/sources/is-symmetric-tree.ts new file mode 100644 index 00000000..1400a5a2 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/sources/is-symmetric-tree.ts @@ -0,0 +1,24 @@ +// Is Symmetric Tree — recursive: compare left.left with right.right and left.right with right.left + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function isSymmetricTree(root: TreeNode | null): boolean { + if (root === null) return true; // @step:initialize + + function isMirror(leftNode: TreeNode | null, rightNode: TreeNode | null): boolean { + if (leftNode === null && rightNode === null) return true; // @step:check-balance + if (leftNode === null || rightNode === null) return false; // @step:check-balance + if (leftNode.value !== rightNode.value) return false; // @step:check-balance + + // Outer pair and inner pair must both be mirrors + const outerMatch = isMirror(leftNode.left, rightNode.right); // @step:traverse-left + const innerMatch = isMirror(leftNode.right, rightNode.left); // @step:traverse-right + return outerMatch && innerMatch; // @step:check-balance + } + + return isMirror(root.left, root.right); // @step:complete +} diff --git a/src/algorithms/trees/properties/is-symmetric-tree/step-generator.test.ts b/src/algorithms/trees/properties/is-symmetric-tree/step-generator.test.ts new file mode 100644 index 00000000..c48355d7 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateIsSymmetricTreeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateIsSymmetricTreeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateIsSymmetricTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateIsSymmetricTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateIsSymmetricTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateIsSymmetricTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateIsSymmetricTreeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/is-symmetric-tree/step-generator.ts b/src/algorithms/trees/properties/is-symmetric-tree/step-generator.ts new file mode 100644 index 00000000..16389021 --- /dev/null +++ b/src/algorithms/trees/properties/is-symmetric-tree/step-generator.ts @@ -0,0 +1,64 @@ +/** Step generator for Is Symmetric Tree — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.IS_SYMMETRIC_TREE!); + +export interface IsSymmetricTreeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateIsSymmetricTreeSteps(input: IsSymmetricTreeInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + function isMirror(leftId: string | null, rightId: string | null): boolean { + if (!leftId && !rightId) return true; + if (!leftId || !rightId) { + tracker.recordResult(false, { leftId, rightId, reason: "one null" }); + return false; + } + + const leftNode = nodeMap.get(leftId); + const rightNode = nodeMap.get(rightId); + if (!leftNode || !rightNode) return false; + + tracker.checkNode(leftId, { + currentNode: leftId, + mirrorNode: rightId, + leftValue: leftNode.value, + rightValue: rightNode.value, + }); + + if (leftNode.value !== rightNode.value) { + tracker.markInvalid(leftId, { + currentNode: leftId, + mirrorNode: rightId, + leftValue: leftNode.value, + rightValue: rightNode.value, + }); + return false; + } + + tracker.markValid(leftId, { currentNode: leftId, mirrorNode: rightId, value: leftNode.value }); + + const outerMatch = isMirror(leftNode.leftChildId, rightNode.rightChildId); + if (!outerMatch) return false; + return isMirror(leftNode.rightChildId, rightNode.leftChildId); + } + + const rootNode = nodeMap.get(rootId); + const result = !rootNode ? true : isMirror(rootNode.leftChildId, rootNode.rightChildId); + + tracker.recordResult(result, { result }); + tracker.complete({ result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/MaximumDepthIterativePipeline.stories.tsx b/src/algorithms/trees/properties/maximum-depth-iterative/MaximumDepthIterativePipeline.stories.tsx new file mode 100644 index 00000000..7614b1fc --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/MaximumDepthIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Maximum Depth Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMaximumDepthIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMaximumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Maximum Depth Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/educational.ts b/src/algorithms/trees/properties/maximum-depth-iterative/educational.ts new file mode 100644 index 00000000..35bcc5da --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/educational.ts @@ -0,0 +1,46 @@ +import type { EducationalContent } from "@/types"; + +export const maximumDepthIterativeEducational: EducationalContent = { + overview: + "**Maximum Depth (Iterative)** computes tree height using BFS (level-order traversal). " + + "Each time all nodes at a given level are processed, the depth counter increments. " + + "The final depth count equals the maximum depth of the tree.", + + howItWorks: + "The algorithm processes the tree level by level:\n\n" + + "1. Enqueue the root.\n" + + "2. For each level, snapshot the current queue size — that is how many nodes are at this level.\n" + + "3. Process exactly that many nodes, enqueueing their children.\n" + + "4. Increment depth by 1 after processing each complete level.\n\n" + + "When the queue empties, depth holds the maximum tree depth.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — every node is visited once.\n\n" + + "**Space Complexity: `O(w)`** where `w` is the maximum width of the tree. " + + "For a balanced tree this is `O(n/2)` at the leaf level. Better than the recursive version for skewed trees.", + + bestAndWorstCase: + "**Best case** is a single-node tree — one level processed immediately.\n\n" + + "**Worst case** is a wide balanced tree — the queue holds up to `n/2` nodes simultaneously at the leaf level.", + + realWorldUses: [ + "**Avoiding recursion limits:** Preferred over the recursive version in Python (default recursion limit 1000) or environments with tight stack constraints.", + "**Parallel level processing:** BFS naturally groups nodes by level, useful when level-based operations are needed.", + "**Memory-safe traversal:** Stack overflow is impossible since only a queue is used.", + ], + + strengthsAndLimitations: { + strengths: [ + "No risk of stack overflow — uses an explicit queue instead of the call stack.", + "Naturally produces level-by-level processing, useful for multi-purpose BFS passes.", + ], + limitations: [ + "Uses O(w) queue memory which can be O(n) for wide balanced trees.", + "More verbose than the elegant recursive version.", + ], + }, + + whenToUseIt: + "Use the iterative BFS variant in environments with recursion limits or when processing very deep trees. " + + "If level-order metadata (level number, nodes per level) is needed alongside depth, BFS is the natural choice.", +}; diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/index.ts b/src/algorithms/trees/properties/maximum-depth-iterative/index.ts new file mode 100644 index 00000000..eab2cdc2 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { maximumDepthIterative } from "./sources/maximum-depth-iterative.ts?fn"; +import { generateMaximumDepthIterativeSteps } from "./step-generator"; +import type { MaximumDepthIterativeInput } from "./step-generator"; +import { maximumDepthIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/maximum-depth-iterative.ts?raw"; +import pythonSource from "./sources/maximum-depth-iterative.py?raw"; +import javaSource from "./sources/MaximumDepthIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeMaximumDepthIterative(input: MaximumDepthIterativeInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return maximumDepthIterative(toBSTNode(input.rootId)) as number; +} + +const maximumDepthIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MAXIMUM_DEPTH_ITERATIVE!, + name: "Maximum Depth (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Computes maximum depth using BFS level-by-level counting — each completed level increments depth", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeMaximumDepthIterative, + generateSteps: generateMaximumDepthIterativeSteps, + educational: maximumDepthIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(maximumDepthIterativeDefinition); diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/maximum-depth-iterative.test.ts b/src/algorithms/trees/properties/maximum-depth-iterative/maximum-depth-iterative.test.ts new file mode 100644 index 00000000..91c0e989 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/maximum-depth-iterative.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { maximumDepthIterative } from "./sources/maximum-depth-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("maximumDepthIterative", () => { + it("returns 3 for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(maximumDepthIterative(root)).toBe(3); + }); + + it("returns 0 for a null root", () => { + expect(maximumDepthIterative(null)).toBe(0); + }); + + it("returns 1 for a single node", () => { + expect(maximumDepthIterative(node(42))).toBe(1); + }); + + it("returns correct depth for a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(maximumDepthIterative(root)).toBe(5); + }); + + it("returns 2 for a two-level tree", () => { + expect(maximumDepthIterative(node(1, node(2)))).toBe(2); + }); +}); diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/sources/MaximumDepthIterative.java b/src/algorithms/trees/properties/maximum-depth-iterative/sources/MaximumDepthIterative.java new file mode 100644 index 00000000..3fc444e3 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/sources/MaximumDepthIterative.java @@ -0,0 +1,33 @@ +// Maximum Depth of Binary Tree — BFS level counting with a queue +import java.util.LinkedList; +import java.util.Queue; + +class MaximumDepthIterativeNode { + int value; + MaximumDepthIterativeNode left, right; + MaximumDepthIterativeNode(int value) { this.value = value; } +} + +class MaximumDepthIterative { + public int maximumDepthIterative(MaximumDepthIterativeNode root) { + if (root == null) return 0; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(root); // @step:initialize + int depth = 0; // @step:initialize + + while (!queue.isEmpty()) { // @step:visit + int levelSize = queue.size(); // @step:visit + depth++; // @step:update-height + + // Process all nodes at the current level + for (int nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { // @step:visit + MaximumDepthIterativeNode current = queue.poll(); // @step:visit + if (current.left != null) queue.offer(current.left); // @step:traverse-left + if (current.right != null) queue.offer(current.right); // @step:traverse-right + } + } + + return depth; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/sources/maximum-depth-iterative.py b/src/algorithms/trees/properties/maximum-depth-iterative/sources/maximum-depth-iterative.py new file mode 100644 index 00000000..733f6c9a --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/sources/maximum-depth-iterative.py @@ -0,0 +1,29 @@ +# Maximum Depth of Binary Tree — BFS level counting with a queue +from collections import deque + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def maximum_depth_iterative(root) -> int: + if root is None: # @step:initialize + return 0 + + queue = deque([root]) # @step:initialize + depth = 0 # @step:initialize + + while queue: # @step:visit + level_size = len(queue) # @step:visit + depth += 1 # @step:update-height + + # Process all nodes at the current level + for _ in range(level_size): # @step:visit + current = queue.popleft() # @step:visit + if current.left is not None: # @step:traverse-left + queue.append(current.left) # @step:traverse-left + if current.right is not None: # @step:traverse-right + queue.append(current.right) # @step:traverse-right + + return depth # @step:complete diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/sources/maximum-depth-iterative.ts b/src/algorithms/trees/properties/maximum-depth-iterative/sources/maximum-depth-iterative.ts new file mode 100644 index 00000000..177315e0 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/sources/maximum-depth-iterative.ts @@ -0,0 +1,30 @@ +// Maximum Depth of Binary Tree — BFS level counting with a queue + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function maximumDepthIterative(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + const queue: TreeNode[] = [root]; // @step:initialize + let depth = 0; // @step:initialize + + while (queue.length > 0) { + // @step:visit + const levelSize = queue.length; // @step:visit + depth += 1; // @step:update-height + + // Process all nodes at the current level + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + // @step:visit + const current = queue.shift()!; // @step:visit + if (current.left !== null) queue.push(current.left); // @step:traverse-left + if (current.right !== null) queue.push(current.right); // @step:traverse-right + } + } + + return depth; // @step:complete +} diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/step-generator.test.ts b/src/algorithms/trees/properties/maximum-depth-iterative/step-generator.test.ts new file mode 100644 index 00000000..f4d96efc --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMaximumDepthIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMaximumDepthIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateMaximumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMaximumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMaximumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMaximumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMaximumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/maximum-depth-iterative/step-generator.ts b/src/algorithms/trees/properties/maximum-depth-iterative/step-generator.ts new file mode 100644 index 00000000..39e319c6 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth-iterative/step-generator.ts @@ -0,0 +1,49 @@ +/** Step generator for Maximum Depth (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MAXIMUM_DEPTH_ITERATIVE!); + +export interface MaximumDepthIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateMaximumDepthIterativeSteps( + input: MaximumDepthIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const queue: string[] = [rootId]; + let depth = 0; + + while (queue.length > 0) { + const levelSize = queue.length; + depth += 1; + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + const nodeId = queue.shift()!; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, depth }); + + if (node.leftChildId) queue.push(node.leftChildId); + if (node.rightChildId) queue.push(node.rightChildId); + } + + tracker.updateDepth(rootId, depth, { depth, levelSize }); + } + + tracker.recordResult(depth, { result: depth }); + tracker.complete({ result: depth }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/maximum-depth/MaximumDepthPipeline.stories.tsx b/src/algorithms/trees/properties/maximum-depth/MaximumDepthPipeline.stories.tsx new file mode 100644 index 00000000..3722b6ec --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/MaximumDepthPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Maximum Depth. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMaximumDepthSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Maximum Depth", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/maximum-depth/educational.ts b/src/algorithms/trees/properties/maximum-depth/educational.ts new file mode 100644 index 00000000..5d06e996 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/educational.ts @@ -0,0 +1,48 @@ +import type { EducationalContent } from "@/types"; + +export const maximumDepthEducational: EducationalContent = { + overview: + "**Maximum Depth** (also called tree height) is the length of the longest path from the root to any leaf node. " + + "For a balanced binary tree with `n` nodes, the depth is `O(log n)`. For a degenerate tree, it reaches `O(n)`.", + + howItWorks: + "The algorithm uses recursive DFS. At each node it:\n\n" + + "1. **Recurses left** — compute depth of the left subtree.\n" + + "2. **Recurses right** — compute depth of the right subtree.\n" + + "3. **Returns** `max(leftDepth, rightDepth) + 1` to account for the current node.\n\n" + + "The base case returns `0` for a `null` node. The recursion unwinds naturally, passing heights up the call stack.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)`** — the call stack depth equals the tree height `h`. " + + "For balanced trees `h = O(log n)`, for skewed trees `h = O(n)`.", + + bestAndWorstCase: + "**Best case** is a balanced tree — the call stack stays at `O(log n)` depth.\n\n" + + "**Worst case** is a degenerate (fully skewed) tree — the recursion depth reaches `O(n)`, " + + "which may cause a stack overflow for extremely large inputs.", + + realWorldUses: [ + "**Validating AVL / Red-Black trees:** Maximum depth is compared between subtrees to enforce balance invariants.", + "**Network routing trees:** Tree depth affects worst-case lookup time in hierarchical routing structures.", + "**Compiler parse trees:** AST depth limits are used to detect deeply nested expressions or recursion bombs.", + "**File system traversal:** Directory depth checking to prevent infinite recursive descent.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple recursive implementation that directly mirrors the definition.", + "O(log n) stack depth for balanced trees keeps memory usage minimal.", + "No auxiliary data structures needed.", + ], + limitations: [ + "Recursive implementation risks stack overflow on degenerate trees with very large n.", + "Visits every node even when early termination could help in some specialized depth-bounded searches.", + ], + }, + + whenToUseIt: + "Use maximum depth when you need to measure tree height for balance validation, " + + "space allocation, or complexity analysis. For large trees where stack overflow is a concern, " + + "use the iterative BFS variant instead.", +}; diff --git a/src/algorithms/trees/properties/maximum-depth/index.ts b/src/algorithms/trees/properties/maximum-depth/index.ts new file mode 100644 index 00000000..aa4d35fd --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { maximumDepth } from "./sources/maximum-depth.ts?fn"; +import { generateMaximumDepthSteps } from "./step-generator"; +import type { MaximumDepthInput } from "./step-generator"; +import { maximumDepthEducational } from "./educational"; + +import typescriptSource from "./sources/maximum-depth.ts?raw"; +import pythonSource from "./sources/maximum-depth.py?raw"; +import javaSource from "./sources/MaximumDepth.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeMaximumDepth(input: MaximumDepthInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return maximumDepth(toBSTNode(input.rootId)) as number; +} + +const maximumDepthDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MAXIMUM_DEPTH!, + name: "Maximum Depth", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Recursively computes the maximum depth (height) of a binary tree by returning max(leftDepth, rightDepth) + 1 at each node", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeMaximumDepth, + generateSteps: generateMaximumDepthSteps, + educational: maximumDepthEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(maximumDepthDefinition); diff --git a/src/algorithms/trees/properties/maximum-depth/maximum-depth.test.ts b/src/algorithms/trees/properties/maximum-depth/maximum-depth.test.ts new file mode 100644 index 00000000..afff3dbe --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/maximum-depth.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { maximumDepth } from "./sources/maximum-depth.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("maximumDepth", () => { + it("returns 3 for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(maximumDepth(root)).toBe(3); + }); + + it("returns 0 for a null root", () => { + expect(maximumDepth(null)).toBe(0); + }); + + it("returns 1 for a single node", () => { + expect(maximumDepth(node(42))).toBe(1); + }); + + it("returns correct depth for a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(maximumDepth(root)).toBe(5); + }); + + it("returns correct depth for a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3))); + expect(maximumDepth(root)).toBe(3); + }); + + it("returns 2 for a two-level tree", () => { + expect(maximumDepth(node(1, node(2)))).toBe(2); + }); +}); diff --git a/src/algorithms/trees/properties/maximum-depth/sources/MaximumDepth.java b/src/algorithms/trees/properties/maximum-depth/sources/MaximumDepth.java new file mode 100644 index 00000000..905be468 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/sources/MaximumDepth.java @@ -0,0 +1,20 @@ +// Maximum Depth of Binary Tree — recursive DFS returning max(left, right) + 1 + +class MaximumDepthNode { + int value; + MaximumDepthNode left, right; + MaximumDepthNode(int value) { this.value = value; } +} + +class MaximumDepth { + public int maximumDepth(MaximumDepthNode root) { + if (root == null) return 0; // @step:initialize + + // Recursively compute depth of left and right subtrees + int leftDepth = maximumDepth(root.left); // @step:traverse-left + int rightDepth = maximumDepth(root.right); // @step:traverse-right + + // Return the larger subtree depth plus 1 for the current node + return Math.max(leftDepth, rightDepth) + 1; // @step:update-height + } +} diff --git a/src/algorithms/trees/properties/maximum-depth/sources/maximum-depth.py b/src/algorithms/trees/properties/maximum-depth/sources/maximum-depth.py new file mode 100644 index 00000000..f1bc3268 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/sources/maximum-depth.py @@ -0,0 +1,18 @@ +# Maximum Depth of Binary Tree — recursive DFS returning max(left, right) + 1 + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def maximum_depth(root) -> int: + if root is None: # @step:initialize + return 0 + + # Recursively compute depth of left and right subtrees + left_depth = maximum_depth(root.left) # @step:traverse-left + right_depth = maximum_depth(root.right) # @step:traverse-right + + # Return the larger subtree depth plus 1 for the current node + return max(left_depth, right_depth) + 1 # @step:update-height diff --git a/src/algorithms/trees/properties/maximum-depth/sources/maximum-depth.ts b/src/algorithms/trees/properties/maximum-depth/sources/maximum-depth.ts new file mode 100644 index 00000000..924f7c19 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/sources/maximum-depth.ts @@ -0,0 +1,18 @@ +// Maximum Depth of Binary Tree — recursive DFS returning max(left, right) + 1 + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function maximumDepth(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + // Recursively compute depth of left and right subtrees + const leftDepth = maximumDepth(root.left); // @step:traverse-left + const rightDepth = maximumDepth(root.right); // @step:traverse-right + + // Return the larger subtree depth plus 1 for the current node + return Math.max(leftDepth, rightDepth) + 1; // @step:update-height +} diff --git a/src/algorithms/trees/properties/maximum-depth/step-generator.test.ts b/src/algorithms/trees/properties/maximum-depth/step-generator.test.ts new file mode 100644 index 00000000..1ec50a12 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMaximumDepthSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMaximumDepthSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("records result of 3 for a balanced 3-level tree", () => { + const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["result"]).toBe(3); + }); + + it("has incrementing step indices", () => { + const steps = generateMaximumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/maximum-depth/step-generator.ts b/src/algorithms/trees/properties/maximum-depth/step-generator.ts new file mode 100644 index 00000000..825e26eb --- /dev/null +++ b/src/algorithms/trees/properties/maximum-depth/step-generator.ts @@ -0,0 +1,42 @@ +/** Step generator for Maximum Depth — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MAXIMUM_DEPTH!); + +export interface MaximumDepthInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateMaximumDepthSteps(input: MaximumDepthInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + function computeDepth(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + const leftDepth = computeDepth(node.leftChildId); + const rightDepth = computeDepth(node.rightChildId); + const depth = Math.max(leftDepth, rightDepth) + 1; + + tracker.updateHeight(nodeId, depth, { currentNode: nodeId, depth }); + return depth; + } + + const result = computeDepth(rootId); + tracker.recordResult(result, { result }); + tracker.complete({ result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/maximum-path-sum/MaximumPathSumPipeline.stories.tsx b/src/algorithms/trees/properties/maximum-path-sum/MaximumPathSumPipeline.stories.tsx new file mode 100644 index 00000000..08daef0c --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/MaximumPathSumPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Maximum Path Sum. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMaximumPathSumSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMaximumPathSumSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Maximum Path Sum", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/maximum-path-sum/educational.ts b/src/algorithms/trees/properties/maximum-path-sum/educational.ts new file mode 100644 index 00000000..64e256f3 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/educational.ts @@ -0,0 +1,45 @@ +import type { EducationalContent } from "@/types"; + +export const maximumPathSumEducational: EducationalContent = { + overview: + "**Maximum Path Sum** finds the maximum sum of any path between two nodes in the tree. " + + "A path can start and end at any nodes — it does not have to pass through the root or touch a leaf. " + + "Paths must follow parent-child edges.", + + howItWorks: + "A recursive helper `maxGain(node)` returns the maximum contribution this node can make to its parent:\n\n" + + "1. Compute `leftGain = max(maxGain(left), 0)` — only use if positive.\n" + + "2. Compute `rightGain = max(maxGain(right), 0)` — only use if positive.\n" + + "3. Compute `pathThroughNode = node.value + leftGain + rightGain`.\n" + + "4. Update the global maximum with `pathThroughNode`.\n" + + "5. **Return** `node.value + max(leftGain, rightGain)` — the node can only contribute one branch to its parent.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node is visited once.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case** is all positive values — the maximum path spans the entire tree.\n\n" + + "**Worst case** is all negative values — the maximum path is a single node (the least negative).", + + realWorldUses: [ + "**Maximum profit route:** Finding the most profitable path through a tree of financial decisions.", + "**Network bandwidth:** Maximum throughput through any tree path in a hierarchical network.", + "**Gene expression scoring:** Finding the maximum-scoring path in a phylogenetic scoring tree.", + ], + + strengthsAndLimitations: { + strengths: [ + "Handles negative values correctly by using max(gain, 0).", + "O(n) single-pass solution with a global variable for the running maximum.", + ], + limitations: [ + "Does not return the actual path — only the sum.", + "The global state makes it slightly harder to reason about pure function semantics.", + ], + }, + + whenToUseIt: + "Use maximum path sum in competitive programming and interview problems asking for maximum inter-node paths. " + + "The key insight — clamping gains to zero — also applies to related maximum subarray problems.", +}; diff --git a/src/algorithms/trees/properties/maximum-path-sum/index.ts b/src/algorithms/trees/properties/maximum-path-sum/index.ts new file mode 100644 index 00000000..eaf41a17 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { maximumPathSum } from "./sources/maximum-path-sum.ts?fn"; +import { generateMaximumPathSumSteps } from "./step-generator"; +import type { MaximumPathSumInput } from "./step-generator"; +import { maximumPathSumEducational } from "./educational"; + +import typescriptSource from "./sources/maximum-path-sum.ts?raw"; +import pythonSource from "./sources/maximum-path-sum.py?raw"; +import javaSource from "./sources/MaximumPathSum.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7]. Max path is 3+2+4+6+7=22. */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeMaximumPathSum(input: MaximumPathSumInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return maximumPathSum(toBSTNode(input.rootId)) as number; +} + +const maximumPathSumDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MAXIMUM_PATH_SUM!, + name: "Maximum Path Sum", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Finds the maximum sum path between any two nodes. At each node it computes leftGain + node + rightGain and tracks the global maximum.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeMaximumPathSum, + generateSteps: generateMaximumPathSumSteps, + educational: maximumPathSumEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(maximumPathSumDefinition); diff --git a/src/algorithms/trees/properties/maximum-path-sum/maximum-path-sum.test.ts b/src/algorithms/trees/properties/maximum-path-sum/maximum-path-sum.test.ts new file mode 100644 index 00000000..c6c360e4 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/maximum-path-sum.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { maximumPathSum } from "./sources/maximum-path-sum.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("maximumPathSum", () => { + it("returns correct max path sum for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + // best path: 3+2+4+6+7 = 22 + expect(maximumPathSum(root)).toBe(22); + }); + + it("handles a single node", () => { + expect(maximumPathSum(node(-3))).toBe(-3); + }); + + it("handles all negative values", () => { + const root = node(-1, node(-2), node(-3)); + expect(maximumPathSum(root)).toBe(-1); + }); + + it("returns null for null root", () => { + expect(maximumPathSum(null)).toBe(-Infinity); + }); +}); diff --git a/src/algorithms/trees/properties/maximum-path-sum/sources/MaximumPathSum.java b/src/algorithms/trees/properties/maximum-path-sum/sources/MaximumPathSum.java new file mode 100644 index 00000000..7a0ded1e --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/sources/MaximumPathSum.java @@ -0,0 +1,32 @@ +// Maximum Path Sum — recursive: at each node compute max path through it, track global max + +class MaxPathSumNode { + int value; + MaxPathSumNode left, right; + MaxPathSumNode(int value) { this.value = value; } +} + +class MaximumPathSum { + private int globalMax; // @step:initialize + + public int maximumPathSum(MaxPathSumNode root) { + globalMax = root != null ? root.value : Integer.MIN_VALUE; // @step:initialize + maxGain(root); // @step:initialize + return globalMax; // @step:complete + } + + private int maxGain(MaxPathSumNode node) { + if (node == null) return 0; // @step:initialize + + // Only include subtree if it contributes positively + int leftGain = Math.max(maxGain(node.left), 0); // @step:traverse-left + int rightGain = Math.max(maxGain(node.right), 0); // @step:traverse-right + + // Path through this node + int pathThroughNode = node.value + leftGain + rightGain; // @step:compute-value + globalMax = Math.max(globalMax, pathThroughNode); // @step:update-height + + // Return max gain if we continue from this node to parent + return node.value + Math.max(leftGain, rightGain); // @step:add-to-result + } +} diff --git a/src/algorithms/trees/properties/maximum-path-sum/sources/maximum-path-sum.py b/src/algorithms/trees/properties/maximum-path-sum/sources/maximum-path-sum.py new file mode 100644 index 00000000..ef53d4f8 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/sources/maximum-path-sum.py @@ -0,0 +1,28 @@ +# Maximum Path Sum — recursive: at each node compute max path through it, track global max + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def maximum_path_sum(root) -> int: + global_max = [root.value if root is not None else float("-inf")] # @step:initialize + + def max_gain(node) -> int: + if node is None: # @step:initialize + return 0 + + # Only include subtree if it contributes positively + left_gain = max(max_gain(node.left), 0) # @step:traverse-left + right_gain = max(max_gain(node.right), 0) # @step:traverse-right + + # Path through this node + path_through_node = node.value + left_gain + right_gain # @step:compute-value + global_max[0] = max(global_max[0], path_through_node) # @step:update-height + + # Return max gain if we continue from this node to parent + return node.value + max(left_gain, right_gain) # @step:add-to-result + + max_gain(root) # @step:initialize + return global_max[0] # @step:complete diff --git a/src/algorithms/trees/properties/maximum-path-sum/sources/maximum-path-sum.ts b/src/algorithms/trees/properties/maximum-path-sum/sources/maximum-path-sum.ts new file mode 100644 index 00000000..9bf07d33 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/sources/maximum-path-sum.ts @@ -0,0 +1,29 @@ +// Maximum Path Sum — recursive: at each node compute max path through it, track global max + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function maximumPathSum(root: TreeNode | null): number { + let globalMax = root !== null ? root.value : -Infinity; // @step:initialize + + function maxGain(node: TreeNode | null): number { + if (node === null) return 0; // @step:initialize + + // Only include subtree if it contributes positively + const leftGain = Math.max(maxGain(node.left), 0); // @step:traverse-left + const rightGain = Math.max(maxGain(node.right), 0); // @step:traverse-right + + // Path through this node: left branch + node value + right branch + const pathThroughNode = node.value + leftGain + rightGain; // @step:compute-value + globalMax = Math.max(globalMax, pathThroughNode); // @step:update-height + + // Return max gain if we continue from this node to parent + return node.value + Math.max(leftGain, rightGain); // @step:add-to-result + } + + maxGain(root); // @step:initialize + return globalMax; // @step:complete +} diff --git a/src/algorithms/trees/properties/maximum-path-sum/step-generator.test.ts b/src/algorithms/trees/properties/maximum-path-sum/step-generator.test.ts new file mode 100644 index 00000000..c39ab0a1 --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMaximumPathSumSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMaximumPathSumSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateMaximumPathSumSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMaximumPathSumSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMaximumPathSumSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMaximumPathSumSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMaximumPathSumSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/maximum-path-sum/step-generator.ts b/src/algorithms/trees/properties/maximum-path-sum/step-generator.ts new file mode 100644 index 00000000..8279a41f --- /dev/null +++ b/src/algorithms/trees/properties/maximum-path-sum/step-generator.ts @@ -0,0 +1,55 @@ +/** Step generator for Maximum Path Sum — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MAXIMUM_PATH_SUM!); + +export interface MaximumPathSumInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateMaximumPathSumSteps(input: MaximumPathSumInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const rootNode = nodeMap.get(rootId); + let globalMax = rootNode ? rootNode.value : -Infinity; + + function maxGain(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + const leftGain = Math.max(maxGain(node.leftChildId), 0); + const rightGain = Math.max(maxGain(node.rightChildId), 0); + + const pathThroughNode = node.value + leftGain + rightGain; + if (pathThroughNode > globalMax) { + globalMax = pathThroughNode; + } + + tracker.updateHeight(nodeId, globalMax, { + currentNode: nodeId, + leftGain, + rightGain, + pathThroughNode, + globalMax, + }); + return node.value + Math.max(leftGain, rightGain); + } + + maxGain(rootId); + tracker.recordResult(globalMax, { result: globalMax }); + tracker.complete({ result: globalMax }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/MinimumDepthIterativePipeline.stories.tsx b/src/algorithms/trees/properties/minimum-depth-iterative/MinimumDepthIterativePipeline.stories.tsx new file mode 100644 index 00000000..30758940 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/MinimumDepthIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Minimum Depth Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMinimumDepthIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMinimumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Minimum Depth Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/educational.ts b/src/algorithms/trees/properties/minimum-depth-iterative/educational.ts new file mode 100644 index 00000000..8227d49a --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/educational.ts @@ -0,0 +1,45 @@ +import type { EducationalContent } from "@/types"; + +export const minimumDepthIterativeEducational: EducationalContent = { + overview: + "**Minimum Depth (Iterative)** uses BFS to find the depth at which the first leaf node appears. " + + "Since BFS explores level by level, the very first leaf encountered is guaranteed to be at minimum depth — " + + "enabling an early-exit optimization that the recursive version cannot exploit.", + + howItWorks: + "1. Enqueue the root at depth 1.\n" + + "2. Process each node from the queue:\n" + + " - If it is a leaf (no children), **immediately return the current depth**.\n" + + " - Otherwise, enqueue its children at depth + 1.\n\n" + + "The early return makes this approach significantly faster than the recursive version when the minimum depth leaf is shallow.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** worst case, but often much less in practice due to early termination.\n\n" + + "**Space Complexity: `O(w)`** where `w` is the maximum BFS queue width.", + + bestAndWorstCase: + "**Best case** is when the root has a leaf child — returns after processing level 1, `O(1)` effectively.\n\n" + + "**Worst case** is a degenerate right-skewed tree where the only leaf is at depth `n` — " + + "all nodes must be visited, making it `O(n)`.", + + realWorldUses: [ + "**Short-circuit tree analysis:** When the target is to find any matching leaf as quickly as possible.", + "**Fewest-steps planning:** BFS naturally finds the shallowest goal in unweighted trees.", + "**Level-sensitive processing:** The BFS loop naturally groups processing by depth level.", + ], + + strengthsAndLimitations: { + strengths: [ + "Terminates as soon as the first leaf is found — often much faster than visiting all nodes.", + "No recursion stack overflow risk.", + ], + limitations: [ + "Uses queue memory proportional to tree width.", + "More verbose implementation than the recursive version.", + ], + }, + + whenToUseIt: + "Prefer the BFS iterative version whenever minimum depth must be computed on large or potentially skewed trees. " + + "The early-exit property makes it faster in average cases.", +}; diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/index.ts b/src/algorithms/trees/properties/minimum-depth-iterative/index.ts new file mode 100644 index 00000000..4635095d --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { minimumDepthIterative } from "./sources/minimum-depth-iterative.ts?fn"; +import { generateMinimumDepthIterativeSteps } from "./step-generator"; +import type { MinimumDepthIterativeInput } from "./step-generator"; +import { minimumDepthIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/minimum-depth-iterative.ts?raw"; +import pythonSource from "./sources/minimum-depth-iterative.py?raw"; +import javaSource from "./sources/MinimumDepthIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeMinimumDepthIterative(input: MinimumDepthIterativeInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return minimumDepthIterative(toBSTNode(input.rootId)) as number; +} + +const minimumDepthIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MINIMUM_DEPTH_ITERATIVE!, + name: "Minimum Depth (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "BFS-based minimum depth — the first leaf node encountered during level-order traversal is at minimum depth", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeMinimumDepthIterative, + generateSteps: generateMinimumDepthIterativeSteps, + educational: minimumDepthIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(minimumDepthIterativeDefinition); diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/minimum-depth-iterative.test.ts b/src/algorithms/trees/properties/minimum-depth-iterative/minimum-depth-iterative.test.ts new file mode 100644 index 00000000..37b92a69 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/minimum-depth-iterative.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { minimumDepthIterative } from "./sources/minimum-depth-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("minimumDepthIterative", () => { + it("returns 3 for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(minimumDepthIterative(root)).toBe(3); + }); + + it("returns 0 for a null root", () => { + expect(minimumDepthIterative(null)).toBe(0); + }); + + it("returns 1 for a single node", () => { + expect(minimumDepthIterative(node(42))).toBe(1); + }); + + it("returns 2 for a partial tree with leaf at level 2", () => { + expect(minimumDepthIterative(node(1, node(2)))).toBe(2); + }); +}); diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/sources/MinimumDepthIterative.java b/src/algorithms/trees/properties/minimum-depth-iterative/sources/MinimumDepthIterative.java new file mode 100644 index 00000000..c6e98a5b --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/sources/MinimumDepthIterative.java @@ -0,0 +1,38 @@ +// Minimum Depth of Binary Tree — BFS returns depth at first leaf encountered +import java.util.LinkedList; +import java.util.Queue; + +class MinimumDepthIterativeNode { + int value; + MinimumDepthIterativeNode left, right; + MinimumDepthIterativeNode(int value) { this.value = value; } +} + +class MinimumDepthIterative { + public int minimumDepthIterative(MinimumDepthIterativeNode root) { + if (root == null) return 0; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(root); // @step:initialize + int depth = 0; // @step:initialize + + while (!queue.isEmpty()) { // @step:visit + int levelSize = queue.size(); // @step:visit + depth++; // @step:update-height + + for (int nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { // @step:visit + MinimumDepthIterativeNode current = queue.poll(); // @step:visit + + // First leaf node encountered is the minimum depth + if (current.left == null && current.right == null) { // @step:visit + return depth; // @step:complete + } + + if (current.left != null) queue.offer(current.left); // @step:traverse-left + if (current.right != null) queue.offer(current.right); // @step:traverse-right + } + } + + return depth; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/sources/minimum-depth-iterative.py b/src/algorithms/trees/properties/minimum-depth-iterative/sources/minimum-depth-iterative.py new file mode 100644 index 00000000..d8bb1890 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/sources/minimum-depth-iterative.py @@ -0,0 +1,33 @@ +# Minimum Depth of Binary Tree — BFS returns depth at first leaf encountered +from collections import deque + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def minimum_depth_iterative(root) -> int: + if root is None: # @step:initialize + return 0 + + queue = deque([root]) # @step:initialize + depth = 0 # @step:initialize + + while queue: # @step:visit + level_size = len(queue) # @step:visit + depth += 1 # @step:update-height + + for _ in range(level_size): # @step:visit + current = queue.popleft() # @step:visit + + # First leaf node encountered is the minimum depth + if current.left is None and current.right is None: # @step:visit + return depth # @step:complete + + if current.left is not None: # @step:traverse-left + queue.append(current.left) # @step:traverse-left + if current.right is not None: # @step:traverse-right + queue.append(current.right) # @step:traverse-right + + return depth # @step:complete diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/sources/minimum-depth-iterative.ts b/src/algorithms/trees/properties/minimum-depth-iterative/sources/minimum-depth-iterative.ts new file mode 100644 index 00000000..558c8302 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/sources/minimum-depth-iterative.ts @@ -0,0 +1,36 @@ +// Minimum Depth of Binary Tree — BFS returns depth at first leaf encountered + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function minimumDepthIterative(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + const queue: TreeNode[] = [root]; // @step:initialize + let depth = 0; // @step:initialize + + while (queue.length > 0) { + // @step:visit + const levelSize = queue.length; // @step:visit + depth += 1; // @step:update-height + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + // @step:visit + const current = queue.shift()!; // @step:visit + + // First leaf node encountered is the minimum depth + if (current.left === null && current.right === null) { + // @step:visit + return depth; // @step:complete + } + + if (current.left !== null) queue.push(current.left); // @step:traverse-left + if (current.right !== null) queue.push(current.right); // @step:traverse-right + } + } + + return depth; // @step:complete +} diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/step-generator.test.ts b/src/algorithms/trees/properties/minimum-depth-iterative/step-generator.test.ts new file mode 100644 index 00000000..7c9c2c72 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMinimumDepthIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMinimumDepthIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateMinimumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMinimumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMinimumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMinimumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMinimumDepthIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/minimum-depth-iterative/step-generator.ts b/src/algorithms/trees/properties/minimum-depth-iterative/step-generator.ts new file mode 100644 index 00000000..8a9522a4 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth-iterative/step-generator.ts @@ -0,0 +1,49 @@ +/** Step generator for Minimum Depth (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MINIMUM_DEPTH_ITERATIVE!); + +export interface MinimumDepthIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateMinimumDepthIterativeSteps( + input: MinimumDepthIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const queue: Array<[string, number]> = [[rootId, 1]]; + let result = 0; + + while (queue.length > 0) { + const entry = queue.shift()!; + const [nodeId, depth] = entry; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, depth }); + + if (!node.leftChildId && !node.rightChildId) { + result = depth; + tracker.updateDepth(nodeId, depth, { currentNode: nodeId, depth, isLeaf: true }); + break; + } + + if (node.leftChildId) queue.push([node.leftChildId, depth + 1]); + if (node.rightChildId) queue.push([node.rightChildId, depth + 1]); + } + + tracker.recordResult(result, { result }); + tracker.complete({ result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/minimum-depth/MinimumDepthPipeline.stories.tsx b/src/algorithms/trees/properties/minimum-depth/MinimumDepthPipeline.stories.tsx new file mode 100644 index 00000000..1975ff6d --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/MinimumDepthPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Minimum Depth. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMinimumDepthSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Minimum Depth", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/minimum-depth/educational.ts b/src/algorithms/trees/properties/minimum-depth/educational.ts new file mode 100644 index 00000000..f37e5b7b --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/educational.ts @@ -0,0 +1,45 @@ +import type { EducationalContent } from "@/types"; + +export const minimumDepthEducational: EducationalContent = { + overview: + "**Minimum Depth** is the length of the shortest path from the root to any leaf node. " + + "A leaf is a node with no children. Importantly, a node with only one child is NOT a leaf — " + + "the algorithm must traverse through single-child nodes to reach an actual leaf.", + + howItWorks: + "The recursive algorithm handles three cases:\n\n" + + "1. **Null node** — returns 0.\n" + + "2. **Only one child** — recurse into the existing child only (single-child nodes are not leaves).\n" + + "3. **Both children** — return `min(leftDepth, rightDepth) + 1`.\n\n" + + "The critical insight is case 2: a node with only a right child must count the depth through that right subtree, not return 1.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — may visit all nodes in the worst case.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth equals tree height.", + + bestAndWorstCase: + "**Best case** is a complete tree where the shallowest leaf is at depth 1 (root has leaf child).\n\n" + + "**Worst case** is a tree shaped such that the minimum depth leaf is at the bottom — " + + "all nodes may need to be visited before the minimum is determined.", + + realWorldUses: [ + "**Earliest termination detection:** Finding the minimum steps to reach any accepting state in a state machine.", + "**Network shortest hop:** Minimum depth in a routing tree tells you the minimum hops to reach any leaf node.", + "**Game tree shortest win:** Minimum depth of winning leaves represents the fastest guaranteed win.", + ], + + strengthsAndLimitations: { + strengths: [ + "Correctly handles the tricky single-child case that naive max-depth adaptations miss.", + "Simple recursive implementation once the edge cases are understood.", + ], + limitations: [ + "Must visit all nodes in the worst case, even if the minimum is near the root.", + "The iterative BFS version can short-circuit as soon as the first leaf is found.", + ], + }, + + whenToUseIt: + "Use minimum depth when you need the shallowest leaf. The BFS (iterative) variant is more efficient in practice " + + "because it returns immediately upon finding the first leaf at any level.", +}; diff --git a/src/algorithms/trees/properties/minimum-depth/index.ts b/src/algorithms/trees/properties/minimum-depth/index.ts new file mode 100644 index 00000000..9aad90bf --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { minimumDepth } from "./sources/minimum-depth.ts?fn"; +import { generateMinimumDepthSteps } from "./step-generator"; +import type { MinimumDepthInput } from "./step-generator"; +import { minimumDepthEducational } from "./educational"; + +import typescriptSource from "./sources/minimum-depth.ts?raw"; +import pythonSource from "./sources/minimum-depth.py?raw"; +import javaSource from "./sources/MinimumDepth.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeMinimumDepth(input: MinimumDepthInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return minimumDepth(toBSTNode(input.rootId)) as number; +} + +const minimumDepthDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MINIMUM_DEPTH!, + name: "Minimum Depth", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Recursively finds the minimum depth — shortest path from root to any leaf node. Handles single-child nodes correctly.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeMinimumDepth, + generateSteps: generateMinimumDepthSteps, + educational: minimumDepthEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(minimumDepthDefinition); diff --git a/src/algorithms/trees/properties/minimum-depth/minimum-depth.test.ts b/src/algorithms/trees/properties/minimum-depth/minimum-depth.test.ts new file mode 100644 index 00000000..db8adad5 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/minimum-depth.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { minimumDepth } from "./sources/minimum-depth.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("minimumDepth", () => { + it("returns 3 for a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(minimumDepth(root)).toBe(3); + }); + + it("returns 0 for a null root", () => { + expect(minimumDepth(null)).toBe(0); + }); + + it("returns 1 for a single node", () => { + expect(minimumDepth(node(42))).toBe(1); + }); + + it("does not count single-child nodes as leaves", () => { + const root = node(1, null, node(2, null, node(3))); + expect(minimumDepth(root)).toBe(3); + }); + + it("returns 2 for a tree with one leaf at level 2", () => { + expect(minimumDepth(node(1, node(2)))).toBe(2); + }); +}); diff --git a/src/algorithms/trees/properties/minimum-depth/sources/MinimumDepth.java b/src/algorithms/trees/properties/minimum-depth/sources/MinimumDepth.java new file mode 100644 index 00000000..41ee5209 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/sources/MinimumDepth.java @@ -0,0 +1,33 @@ +// Minimum Depth of Binary Tree — recursive DFS to nearest leaf + +class MinimumDepthNode { + int value; + MinimumDepthNode left, right; + MinimumDepthNode(int value) { this.value = value; } +} + +class MinimumDepth { + public int minimumDepth(MinimumDepthNode root) { + if (root == null) return 0; // @step:initialize + + // If only right child exists, recurse right + if (root.left == null && root.right != null) { // @step:visit + return minimumDepth(root.right) + 1; // @step:traverse-right + } + + // If only left child exists, recurse left + if (root.right == null && root.left != null) { // @step:visit + return minimumDepth(root.left) + 1; // @step:traverse-left + } + + // If leaf node, depth is 1 + if (root.left == null && root.right == null) { // @step:visit + return 1; // @step:update-height + } + + // Both children exist — take minimum + int leftDepth = minimumDepth(root.left); // @step:traverse-left + int rightDepth = minimumDepth(root.right); // @step:traverse-right + return Math.min(leftDepth, rightDepth) + 1; // @step:update-height + } +} diff --git a/src/algorithms/trees/properties/minimum-depth/sources/minimum-depth.py b/src/algorithms/trees/properties/minimum-depth/sources/minimum-depth.py new file mode 100644 index 00000000..39aef768 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/sources/minimum-depth.py @@ -0,0 +1,28 @@ +# Minimum Depth of Binary Tree — recursive DFS to nearest leaf + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def minimum_depth(root) -> int: + if root is None: # @step:initialize + return 0 + + # If only right child exists, recurse right + if root.left is None and root.right is not None: # @step:visit + return minimum_depth(root.right) + 1 # @step:traverse-right + + # If only left child exists, recurse left + if root.right is None and root.left is not None: # @step:visit + return minimum_depth(root.left) + 1 # @step:traverse-left + + # If leaf node, depth is 1 + if root.left is None and root.right is None: # @step:visit + return 1 # @step:update-height + + # Both children exist — take minimum + left_depth = minimum_depth(root.left) # @step:traverse-left + right_depth = minimum_depth(root.right) # @step:traverse-right + return min(left_depth, right_depth) + 1 # @step:update-height diff --git a/src/algorithms/trees/properties/minimum-depth/sources/minimum-depth.ts b/src/algorithms/trees/properties/minimum-depth/sources/minimum-depth.ts new file mode 100644 index 00000000..5507771f --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/sources/minimum-depth.ts @@ -0,0 +1,34 @@ +// Minimum Depth of Binary Tree — recursive DFS to nearest leaf + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function minimumDepth(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + // If only right child exists, recurse right + if (root.left === null && root.right !== null) { + // @step:visit + return minimumDepth(root.right) + 1; // @step:traverse-right + } + + // If only left child exists, recurse left + if (root.right === null && root.left !== null) { + // @step:visit + return minimumDepth(root.left) + 1; // @step:traverse-left + } + + // If leaf node, depth is 1 + if (root.left === null && root.right === null) { + // @step:visit + return 1; // @step:update-height + } + + // Both children exist — take minimum + const leftDepth = minimumDepth(root.left); // @step:traverse-left + const rightDepth = minimumDepth(root.right); // @step:traverse-right + return Math.min(leftDepth, rightDepth) + 1; // @step:update-height +} diff --git a/src/algorithms/trees/properties/minimum-depth/step-generator.test.ts b/src/algorithms/trees/properties/minimum-depth/step-generator.test.ts new file mode 100644 index 00000000..b4852e46 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMinimumDepthSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMinimumDepthSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("records result of 3 for a balanced tree", () => { + const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["result"]).toBe(3); + }); + + it("has incrementing step indices", () => { + const steps = generateMinimumDepthSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/minimum-depth/step-generator.ts b/src/algorithms/trees/properties/minimum-depth/step-generator.ts new file mode 100644 index 00000000..9d863743 --- /dev/null +++ b/src/algorithms/trees/properties/minimum-depth/step-generator.ts @@ -0,0 +1,62 @@ +/** Step generator for Minimum Depth — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MINIMUM_DEPTH!); + +export interface MinimumDepthInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateMinimumDepthSteps(input: MinimumDepthInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + function computeMinDepth(nodeId: string | null): number { + if (!nodeId) return 0; + const node = nodeMap.get(nodeId); + if (!node) return 0; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value }); + + // Leaf node + if (!node.leftChildId && !node.rightChildId) { + tracker.updateHeight(nodeId, 1, { currentNode: nodeId, depth: 1 }); + return 1; + } + + if (!node.leftChildId && node.rightChildId) { + const rightDepth = computeMinDepth(node.rightChildId); + const depth = rightDepth + 1; + tracker.updateHeight(nodeId, depth, { currentNode: nodeId, depth }); + return depth; + } + + if (!node.rightChildId && node.leftChildId) { + const leftDepth = computeMinDepth(node.leftChildId); + const depth = leftDepth + 1; + tracker.updateHeight(nodeId, depth, { currentNode: nodeId, depth }); + return depth; + } + + const leftDepth = computeMinDepth(node.leftChildId); + const rightDepth = computeMinDepth(node.rightChildId); + const depth = Math.min(leftDepth, rightDepth) + 1; + + tracker.updateHeight(nodeId, depth, { currentNode: nodeId, depth }); + return depth; + } + + const result = computeMinDepth(rootId); + tracker.recordResult(result, { result }); + tracker.complete({ result }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/path-sum-iterative/PathSumIterativePipeline.stories.tsx b/src/algorithms/trees/properties/path-sum-iterative/PathSumIterativePipeline.stories.tsx new file mode 100644 index 00000000..01ce1479 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/PathSumIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Path Sum Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generatePathSumIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generatePathSumIterativeSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + +const meta: Meta = { + title: "Algorithm Pipelines/Path Sum Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/path-sum-iterative/educational.ts b/src/algorithms/trees/properties/path-sum-iterative/educational.ts new file mode 100644 index 00000000..2924d57a --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/educational.ts @@ -0,0 +1,35 @@ +import type { EducationalContent } from "@/types"; + +export const pathSumIterativeEducational: EducationalContent = { + overview: + "**Path Sum (Iterative)** performs the same root-to-leaf sum check using an explicit stack. " + + "Each stack entry pairs a node with the accumulated path sum from the root to that node.", + + howItWorks: + "1. Push `[root, root.value]` onto the stack.\n" + + "2. Pop an entry `[current, runningSum]`.\n" + + "3. At a leaf, check `runningSum === targetSum`. Return `true` immediately if matched.\n" + + "4. Otherwise, push right and left children with `runningSum + childValue`.\n" + + "5. Continue until the stack empties — return `false`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** worst case.\n\n" + + "**Space Complexity: `O(h)`** — stack holds at most `h` entries.", + + bestAndWorstCase: + "**Best case** is an early match — terminates after visiting few nodes.\n\n" + + "**Worst case** is a full traversal when no path matches.", + + realWorldUses: [ + "**Stack-safe path checking:** Avoids recursion limits for deep trees.", + "**Debugging traversal:** Explicit stack makes it easy to inspect which nodes were processed.", + ], + + strengthsAndLimitations: { + strengths: ["No stack overflow risk.", "Explicit state makes debugging easier."], + limitations: ["More verbose than the recursive version."], + }, + + whenToUseIt: + "Prefer the iterative version when tree depth is large or when recursion must be avoided.", +}; diff --git a/src/algorithms/trees/properties/path-sum-iterative/index.ts b/src/algorithms/trees/properties/path-sum-iterative/index.ts new file mode 100644 index 00000000..fd77dc66 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { pathSumIterative } from "./sources/path-sum-iterative.ts?fn"; +import { generatePathSumIterativeSteps } from "./step-generator"; +import type { PathSumIterativeInput } from "./step-generator"; +import { pathSumIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/path-sum-iterative.ts?raw"; +import pythonSource from "./sources/path-sum-iterative.py?raw"; +import javaSource from "./sources/PathSumIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7]. Path 4→2→1 sums to 7. */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executePathSumIterative(input: PathSumIterativeInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return pathSumIterative(toBSTNode(input.rootId), input.targetSum) as boolean; +} + +const pathSumIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.PATH_SUM_ITERATIVE!, + name: "Path Sum (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Stack-based path sum check — pairs each node with its running path sum during DFS traversal", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetSum: 7 }, + }, + execute: executePathSumIterative, + generateSteps: generatePathSumIterativeSteps, + educational: pathSumIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(pathSumIterativeDefinition); diff --git a/src/algorithms/trees/properties/path-sum-iterative/path-sum-iterative.test.ts b/src/algorithms/trees/properties/path-sum-iterative/path-sum-iterative.test.ts new file mode 100644 index 00000000..7f6805c2 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/path-sum-iterative.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { pathSumIterative } from "./sources/path-sum-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("pathSumIterative", () => { + it("returns true when path sum exists (4+2+1=7)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(pathSumIterative(root, 7)).toBe(true); + }); + + it("returns false when path sum does not exist", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(pathSumIterative(root, 100)).toBe(false); + }); + + it("returns false for null root", () => { + expect(pathSumIterative(null, 5)).toBe(false); + }); + + it("returns true for single node matching target", () => { + expect(pathSumIterative(node(5), 5)).toBe(true); + }); +}); diff --git a/src/algorithms/trees/properties/path-sum-iterative/sources/PathSumIterative.java b/src/algorithms/trees/properties/path-sum-iterative/sources/PathSumIterative.java new file mode 100644 index 00000000..7bc11cb8 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/sources/PathSumIterative.java @@ -0,0 +1,39 @@ +// Path Sum (Iterative) — stack-based DFS with running sum tracking +import java.util.ArrayDeque; +import java.util.Deque; + +class PathSumIterativeNode { + int value; + PathSumIterativeNode left, right; + PathSumIterativeNode(int value) { this.value = value; } +} + +class PathSumIterative { + public boolean pathSumIterative(PathSumIterativeNode root, int targetSum) { + if (root == null) return false; // @step:initialize + + Deque stack = new ArrayDeque<>(); // @step:initialize + stack.push(new Object[]{root, root.value}); // @step:initialize + + while (!stack.isEmpty()) { // @step:visit + Object[] entry = stack.pop(); // @step:visit + PathSumIterativeNode current = (PathSumIterativeNode) entry[0]; // @step:visit + int runningSum = (int) entry[1]; // @step:visit + + // Leaf node — check if path sum matches target + if (current.left == null && current.right == null) { // @step:check-balance + if (runningSum == targetSum) return true; // @step:complete + } + + if (current.right != null) { // @step:traverse-right + stack.push(new Object[]{current.right, runningSum + current.right.value}); // @step:traverse-right + } + + if (current.left != null) { // @step:traverse-left + stack.push(new Object[]{current.left, runningSum + current.left.value}); // @step:traverse-left + } + } + + return false; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/path-sum-iterative/sources/path-sum-iterative.py b/src/algorithms/trees/properties/path-sum-iterative/sources/path-sum-iterative.py new file mode 100644 index 00000000..d6a3f5c9 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/sources/path-sum-iterative.py @@ -0,0 +1,29 @@ +# Path Sum (Iterative) — stack-based DFS with running sum tracking + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def path_sum_iterative(root, target_sum: int) -> bool: + if root is None: # @step:initialize + return False + + stack = [(root, root.value)] # @step:initialize + + while stack: # @step:visit + current, running_sum = stack.pop() # @step:visit + + # Leaf node — check if path sum matches target + if current.left is None and current.right is None: # @step:check-balance + if running_sum == target_sum: # @step:complete + return True + + if current.right is not None: # @step:traverse-right + stack.append((current.right, running_sum + current.right.value)) # @step:traverse-right + + if current.left is not None: # @step:traverse-left + stack.append((current.left, running_sum + current.left.value)) # @step:traverse-left + + return False # @step:complete diff --git a/src/algorithms/trees/properties/path-sum-iterative/sources/path-sum-iterative.ts b/src/algorithms/trees/properties/path-sum-iterative/sources/path-sum-iterative.ts new file mode 100644 index 00000000..aa14b5ca --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/sources/path-sum-iterative.ts @@ -0,0 +1,37 @@ +// Path Sum (Iterative) — stack-based DFS with running sum tracking + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function pathSumIterative(root: TreeNode | null, targetSum: number): boolean { + if (root === null) return false; // @step:initialize + + const stack: Array<[TreeNode, number]> = [[root, root.value]]; // @step:initialize + + while (stack.length > 0) { + // @step:visit + const entry = stack.pop()!; // @step:visit + const [current, runningSum] = entry; // @step:visit + + // Leaf node — check if path sum matches target + if (current.left === null && current.right === null) { + // @step:check-balance + if (runningSum === targetSum) return true; // @step:complete + } + + if (current.right !== null) { + // @step:traverse-right + stack.push([current.right, runningSum + current.right.value]); // @step:traverse-right + } + + if (current.left !== null) { + // @step:traverse-left + stack.push([current.left, runningSum + current.left.value]); // @step:traverse-left + } + } + + return false; // @step:complete +} diff --git a/src/algorithms/trees/properties/path-sum-iterative/step-generator.test.ts b/src/algorithms/trees/properties/path-sum-iterative/step-generator.test.ts new file mode 100644 index 00000000..045bb9e2 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/step-generator.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generatePathSumIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generatePathSumIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generatePathSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetSum: 7, + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generatePathSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetSum: 7, + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generatePathSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetSum: 7, + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generatePathSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetSum: 7, + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generatePathSumIterativeSteps({ + nodes: defaultNodes, + rootId: "n4", + targetSum: 7, + }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/path-sum-iterative/step-generator.ts b/src/algorithms/trees/properties/path-sum-iterative/step-generator.ts new file mode 100644 index 00000000..72842c26 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum-iterative/step-generator.ts @@ -0,0 +1,69 @@ +/** Step generator for Path Sum (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.PATH_SUM_ITERATIVE!); + +export interface PathSumIterativeInput { + nodes: TreeNode[]; + rootId: string; + targetSum: number; +} + +export function generatePathSumIterativeSteps(input: PathSumIterativeInput): ExecutionStep[] { + const { nodes, rootId, targetSum } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, targetSum }); + + const rootNode = nodeMap.get(rootId); + if (!rootNode) { + tracker.recordResult(false, { result: false }); + tracker.complete({ result: false }); + return tracker.getSteps(); + } + + const stack: Array<[string, number]> = [[rootId, rootNode.value]]; + let found = false; + + while (stack.length > 0 && !found) { + const entry = stack.pop()!; + const [nodeId, runningSum] = entry; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, runningSum }); + + if (!node.leftChildId && !node.rightChildId) { + if (runningSum === targetSum) { + found = true; + tracker.markValid(nodeId, { + currentNode: nodeId, + runningSum, + targetSum, + pathSumFound: true, + }); + } else { + tracker.markInvalid(nodeId, { currentNode: nodeId, runningSum, targetSum }); + } + } + + if (node.rightChildId) { + const rightNode = nodeMap.get(node.rightChildId); + if (rightNode) stack.push([node.rightChildId, runningSum + rightNode.value]); + } + if (node.leftChildId) { + const leftNode = nodeMap.get(node.leftChildId); + if (leftNode) stack.push([node.leftChildId, runningSum + leftNode.value]); + } + } + + tracker.recordResult(found, { result: found }); + tracker.complete({ result: found }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/path-sum/PathSumPipeline.stories.tsx b/src/algorithms/trees/properties/path-sum/PathSumPipeline.stories.tsx new file mode 100644 index 00000000..b1026f14 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/PathSumPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Path Sum. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generatePathSumSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generatePathSumSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + +const meta: Meta = { + title: "Algorithm Pipelines/Path Sum", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/path-sum/educational.ts b/src/algorithms/trees/properties/path-sum/educational.ts new file mode 100644 index 00000000..fbb9f47f --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/educational.ts @@ -0,0 +1,44 @@ +import type { EducationalContent } from "@/types"; + +export const pathSumEducational: EducationalContent = { + overview: + "**Path Sum** determines whether any root-to-leaf path in the tree sums to a given target value. " + + "It only considers complete root-to-leaf paths, not any partial path.", + + howItWorks: + "The algorithm uses DFS with a running subtraction:\n\n" + + "1. Subtract the current node value from `targetSum` to get `remaining`.\n" + + "2. At a leaf node, check if `remaining === 0`.\n" + + "3. Otherwise, recurse on both children.\n" + + "4. Return `true` as soon as any path satisfies the condition.\n\n" + + "Early return prevents unnecessary traversal once the target sum is found.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** worst case (target not found), `O(depth)` best case (found early).\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case** is when the leftmost root-to-leaf path satisfies the target — returns after `O(h)` steps.\n\n" + + "**Worst case** is when no path satisfies the target — all nodes are visited.", + + realWorldUses: [ + "**Budget allocation:** Does any root-to-leaf combination of expenses equal the budget?", + "**Authentication chains:** Does a root-to-leaf permission path satisfy a cumulative threshold?", + "**Game state reachability:** Can any sequence of moves reach a target score?", + ], + + strengthsAndLimitations: { + strengths: [ + "Early termination on the first matching path.", + "Clean recursive implementation with a single parameter change per level.", + ], + limitations: [ + "Does not return the actual path — only a boolean. Use all-root-to-leaf-paths for path collection.", + "Recursion depth limited by tree height.", + ], + }, + + whenToUseIt: + "Use path sum to quickly check if any valid path exists. " + + "For collecting all matching paths, use a path-collection variant instead.", +}; diff --git a/src/algorithms/trees/properties/path-sum/index.ts b/src/algorithms/trees/properties/path-sum/index.ts new file mode 100644 index 00000000..a6299b36 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { pathSum } from "./sources/path-sum.ts?fn"; +import { generatePathSumSteps } from "./step-generator"; +import type { PathSumInput } from "./step-generator"; +import { pathSumEducational } from "./educational"; + +import typescriptSource from "./sources/path-sum.ts?raw"; +import pythonSource from "./sources/path-sum.py?raw"; +import javaSource from "./sources/PathSum.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7]. Path 4→2→1 sums to 7. */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executePathSum(input: PathSumInput): boolean { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return pathSum(toBSTNode(input.rootId), input.targetSum) as boolean; +} + +const pathSumDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.PATH_SUM!, + name: "Path Sum", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Checks if any root-to-leaf path in the tree sums to the target value using recursive DFS with running subtraction", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4", targetSum: 7 }, + }, + execute: executePathSum, + generateSteps: generatePathSumSteps, + educational: pathSumEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(pathSumDefinition); diff --git a/src/algorithms/trees/properties/path-sum/path-sum.test.ts b/src/algorithms/trees/properties/path-sum/path-sum.test.ts new file mode 100644 index 00000000..60c963bc --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/path-sum.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { pathSum } from "./sources/path-sum.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("pathSum", () => { + it("returns true when path sum exists (4+2+1=7)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(pathSum(root, 7)).toBe(true); + }); + + it("returns false when path sum does not exist", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(pathSum(root, 100)).toBe(false); + }); + + it("returns false for null root", () => { + expect(pathSum(null, 5)).toBe(false); + }); + + it("returns true for single node matching target", () => { + expect(pathSum(node(5), 5)).toBe(true); + }); + + it("returns false for single node not matching target", () => { + expect(pathSum(node(5), 3)).toBe(false); + }); +}); diff --git a/src/algorithms/trees/properties/path-sum/sources/PathSum.java b/src/algorithms/trees/properties/path-sum/sources/PathSum.java new file mode 100644 index 00000000..ca92766c --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/sources/PathSum.java @@ -0,0 +1,27 @@ +// Path Sum — recursive DFS: check if any root-to-leaf path sums to target + +class PathSumNode { + int value; + PathSumNode left, right; + PathSumNode(int value) { this.value = value; } +} + +class PathSum { + public boolean pathSum(PathSumNode root, int targetSum) { + if (root == null) return false; // @step:initialize + + // Leaf node — check if remaining sum equals node value + if (root.left == null && root.right == null) { // @step:visit + return root.value == targetSum; // @step:check-balance + } + + int remaining = targetSum - root.value; // @step:compute-value + + // Recurse on left and right subtrees + boolean foundLeft = pathSum(root.left, remaining); // @step:traverse-left + if (foundLeft) return true; // @step:check-balance + + boolean foundRight = pathSum(root.right, remaining); // @step:traverse-right + return foundRight; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/path-sum/sources/path-sum.py b/src/algorithms/trees/properties/path-sum/sources/path-sum.py new file mode 100644 index 00000000..5a76c092 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/sources/path-sum.py @@ -0,0 +1,25 @@ +# Path Sum — recursive DFS: check if any root-to-leaf path sums to target + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def path_sum(root, target_sum: int) -> bool: + if root is None: # @step:initialize + return False + + # Leaf node — check if remaining sum equals node value + if root.left is None and root.right is None: # @step:visit + return root.value == target_sum # @step:check-balance + + remaining = target_sum - root.value # @step:compute-value + + # Recurse on left and right subtrees + found_left = path_sum(root.left, remaining) # @step:traverse-left + if found_left: # @step:check-balance + return True + + found_right = path_sum(root.right, remaining) # @step:traverse-right + return found_right # @step:complete diff --git a/src/algorithms/trees/properties/path-sum/sources/path-sum.ts b/src/algorithms/trees/properties/path-sum/sources/path-sum.ts new file mode 100644 index 00000000..26389010 --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/sources/path-sum.ts @@ -0,0 +1,26 @@ +// Path Sum — recursive DFS: check if any root-to-leaf path sums to target + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function pathSum(root: TreeNode | null, targetSum: number): boolean { + if (root === null) return false; // @step:initialize + + // Leaf node — check if remaining sum equals node value + if (root.left === null && root.right === null) { + // @step:visit + return root.value === targetSum; // @step:check-balance + } + + const remaining = targetSum - root.value; // @step:compute-value + + // Recurse on left and right subtrees + const foundLeft = pathSum(root.left, remaining); // @step:traverse-left + if (foundLeft) return true; // @step:check-balance + + const foundRight = pathSum(root.right, remaining); // @step:traverse-right + return foundRight; // @step:complete +} diff --git a/src/algorithms/trees/properties/path-sum/step-generator.test.ts b/src/algorithms/trees/properties/path-sum/step-generator.test.ts new file mode 100644 index 00000000..5f7e347c --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generatePathSumSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generatePathSumSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generatePathSumSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generatePathSumSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generatePathSumSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generatePathSumSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generatePathSumSteps({ nodes: defaultNodes, rootId: "n4", targetSum: 7 }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/path-sum/step-generator.ts b/src/algorithms/trees/properties/path-sum/step-generator.ts new file mode 100644 index 00000000..f027be3a --- /dev/null +++ b/src/algorithms/trees/properties/path-sum/step-generator.ts @@ -0,0 +1,57 @@ +/** Step generator for Path Sum — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.PATH_SUM!); + +export interface PathSumInput { + nodes: TreeNode[]; + rootId: string; + targetSum: number; +} + +export function generatePathSumSteps(input: PathSumInput): ExecutionStep[] { + const { nodes, rootId, targetSum } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, targetSum }); + + let found = false; + + function dfs(nodeId: string | null, remaining: number): void { + if (!nodeId || found) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, remaining }); + + const newRemaining = remaining - node.value; + + if (!node.leftChildId && !node.rightChildId) { + if (newRemaining === 0) { + found = true; + tracker.markValid(nodeId, { currentNode: nodeId, value: node.value, pathSumFound: true }); + } else { + tracker.markInvalid(nodeId, { + currentNode: nodeId, + value: node.value, + remaining: newRemaining, + }); + } + return; + } + + dfs(node.leftChildId, newRemaining); + dfs(node.rightChildId, newRemaining); + } + + dfs(rootId, targetSum); + tracker.recordResult(found, { result: found }); + tracker.complete({ result: found }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/SumOfLeftLeavesIterativePipeline.stories.tsx b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/SumOfLeftLeavesIterativePipeline.stories.tsx new file mode 100644 index 00000000..dd326c64 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/SumOfLeftLeavesIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Sum of Left Leaves Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSumOfLeftLeavesIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateSumOfLeftLeavesIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Sum of Left Leaves Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/educational.ts b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/educational.ts new file mode 100644 index 00000000..0a0df177 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/educational.ts @@ -0,0 +1,29 @@ +import type { EducationalContent } from "@/types"; + +export const sumOfLeftLeavesIterativeEducational: EducationalContent = { + overview: + "**Sum of Left Leaves (Iterative)** uses a stack to simulate the same DFS as the recursive version, " + + "carrying an `isLeft` boolean alongside each node on the stack.", + + howItWorks: + "1. Push `[root, isLeft=false]` onto the stack.\n" + + "2. Pop `[current, isLeft]`.\n" + + "3. If it's a left leaf (`isLeft && !left && !right`), add to sum.\n" + + "4. Push right child with `isLeft=false` and left child with `isLeft=true`.\n" + + "5. Continue until the stack empties.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — all nodes are visited.\n\n" + + "**Space Complexity: `O(h)`** — stack depth.", + + bestAndWorstCase: "Both best and worst cases are `O(n)` since all nodes must be visited.", + + realWorldUses: ["**Recursion-free left-leaf analysis:** Safe for deep trees."], + + strengthsAndLimitations: { + strengths: ["No recursion stack overflow risk."], + limitations: ["More verbose than the recursive version."], + }, + + whenToUseIt: "Use the iterative version for large or deeply nested trees.", +}; diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/index.ts b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/index.ts new file mode 100644 index 00000000..f54b7544 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sumOfLeftLeavesIterative } from "./sources/sum-of-left-leaves-iterative.ts?fn"; +import { generateSumOfLeftLeavesIterativeSteps } from "./step-generator"; +import type { SumOfLeftLeavesIterativeInput } from "./step-generator"; +import { sumOfLeftLeavesIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/sum-of-left-leaves-iterative.ts?raw"; +import pythonSource from "./sources/sum-of-left-leaves-iterative.py?raw"; +import javaSource from "./sources/SumOfLeftLeavesIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left leaves are 1 and 5 (sum=6) */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeSumOfLeftLeavesIterative(input: SumOfLeftLeavesIterativeInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return sumOfLeftLeavesIterative(toBSTNode(input.rootId)) as number; +} + +const sumOfLeftLeavesIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SUM_OF_LEFT_LEAVES_ITERATIVE!, + name: "Sum of Left Leaves (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Stack-based DFS that tracks whether each node was reached as a left child to identify left leaves", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeSumOfLeftLeavesIterative, + generateSteps: generateSumOfLeftLeavesIterativeSteps, + educational: sumOfLeftLeavesIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(sumOfLeftLeavesIterativeDefinition); diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/SumOfLeftLeavesIterative.java b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/SumOfLeftLeavesIterative.java new file mode 100644 index 00000000..9fd30dd8 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/SumOfLeftLeavesIterative.java @@ -0,0 +1,40 @@ +// Sum of Left Leaves (Iterative) — stack-based DFS checking left leaf condition +import java.util.ArrayDeque; +import java.util.Deque; + +class LeftLeavesIterativeNode { + int value; + LeftLeavesIterativeNode left, right; + LeftLeavesIterativeNode(int value) { this.value = value; } +} + +class SumOfLeftLeavesIterative { + public int sumOfLeftLeavesIterative(LeftLeavesIterativeNode root) { + if (root == null) return 0; // @step:initialize + + Deque stack = new ArrayDeque<>(); // @step:initialize + stack.push(new Object[]{root, false}); // @step:initialize + int totalSum = 0; // @step:initialize + + while (!stack.isEmpty()) { // @step:visit + Object[] entry = stack.pop(); // @step:visit + LeftLeavesIterativeNode current = (LeftLeavesIterativeNode) entry[0]; // @step:visit + boolean isLeft = (boolean) entry[1]; // @step:visit + + // Accumulate value when we find a left leaf + if (current.left == null && current.right == null && isLeft) { // @step:check-balance + totalSum += current.value; // @step:add-to-result + } + + if (current.right != null) { // @step:traverse-right + stack.push(new Object[]{current.right, false}); // @step:traverse-right + } + + if (current.left != null) { // @step:traverse-left + stack.push(new Object[]{current.left, true}); // @step:traverse-left + } + } + + return totalSum; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/sum-of-left-leaves-iterative.py b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/sum-of-left-leaves-iterative.py new file mode 100644 index 00000000..ad4793b9 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/sum-of-left-leaves-iterative.py @@ -0,0 +1,29 @@ +# Sum of Left Leaves (Iterative) — stack-based DFS checking left leaf condition + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def sum_of_left_leaves_iterative(root) -> int: + if root is None: # @step:initialize + return 0 + + stack = [(root, False)] # @step:initialize + total_sum = 0 # @step:initialize + + while stack: # @step:visit + current, is_left = stack.pop() # @step:visit + + # Accumulate value when we find a left leaf + if current.left is None and current.right is None and is_left: # @step:check-balance + total_sum += current.value # @step:add-to-result + + if current.right is not None: # @step:traverse-right + stack.append((current.right, False)) # @step:traverse-right + + if current.left is not None: # @step:traverse-left + stack.append((current.left, True)) # @step:traverse-left + + return total_sum # @step:complete diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/sum-of-left-leaves-iterative.ts b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/sum-of-left-leaves-iterative.ts new file mode 100644 index 00000000..0bafcf3f --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sources/sum-of-left-leaves-iterative.ts @@ -0,0 +1,38 @@ +// Sum of Left Leaves (Iterative) — stack-based DFS checking left leaf condition + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function sumOfLeftLeavesIterative(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + const stack: Array<[TreeNode, boolean]> = [[root, false]]; // @step:initialize + let totalSum = 0; // @step:initialize + + while (stack.length > 0) { + // @step:visit + const entry = stack.pop()!; // @step:visit + const [current, isLeft] = entry; // @step:visit + + // Accumulate value when we find a left leaf + if (current.left === null && current.right === null && isLeft) { + // @step:check-balance + totalSum += current.value; // @step:add-to-result + } + + if (current.right !== null) { + // @step:traverse-right + stack.push([current.right, false]); // @step:traverse-right + } + + if (current.left !== null) { + // @step:traverse-left + stack.push([current.left, true]); // @step:traverse-left + } + } + + return totalSum; // @step:complete +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/step-generator.test.ts b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/step-generator.test.ts new file mode 100644 index 00000000..a9191f96 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSumOfLeftLeavesIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateSumOfLeftLeavesIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateSumOfLeftLeavesIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSumOfLeftLeavesIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSumOfLeftLeavesIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSumOfLeftLeavesIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSumOfLeftLeavesIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/step-generator.ts b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/step-generator.ts new file mode 100644 index 00000000..07929ac4 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/step-generator.ts @@ -0,0 +1,53 @@ +/** Step generator for Sum of Left Leaves (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SUM_OF_LEFT_LEAVES_ITERATIVE!); + +export interface SumOfLeftLeavesIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateSumOfLeftLeavesIterativeSteps( + input: SumOfLeftLeavesIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const stack: Array<[string, boolean]> = [[rootId, false]]; + let totalSum = 0; + + while (stack.length > 0) { + const entry = stack.pop()!; + const [nodeId, isLeft] = entry; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, isLeft }); + + if (!node.leftChildId && !node.rightChildId && isLeft) { + totalSum += node.value; + tracker.markValid(nodeId, { + currentNode: nodeId, + value: node.value, + isLeftLeaf: true, + totalSum, + }); + } + + if (node.rightChildId) stack.push([node.rightChildId, false]); + if (node.leftChildId) stack.push([node.leftChildId, true]); + } + + tracker.recordResult(totalSum, { result: totalSum }); + tracker.complete({ result: totalSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sum-of-left-leaves-iterative.test.ts b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sum-of-left-leaves-iterative.test.ts new file mode 100644 index 00000000..9204cf08 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves-iterative/sum-of-left-leaves-iterative.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { sumOfLeftLeavesIterative } from "./sources/sum-of-left-leaves-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("sumOfLeftLeavesIterative", () => { + it("returns 6 (1+5) for a 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(sumOfLeftLeavesIterative(root)).toBe(6); + }); + + it("returns 0 for null root", () => { + expect(sumOfLeftLeavesIterative(null)).toBe(0); + }); + + it("returns 0 for a single node", () => { + expect(sumOfLeftLeavesIterative(node(1))).toBe(0); + }); + + it("returns the value of a left leaf", () => { + expect(sumOfLeftLeavesIterative(node(1, node(5)))).toBe(5); + }); +}); diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/SumOfLeftLeavesPipeline.stories.tsx b/src/algorithms/trees/properties/sum-of-left-leaves/SumOfLeftLeavesPipeline.stories.tsx new file mode 100644 index 00000000..4325154d --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/SumOfLeftLeavesPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Sum of Left Leaves. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSumOfLeftLeavesSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateSumOfLeftLeavesSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Sum of Left Leaves", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/educational.ts b/src/algorithms/trees/properties/sum-of-left-leaves/educational.ts new file mode 100644 index 00000000..8655d485 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/educational.ts @@ -0,0 +1,37 @@ +import type { EducationalContent } from "@/types"; + +export const sumOfLeftLeavesEducational: EducationalContent = { + overview: + "**Sum of Left Leaves** sums the values of all nodes that are: (a) a leaf (no children), and (b) a left child of their parent. " + + "Right leaves and non-leaf left children are excluded.", + + howItWorks: + "A DFS helper `dfs(node, isLeft)` passes a flag indicating whether the current node was reached from the left:\n\n" + + "1. If `node` is a leaf AND `isLeft` is `true`, add its value to the sum.\n" + + "2. Recurse left with `isLeft = true`.\n" + + "3. Recurse right with `isLeft = false`.\n\n" + + "The root is called with `isLeft = false` since it has no parent.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — all nodes are visited.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: + "**Best case** and **worst case** are both `O(n)` — all nodes are visited regardless of the tree structure.", + + realWorldUses: [ + "**Asymmetric tree analysis:** Comparing left vs right leaf distributions.", + "**Tax/accounting trees:** Summing leaves on a specific side of hierarchical accounts.", + ], + + strengthsAndLimitations: { + strengths: [ + "Clean use of a flag parameter to propagate left-child context without extra data structures.", + ], + limitations: ["Only returns a sum, not the list of left leaves."], + }, + + whenToUseIt: + "Use when you need to aggregate only left leaf values. " + + "The flag-passing pattern generalizes to other 'conditional leaf' aggregation problems.", +}; diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/index.ts b/src/algorithms/trees/properties/sum-of-left-leaves/index.ts new file mode 100644 index 00000000..a907b719 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sumOfLeftLeaves } from "./sources/sum-of-left-leaves.ts?fn"; +import { generateSumOfLeftLeavesSteps } from "./step-generator"; +import type { SumOfLeftLeavesInput } from "./step-generator"; +import { sumOfLeftLeavesEducational } from "./educational"; + +import typescriptSource from "./sources/sum-of-left-leaves.ts?raw"; +import pythonSource from "./sources/sum-of-left-leaves.py?raw"; +import javaSource from "./sources/SumOfLeftLeaves.java?raw"; + +/** Balanced 7-node BST: root=4, left leaves are 1 and 5 (sum=6) */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeSumOfLeftLeaves(input: SumOfLeftLeavesInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return sumOfLeftLeaves(toBSTNode(input.rootId)) as number; +} + +const sumOfLeftLeavesDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SUM_OF_LEFT_LEAVES!, + name: "Sum of Left Leaves", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Sums the values of all left leaf nodes. A leaf is a node with no children. A left leaf is a leaf that is the left child of its parent.", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeSumOfLeftLeaves, + generateSteps: generateSumOfLeftLeavesSteps, + educational: sumOfLeftLeavesEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(sumOfLeftLeavesDefinition); diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/sources/SumOfLeftLeaves.java b/src/algorithms/trees/properties/sum-of-left-leaves/sources/SumOfLeftLeaves.java new file mode 100644 index 00000000..40c844ed --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/sources/SumOfLeftLeaves.java @@ -0,0 +1,27 @@ +// Sum of Left Leaves — recursive: sum values of all left leaf nodes + +class LeftLeavesNode { + int value; + LeftLeavesNode left, right; + LeftLeavesNode(int value) { this.value = value; } +} + +class SumOfLeftLeaves { + public int sumOfLeftLeaves(LeftLeavesNode root) { + if (root == null) return 0; // @step:initialize + return dfs(root, false); // @step:complete + } + + private int dfs(LeftLeavesNode node, boolean isLeft) { + if (node == null) return 0; // @step:initialize + + // Left leaf node contributes its value + if (node.left == null && node.right == null && isLeft) { // @step:visit + return node.value; // @step:add-to-result + } + + int leftSum = dfs(node.left, true); // @step:traverse-left + int rightSum = dfs(node.right, false); // @step:traverse-right + return leftSum + rightSum; // @step:compute-value + } +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/sources/sum-of-left-leaves.py b/src/algorithms/trees/properties/sum-of-left-leaves/sources/sum-of-left-leaves.py new file mode 100644 index 00000000..3253f0ee --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/sources/sum-of-left-leaves.py @@ -0,0 +1,25 @@ +# Sum of Left Leaves — recursive: sum values of all left leaf nodes + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def sum_of_left_leaves(root) -> int: + if root is None: # @step:initialize + return 0 + + def dfs(node, is_left: bool) -> int: + if node is None: # @step:initialize + return 0 + + # Left leaf node contributes its value + if node.left is None and node.right is None and is_left: # @step:visit + return node.value # @step:add-to-result + + left_sum = dfs(node.left, True) # @step:traverse-left + right_sum = dfs(node.right, False) # @step:traverse-right + return left_sum + right_sum # @step:compute-value + + return dfs(root, False) # @step:complete diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/sources/sum-of-left-leaves.ts b/src/algorithms/trees/properties/sum-of-left-leaves/sources/sum-of-left-leaves.ts new file mode 100644 index 00000000..e34bca75 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/sources/sum-of-left-leaves.ts @@ -0,0 +1,27 @@ +// Sum of Left Leaves — recursive: sum values of all left leaf nodes + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function sumOfLeftLeaves(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + function dfs(node: TreeNode | null, isLeft: boolean): number { + if (node === null) return 0; // @step:initialize + + // Left leaf node contributes its value + if (node.left === null && node.right === null && isLeft) { + // @step:visit + return node.value; // @step:add-to-result + } + + const leftSum = dfs(node.left, true); // @step:traverse-left + const rightSum = dfs(node.right, false); // @step:traverse-right + return leftSum + rightSum; // @step:compute-value + } + + return dfs(root, false); // @step:complete +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/step-generator.test.ts b/src/algorithms/trees/properties/sum-of-left-leaves/step-generator.test.ts new file mode 100644 index 00000000..6ff4236c --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSumOfLeftLeavesSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateSumOfLeftLeavesSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateSumOfLeftLeavesSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSumOfLeftLeavesSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSumOfLeftLeavesSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSumOfLeftLeavesSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSumOfLeftLeavesSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/step-generator.ts b/src/algorithms/trees/properties/sum-of-left-leaves/step-generator.ts new file mode 100644 index 00000000..f6fb1674 --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/step-generator.ts @@ -0,0 +1,51 @@ +/** Step generator for Sum of Left Leaves — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SUM_OF_LEFT_LEAVES!); + +export interface SumOfLeftLeavesInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateSumOfLeftLeavesSteps(input: SumOfLeftLeavesInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + let totalSum = 0; + + function dfs(nodeId: string | null, isLeft: boolean): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, isLeft }); + + if (!node.leftChildId && !node.rightChildId && isLeft) { + totalSum += node.value; + tracker.markValid(nodeId, { + currentNode: nodeId, + value: node.value, + isLeftLeaf: true, + totalSum, + }); + return; + } + + dfs(node.leftChildId, true); + dfs(node.rightChildId, false); + } + + dfs(rootId, false); + tracker.recordResult(totalSum, { result: totalSum }); + tracker.complete({ result: totalSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/sum-of-left-leaves/sum-of-left-leaves.test.ts b/src/algorithms/trees/properties/sum-of-left-leaves/sum-of-left-leaves.test.ts new file mode 100644 index 00000000..de496d7f --- /dev/null +++ b/src/algorithms/trees/properties/sum-of-left-leaves/sum-of-left-leaves.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { sumOfLeftLeaves } from "./sources/sum-of-left-leaves.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("sumOfLeftLeaves", () => { + it("returns 6 (1+5) for a 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(sumOfLeftLeaves(root)).toBe(6); + }); + + it("returns 0 for null root", () => { + expect(sumOfLeftLeaves(null)).toBe(0); + }); + + it("returns 0 for single node (root is not a left leaf)", () => { + expect(sumOfLeftLeaves(node(1))).toBe(0); + }); + + it("returns the value of a left leaf in a two-node tree", () => { + expect(sumOfLeftLeaves(node(1, node(5)))).toBe(5); + }); + + it("returns 0 when there are no left leaves", () => { + expect(sumOfLeftLeaves(node(1, null, node(2)))).toBe(0); + }); +}); diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/SumRootToLeafNumbersIterativePipeline.stories.tsx b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/SumRootToLeafNumbersIterativePipeline.stories.tsx new file mode 100644 index 00000000..2936b655 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/SumRootToLeafNumbersIterativePipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Sum Root to Leaf Numbers Iterative. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSumRootToLeafNumbersIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateSumRootToLeafNumbersIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Sum Root to Leaf Numbers Iterative", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/educational.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/educational.ts new file mode 100644 index 00000000..6586337d --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/educational.ts @@ -0,0 +1,27 @@ +import type { EducationalContent } from "@/types"; + +export const sumRootToLeafNumbersIterativeEducational: EducationalContent = { + overview: + "**Sum Root to Leaf Numbers (Iterative)** performs the same digit-path summation using an explicit stack. " + + "Each stack entry pairs a node with its running decimal number.", + + howItWorks: + "1. Push `[root, root.value]` onto the stack.\n" + + "2. Pop `[current, runningNumber]`.\n" + + "3. At a leaf, add `runningNumber` to total sum.\n" + + "4. Push children with `runningNumber * 10 + child.value`.\n" + + "5. Return total when stack empties.", + + timeAndSpaceComplexity: "**Time Complexity: `O(n)`**.\n\n**Space Complexity: `O(h)`**.", + + bestAndWorstCase: "Same as the recursive version.", + + realWorldUses: ["**Recursion-free number tree summation:** Safe for deep digit trees."], + + strengthsAndLimitations: { + strengths: ["No recursion stack overflow."], + limitations: ["Same overflow risk for very deep trees."], + }, + + whenToUseIt: "Use the iterative version for deep trees or recursion-constrained environments.", +}; diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/index.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/index.ts new file mode 100644 index 00000000..0a5407ca --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/index.ts @@ -0,0 +1,121 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sumRootToLeafNumbersIterative } from "./sources/sum-root-to-leaf-numbers-iterative.ts?fn"; +import { generateSumRootToLeafNumbersIterativeSteps } from "./step-generator"; +import type { SumRootToLeafNumbersIterativeInput } from "./step-generator"; +import { sumRootToLeafNumbersIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/sum-root-to-leaf-numbers-iterative.ts?raw"; +import pythonSource from "./sources/sum-root-to-leaf-numbers-iterative.py?raw"; +import javaSource from "./sources/SumRootToLeafNumbersIterative.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7]. Paths: 421+423+465+467=1776. */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeSumRootToLeafNumbersIterative(input: SumRootToLeafNumbersIterativeInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return sumRootToLeafNumbersIterative(toBSTNode(input.rootId)) as number; +} + +const sumRootToLeafNumbersIterativeDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.SUM_ROOT_TO_LEAF_NUMBERS_ITERATIVE!, + name: "Sum Root to Leaf Numbers (Iterative)", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Iterative stack-based number formation — each stack entry carries the node and its running decimal number", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeSumRootToLeafNumbersIterative, + generateSteps: generateSumRootToLeafNumbersIterativeSteps, + educational: sumRootToLeafNumbersIterativeEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, + }; + +registry.register(sumRootToLeafNumbersIterativeDefinition); diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/SumRootToLeafNumbersIterative.java b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/SumRootToLeafNumbersIterative.java new file mode 100644 index 00000000..dc264409 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/SumRootToLeafNumbersIterative.java @@ -0,0 +1,40 @@ +// Sum Root to Leaf Numbers (Iterative) — stack-based number formation +import java.util.ArrayDeque; +import java.util.Deque; + +class SumLeafIterativeNode { + int value; + SumLeafIterativeNode left, right; + SumLeafIterativeNode(int value) { this.value = value; } +} + +class SumRootToLeafNumbersIterative { + public int sumRootToLeafNumbersIterative(SumLeafIterativeNode root) { + if (root == null) return 0; // @step:initialize + + int totalSum = 0; // @step:initialize + Deque stack = new ArrayDeque<>(); // @step:initialize + stack.push(new Object[]{root, root.value}); // @step:initialize + + while (!stack.isEmpty()) { // @step:visit + Object[] entry = stack.pop(); // @step:visit + SumLeafIterativeNode current = (SumLeafIterativeNode) entry[0]; // @step:visit + int runningNumber = (int) entry[1]; // @step:visit + + // Leaf node — add completed number to total + if (current.left == null && current.right == null) { // @step:check-balance + totalSum += runningNumber; // @step:add-to-result + } + + if (current.right != null) { // @step:traverse-right + stack.push(new Object[]{current.right, runningNumber * 10 + current.right.value}); // @step:traverse-right + } + + if (current.left != null) { // @step:traverse-left + stack.push(new Object[]{current.left, runningNumber * 10 + current.left.value}); // @step:traverse-left + } + } + + return totalSum; // @step:complete + } +} diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/sum-root-to-leaf-numbers-iterative.py b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/sum-root-to-leaf-numbers-iterative.py new file mode 100644 index 00000000..3b6ce72e --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/sum-root-to-leaf-numbers-iterative.py @@ -0,0 +1,29 @@ +# Sum Root to Leaf Numbers (Iterative) — stack-based number formation + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def sum_root_to_leaf_numbers_iterative(root) -> int: + if root is None: # @step:initialize + return 0 + + total_sum = 0 # @step:initialize + stack = [(root, root.value)] # @step:initialize + + while stack: # @step:visit + current, running_number = stack.pop() # @step:visit + + # Leaf node — add completed number to total + if current.left is None and current.right is None: # @step:check-balance + total_sum += running_number # @step:add-to-result + + if current.right is not None: # @step:traverse-right + stack.append((current.right, running_number * 10 + current.right.value)) # @step:traverse-right + + if current.left is not None: # @step:traverse-left + stack.append((current.left, running_number * 10 + current.left.value)) # @step:traverse-left + + return total_sum # @step:complete diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/sum-root-to-leaf-numbers-iterative.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/sum-root-to-leaf-numbers-iterative.ts new file mode 100644 index 00000000..0fe36cc1 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sources/sum-root-to-leaf-numbers-iterative.ts @@ -0,0 +1,38 @@ +// Sum Root to Leaf Numbers (Iterative) — stack-based number formation + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function sumRootToLeafNumbersIterative(root: TreeNode | null): number { + if (root === null) return 0; // @step:initialize + + let totalSum = 0; // @step:initialize + const stack: Array<[TreeNode, number]> = [[root, root.value]]; // @step:initialize + + while (stack.length > 0) { + // @step:visit + const entry = stack.pop()!; // @step:visit + const [current, runningNumber] = entry; // @step:visit + + // Leaf node — add completed number to total + if (current.left === null && current.right === null) { + // @step:check-balance + totalSum += runningNumber; // @step:add-to-result + } + + if (current.right !== null) { + // @step:traverse-right + stack.push([current.right, runningNumber * 10 + current.right.value]); // @step:traverse-right + } + + if (current.left !== null) { + // @step:traverse-left + stack.push([current.left, runningNumber * 10 + current.left.value]); // @step:traverse-left + } + } + + return totalSum; // @step:complete +} diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/step-generator.test.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/step-generator.test.ts new file mode 100644 index 00000000..aaf6bdaf --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSumRootToLeafNumbersIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateSumRootToLeafNumbersIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateSumRootToLeafNumbersIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSumRootToLeafNumbersIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSumRootToLeafNumbersIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSumRootToLeafNumbersIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSumRootToLeafNumbersIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/step-generator.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/step-generator.ts new file mode 100644 index 00000000..4eda9ba5 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/step-generator.ts @@ -0,0 +1,60 @@ +/** Step generator for Sum Root to Leaf Numbers (Iterative) — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SUM_ROOT_TO_LEAF_NUMBERS_ITERATIVE!); + +export interface SumRootToLeafNumbersIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateSumRootToLeafNumbersIterativeSteps( + input: SumRootToLeafNumbersIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + const rootNode = nodeMap.get(rootId); + if (!rootNode) { + tracker.complete({ result: 0 }); + return tracker.getSteps(); + } + + const stack: Array<[string, number]> = [[rootId, rootNode.value]]; + let totalSum = 0; + + while (stack.length > 0) { + const entry = stack.pop()!; + const [nodeId, runningNumber] = entry; + const node = nodeMap.get(nodeId); + if (!node) continue; + + tracker.checkNode(nodeId, { currentNode: nodeId, value: node.value, runningNumber }); + + if (!node.leftChildId && !node.rightChildId) { + totalSum += runningNumber; + tracker.markValid(nodeId, { currentNode: nodeId, leafNumber: runningNumber, totalSum }); + } + + if (node.rightChildId) { + const rightNode = nodeMap.get(node.rightChildId); + if (rightNode) stack.push([node.rightChildId, runningNumber * 10 + rightNode.value]); + } + if (node.leftChildId) { + const leftNode = nodeMap.get(node.leftChildId); + if (leftNode) stack.push([node.leftChildId, runningNumber * 10 + leftNode.value]); + } + } + + tracker.recordResult(totalSum, { result: totalSum }); + tracker.complete({ result: totalSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sum-root-to-leaf-numbers-iterative.test.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sum-root-to-leaf-numbers-iterative.test.ts new file mode 100644 index 00000000..3fe455b0 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers-iterative/sum-root-to-leaf-numbers-iterative.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { sumRootToLeafNumbersIterative } from "./sources/sum-root-to-leaf-numbers-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("sumRootToLeafNumbersIterative", () => { + it("returns correct sum for a 7-node BST (421+423+465+467=1776)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(sumRootToLeafNumbersIterative(root)).toBe(1776); + }); + + it("returns 0 for null root", () => { + expect(sumRootToLeafNumbersIterative(null)).toBe(0); + }); + + it("returns node value for single node", () => { + expect(sumRootToLeafNumbersIterative(node(5))).toBe(5); + }); + + it("returns correct value for simple 3-node tree (12+13=25)", () => { + expect(sumRootToLeafNumbersIterative(node(1, node(2), node(3)))).toBe(25); + }); +}); diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/SumRootToLeafNumbersPipeline.stories.tsx b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/SumRootToLeafNumbersPipeline.stories.tsx new file mode 100644 index 00000000..2e614c2c --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/SumRootToLeafNumbersPipeline.stories.tsx @@ -0,0 +1,111 @@ +/** + * Storybook pipeline stories for Sum Root to Leaf Numbers. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateSumRootToLeafNumbersSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateSumRootToLeafNumbersSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Sum Root to Leaf Numbers", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree before computation begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-computation — partway through the traversal */ +export const MidComputation: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Computation complete — final result recorded */ +export const ComputationComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/educational.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/educational.ts new file mode 100644 index 00000000..e096cb2a --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/educational.ts @@ -0,0 +1,36 @@ +import type { EducationalContent } from "@/types"; + +export const sumRootToLeafNumbersEducational: EducationalContent = { + overview: + "**Sum Root to Leaf Numbers** treats each root-to-leaf path as a decimal integer " + + "and returns the sum of all such integers. For example, the path `4 → 2 → 1` forms the number 421.", + + howItWorks: + "A recursive DFS carries a `runningNumber` built by digit shifting:\n\n" + + "1. At each node: `currentNumber = runningNumber * 10 + node.value`.\n" + + "2. At a leaf, `currentNumber` is the fully formed path number — return it.\n" + + "3. Otherwise, return `dfs(left, currentNumber) + dfs(right, currentNumber)`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** — each node is visited once.\n\n" + + "**Space Complexity: `O(h)`** — call stack depth.", + + bestAndWorstCase: "Both are `O(n)` — all nodes are visited.", + + realWorldUses: [ + "**Binary coded decimal trees:** Used in compiler optimization for numeric literal trees.", + "**Path encoding:** Encoding tree paths as numbers for hashing or comparison.", + ], + + strengthsAndLimitations: { + strengths: [ + "Elegant use of digit shifting to avoid string allocation.", + "Single-pass O(n) solution.", + ], + limitations: [ + "Overflow is possible for deep trees with large digit values — consider big integer arithmetic for production.", + ], + }, + + whenToUseIt: "Use when paths in a digit tree must be interpreted as decimal numbers.", +}; diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/index.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/index.ts new file mode 100644 index 00000000..218e69c2 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/index.ts @@ -0,0 +1,120 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sumRootToLeafNumbers } from "./sources/sum-root-to-leaf-numbers.ts?fn"; +import { generateSumRootToLeafNumbersSteps } from "./step-generator"; +import type { SumRootToLeafNumbersInput } from "./step-generator"; +import { sumRootToLeafNumbersEducational } from "./educational"; + +import typescriptSource from "./sources/sum-root-to-leaf-numbers.ts?raw"; +import pythonSource from "./sources/sum-root-to-leaf-numbers.py?raw"; +import javaSource from "./sources/SumRootToLeafNumbers.java?raw"; + +/** Balanced 7-node BST: root=4, left subtree [2,1,3], right subtree [6,5,7]. Paths: 421+423+465+467=1776. */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +function executeSumRootToLeafNumbers(input: SumRootToLeafNumbersInput): number { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + return sumRootToLeafNumbers(toBSTNode(input.rootId)) as number; +} + +const sumRootToLeafNumbersDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SUM_ROOT_TO_LEAF_NUMBERS!, + name: "Sum Root to Leaf Numbers", + category: CATEGORY.TREES!, + technique: "properties", + description: + "Treats each root-to-leaf path as a decimal number (e.g., 4→2→1 = 421) and returns the sum of all such numbers", + timeComplexity: { best: "O(n)", average: "O(n)", worst: "O(n)" }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nodes: defaultNodes, rootId: "n4" }, + }, + execute: executeSumRootToLeafNumbers, + generateSteps: generateSumRootToLeafNumbersSteps, + educational: sumRootToLeafNumbersEducational, + sources: { typescript: typescriptSource, python: pythonSource, java: javaSource }, +}; + +registry.register(sumRootToLeafNumbersDefinition); diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/SumRootToLeafNumbers.java b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/SumRootToLeafNumbers.java new file mode 100644 index 00000000..586d5cd0 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/SumRootToLeafNumbers.java @@ -0,0 +1,28 @@ +// Sum Root to Leaf Numbers — recursive: treat root-to-leaf paths as numbers, sum them + +class SumLeafNode { + int value; + SumLeafNode left, right; + SumLeafNode(int value) { this.value = value; } +} + +class SumRootToLeafNumbers { + public int sumRootToLeafNumbers(SumLeafNode root) { + return dfs(root, 0); // @step:complete + } + + private int dfs(SumLeafNode node, int runningNumber) { + if (node == null) return 0; // @step:initialize + + int currentNumber = runningNumber * 10 + node.value; // @step:compute-value + + // Leaf node — this path forms a complete number + if (node.left == null && node.right == null) { // @step:visit + return currentNumber; // @step:add-to-result + } + + int leftSum = dfs(node.left, currentNumber); // @step:traverse-left + int rightSum = dfs(node.right, currentNumber); // @step:traverse-right + return leftSum + rightSum; // @step:compute-value + } +} diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/sum-root-to-leaf-numbers.py b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/sum-root-to-leaf-numbers.py new file mode 100644 index 00000000..01be44ba --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/sum-root-to-leaf-numbers.py @@ -0,0 +1,24 @@ +# Sum Root to Leaf Numbers — recursive: treat root-to-leaf paths as numbers, sum them + +class TreeNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def sum_root_to_leaf_numbers(root) -> int: + def dfs(node, running_number: int) -> int: + if node is None: # @step:initialize + return 0 + + current_number = running_number * 10 + node.value # @step:compute-value + + # Leaf node — this path forms a complete number + if node.left is None and node.right is None: # @step:visit + return current_number # @step:add-to-result + + left_sum = dfs(node.left, current_number) # @step:traverse-left + right_sum = dfs(node.right, current_number) # @step:traverse-right + return left_sum + right_sum # @step:compute-value + + return dfs(root, 0) # @step:complete diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/sum-root-to-leaf-numbers.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/sum-root-to-leaf-numbers.ts new file mode 100644 index 00000000..6235df5f --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sources/sum-root-to-leaf-numbers.ts @@ -0,0 +1,27 @@ +// Sum Root to Leaf Numbers — recursive: treat root-to-leaf paths as numbers, sum them + +interface TreeNode { + value: number; + left: TreeNode | null; + right: TreeNode | null; +} + +function sumRootToLeafNumbers(root: TreeNode | null): number { + function dfs(node: TreeNode | null, runningNumber: number): number { + if (node === null) return 0; // @step:initialize + + const currentNumber = runningNumber * 10 + node.value; // @step:compute-value + + // Leaf node — this path forms a complete number + if (node.left === null && node.right === null) { + // @step:visit + return currentNumber; // @step:add-to-result + } + + const leftSum = dfs(node.left, currentNumber); // @step:traverse-left + const rightSum = dfs(node.right, currentNumber); // @step:traverse-right + return leftSum + rightSum; // @step:compute-value + } + + return dfs(root, 0); // @step:complete +} diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/step-generator.test.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/step-generator.test.ts new file mode 100644 index 00000000..db245243 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/step-generator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateSumRootToLeafNumbersSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateSumRootToLeafNumbersSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateSumRootToLeafNumbersSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSumRootToLeafNumbersSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSumRootToLeafNumbersSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateSumRootToLeafNumbersSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSumRootToLeafNumbersSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/step-generator.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/step-generator.ts new file mode 100644 index 00000000..3fbd40e6 --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/step-generator.ts @@ -0,0 +1,53 @@ +/** Step generator for Sum Root to Leaf Numbers — produces ExecutionStep[] using TreePropertyTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreePropertyTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SUM_ROOT_TO_LEAF_NUMBERS!); + +export interface SumRootToLeafNumbersInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateSumRootToLeafNumbersSteps( + input: SumRootToLeafNumbersInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreePropertyTracker(nodes, rootId, LINE_MAP); + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + let totalSum = 0; + + function dfs(nodeId: string | null, runningNumber: number): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + const currentNumber = runningNumber * 10 + node.value; + tracker.checkNode(nodeId, { + currentNode: nodeId, + value: node.value, + runningNumber: currentNumber, + }); + + if (!node.leftChildId && !node.rightChildId) { + totalSum += currentNumber; + tracker.markValid(nodeId, { currentNode: nodeId, leafNumber: currentNumber, totalSum }); + return; + } + + dfs(node.leftChildId, currentNumber); + dfs(node.rightChildId, currentNumber); + } + + dfs(rootId, 0); + tracker.recordResult(totalSum, { result: totalSum }); + tracker.complete({ result: totalSum }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sum-root-to-leaf-numbers.test.ts b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sum-root-to-leaf-numbers.test.ts new file mode 100644 index 00000000..fa85de7d --- /dev/null +++ b/src/algorithms/trees/properties/sum-root-to-leaf-numbers/sum-root-to-leaf-numbers.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { sumRootToLeafNumbers } from "./sources/sum-root-to-leaf-numbers.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("sumRootToLeafNumbers", () => { + it("returns correct sum for a 7-node BST (421+423+465+467=1776)", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(sumRootToLeafNumbers(root)).toBe(1776); + }); + + it("returns 0 for null root", () => { + expect(sumRootToLeafNumbers(null)).toBe(0); + }); + + it("returns node value for single node", () => { + expect(sumRootToLeafNumbers(node(5))).toBe(5); + }); + + it("returns correct value for simple 3-node tree (12+13=25)", () => { + expect(sumRootToLeafNumbers(node(1, node(2), node(3)))).toBe(25); + }); +}); diff --git a/src/algorithms/trees/traversal/boundary-traversal/BoundaryTraversalPipeline.stories.tsx b/src/algorithms/trees/traversal/boundary-traversal/BoundaryTraversalPipeline.stories.tsx new file mode 100644 index 00000000..3a961a98 --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/BoundaryTraversalPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Boundary Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBoundaryTraversalSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Boundary Traversal", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — visiting leaf nodes along the bottom */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — full boundary visited counterclockwise */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/boundary-traversal/boundary-traversal.test.ts b/src/algorithms/trees/traversal/boundary-traversal/boundary-traversal.test.ts new file mode 100644 index 00000000..c92f9904 --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/boundary-traversal.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { boundaryTraversal } from "./sources/boundary-traversal.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("boundaryTraversal", () => { + it("traverses the boundary of a balanced 7-node BST", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + // root=4, left boundary=2, leaves=1,3,5,7, right boundary=6 + expect(boundaryTraversal(root)).toEqual([4, 2, 1, 3, 5, 7, 6]); + }); + + it("returns an empty array for a null root", () => { + expect(boundaryTraversal(null)).toEqual([]); + }); + + it("handles a single-node tree (leaf root)", () => { + expect(boundaryTraversal(node(42))).toEqual([42]); + }); + + it("handles a tree with only a right child", () => { + const root = node(5, null, node(8)); + expect(boundaryTraversal(root)).toEqual([5, 8]); + }); + + it("handles a tree with only a left child", () => { + const root = node(5, node(3)); + expect(boundaryTraversal(root)).toEqual([5, 3]); + }); +}); diff --git a/src/algorithms/trees/traversal/boundary-traversal/educational.ts b/src/algorithms/trees/traversal/boundary-traversal/educational.ts new file mode 100644 index 00000000..8c789da7 --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const boundaryTraversalEducational: EducationalContent = { + overview: + "**Boundary Traversal** visits the outer edge of a binary tree counterclockwise: first the root, then the left boundary (top-down, excluding leaves), then all leaf nodes (left-to-right), and finally the right boundary (bottom-up, excluding leaves). The result traces the perimeter of the tree.", + + howItWorks: + "The algorithm has three distinct phases:\n\n" + + "**Phase 1 — Root:** Visit the root (if it's not a leaf).\n\n" + + "**Phase 2 — Left Boundary:** Starting at the root's left child, walk down following left children preferentially. Skip leaf nodes.\n\n" + + "**Phase 3 — Leaves:** Traverse the entire tree recursively, visiting only leaf nodes in left-to-right order.\n\n" + + "**Phase 4 — Right Boundary:** Starting at the root's right child, walk down following right children preferentially (excluding leaves), then add values bottom-up on the way back.\n\n" + + "### Example: Boundary Traversal on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Result:** `4 → 2 → 1 → 3 → 5 → 7 → 6` — the outer perimeter counterclockwise.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each node is visited at most once across all three phases.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "Recursion depth is bounded by tree height — `O(log n)` for balanced, `O(n)` for degenerate.", + + bestAndWorstCase: + "**Best case** is a balanced tree — `O(log n)` recursion depth.\n\n" + + "**Worst case** is a skewed tree — `O(n)` recursion depth and right-boundary phase touches all nodes.\n\n" + + "Time is always `O(n)` regardless of shape.", + + realWorldUses: [ + "**Tree visualization outline:** Boundary traversal defines the visible outer edge of a tree for outline rendering.", + "**Convex hull analogy:** Like computing the convex hull of points, boundary traversal extracts the outermost structure.", + "**Interview problems:** Boundary traversal is a classic medium-difficulty interview problem testing multi-phase tree reasoning.", + "**Tree perimeter metrics:** Computing the length of a tree's boundary is useful in certain spatial analysis tasks.", + ], + + strengthsAndLimitations: { + strengths: [ + "Provides the complete boundary of a tree in counterclockwise order.", + "Three-phase decomposition makes the algorithm easy to understand and implement.", + "O(n) time ensures scalability.", + ], + limitations: [ + "More complex than simple traversals — requires careful handling of leaf and non-leaf cases.", + "Right boundary is collected bottom-up (post-order), adding complexity.", + "Not directly useful for standard tree operations like search, insert, or delete.", + ], + }, + + whenToUseIt: + "Use boundary traversal when you need the perimeter nodes of a tree — for visualization outlines, interview problems, or spatial boundary calculations. For interior processing, use in-order, pre-order, or level-order traversal instead.", +}; diff --git a/src/algorithms/trees/traversal/boundary-traversal/index.ts b/src/algorithms/trees/traversal/boundary-traversal/index.ts new file mode 100644 index 00000000..9f2625ca --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { boundaryTraversal } from "./sources/boundary-traversal.ts?fn"; +import { generateBoundaryTraversalSteps } from "./step-generator"; +import type { BoundaryTraversalInput } from "./step-generator"; +import { boundaryTraversalEducational } from "./educational"; + +import typescriptSource from "./sources/boundary-traversal.ts?raw"; +import pythonSource from "./sources/boundary-traversal.py?raw"; +import javaSource from "./sources/BoundaryTraversal.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute boundary traversal using the ?fn source */ +function executeBoundaryTraversal(input: BoundaryTraversalInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return boundaryTraversal(root) as number[]; +} + +const boundaryTraversalDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BOUNDARY_TRAVERSAL!, + name: "Boundary Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "Visits the outer perimeter of a tree counterclockwise: root, left boundary, leaves, right boundary", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeBoundaryTraversal, + generateSteps: generateBoundaryTraversalSteps, + educational: boundaryTraversalEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(boundaryTraversalDefinition); diff --git a/src/algorithms/trees/traversal/boundary-traversal/sources/BoundaryTraversal.java b/src/algorithms/trees/traversal/boundary-traversal/sources/BoundaryTraversal.java new file mode 100644 index 00000000..b86e62cd --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/sources/BoundaryTraversal.java @@ -0,0 +1,58 @@ +// Boundary Traversal — left boundary + leaf nodes + right boundary (counterclockwise) +import java.util.ArrayList; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BoundaryTraversal { + private boolean isLeaf(BSTNode node) { + return node.left == null && node.right == null; + } + + private void addLeftBoundary(BSTNode node, List result) { // @step:traverse-left + if (node == null || isLeaf(node)) return; // @step:traverse-left + result.add(node.value); // @step:traverse-left + if (node.left != null) { // @step:traverse-left + addLeftBoundary(node.left, result); // @step:traverse-left + } else { // @step:traverse-left + addLeftBoundary(node.right, result); // @step:traverse-left + } + } + + private void addLeaves(BSTNode node, List result) { // @step:visit + if (node == null) return; // @step:visit + if (isLeaf(node)) { // @step:visit + result.add(node.value); // @step:visit + return; // @step:visit + } + addLeaves(node.left, result); // @step:visit + addLeaves(node.right, result); // @step:visit + } + + private void addRightBoundary(BSTNode node, List result) { // @step:traverse-right + if (node == null || isLeaf(node)) return; // @step:traverse-right + if (node.right != null) { // @step:traverse-right + addRightBoundary(node.right, result); // @step:traverse-right + } else { // @step:traverse-right + addRightBoundary(node.left, result); // @step:traverse-right + } + result.add(node.value); // @step:traverse-right + } + + public List boundaryTraversal(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + if (!isLeaf(root)) result.add(root.value); // @step:initialize + + addLeftBoundary(root.left, result); // @step:traverse-left + addLeaves(root, result); // @step:visit + addRightBoundary(root.right, result); // @step:traverse-right + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/boundary-traversal/sources/boundary-traversal.py b/src/algorithms/trees/traversal/boundary-traversal/sources/boundary-traversal.py new file mode 100644 index 00000000..2a914a6f --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/sources/boundary-traversal.py @@ -0,0 +1,51 @@ +# Boundary Traversal — left boundary + leaf nodes + right boundary (counterclockwise) + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def is_leaf(node) -> bool: + return node.left is None and node.right is None + +def add_left_boundary(node, result: list) -> None: # @step:traverse-left + if node is None or is_leaf(node): # @step:traverse-left + return + result.append(node.value) # @step:traverse-left + if node.left is not None: # @step:traverse-left + add_left_boundary(node.left, result) # @step:traverse-left + else: # @step:traverse-left + add_left_boundary(node.right, result) # @step:traverse-left + +def add_leaves(node, result: list) -> None: # @step:visit + if node is None: # @step:visit + return + if is_leaf(node): # @step:visit + result.append(node.value) # @step:visit + return + add_leaves(node.left, result) # @step:visit + add_leaves(node.right, result) # @step:visit + +def add_right_boundary(node, result: list) -> None: # @step:traverse-right + if node is None or is_leaf(node): # @step:traverse-right + return + if node.right is not None: # @step:traverse-right + add_right_boundary(node.right, result) # @step:traverse-right + else: # @step:traverse-right + add_right_boundary(node.left, result) # @step:traverse-right + result.append(node.value) # @step:traverse-right + +def boundary_traversal(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + if not is_leaf(root): # @step:initialize + result.append(root.value) # @step:initialize + + add_left_boundary(root.left, result) # @step:traverse-left + add_leaves(root, result) # @step:visit + add_right_boundary(root.right, result) # @step:traverse-right + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/boundary-traversal/sources/boundary-traversal.ts b/src/algorithms/trees/traversal/boundary-traversal/sources/boundary-traversal.ts new file mode 100644 index 00000000..66e06ed1 --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/sources/boundary-traversal.ts @@ -0,0 +1,62 @@ +// Boundary Traversal — left boundary + leaf nodes + right boundary (counterclockwise) + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function isLeaf(node: BSTNode): boolean { + return node.left === null && node.right === null; +} + +function addLeftBoundary(node: BSTNode | null, result: number[]): void { + // @step:traverse-left + if (node === null || isLeaf(node)) return; // @step:traverse-left + result.push(node.value); // @step:traverse-left + if (node.left !== null) { + // @step:traverse-left + addLeftBoundary(node.left, result); // @step:traverse-left + } else { + // @step:traverse-left + addLeftBoundary(node.right, result); // @step:traverse-left + } +} + +function addLeaves(node: BSTNode | null, result: number[]): void { + // @step:visit + if (node === null) return; // @step:visit + if (isLeaf(node)) { + // @step:visit + result.push(node.value); // @step:visit + return; // @step:visit + } + addLeaves(node.left, result); // @step:visit + addLeaves(node.right, result); // @step:visit +} + +function addRightBoundary(node: BSTNode | null, result: number[]): void { + // @step:traverse-right + if (node === null || isLeaf(node)) return; // @step:traverse-right + if (node.right !== null) { + // @step:traverse-right + addRightBoundary(node.right, result); // @step:traverse-right + } else { + // @step:traverse-right + addRightBoundary(node.left, result); // @step:traverse-right + } + result.push(node.value); // @step:traverse-right (added after recursion for bottom-up) +} + +function boundaryTraversal(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + if (!isLeaf(root)) result.push(root.value); // @step:initialize + + addLeftBoundary(root.left, result); // @step:traverse-left + addLeaves(root, result); // @step:visit + addRightBoundary(root.right, result); // @step:traverse-right + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/boundary-traversal/step-generator.test.ts b/src/algorithms/trees/traversal/boundary-traversal/step-generator.test.ts new file mode 100644 index 00000000..9f169c84 --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/step-generator.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBoundaryTraversalSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBoundaryTraversalSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 unique boundary nodes", () => { + const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const uniqueValues = new Set(visitSteps.map((step) => step.variables["value"] as number)); + expect(uniqueValues.size).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateBoundaryTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/boundary-traversal/step-generator.ts b/src/algorithms/trees/traversal/boundary-traversal/step-generator.ts new file mode 100644 index 00000000..8109c81a --- /dev/null +++ b/src/algorithms/trees/traversal/boundary-traversal/step-generator.ts @@ -0,0 +1,102 @@ +/** Step generator for Boundary Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BOUNDARY_TRAVERSAL!); + +export interface BoundaryTraversalInput { + nodes: TreeNode[]; + rootId: string; +} + +function isLeafNode(node: TreeNode): boolean { + return node.leftChildId === null && node.rightChildId === null; +} + +export function generateBoundaryTraversalSteps(input: BoundaryTraversalInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, phase: "start" }); + + const rootNode = nodeMap.get(rootId); + if (!rootNode) { + tracker.complete({ result: 0 }); + return tracker.getSteps(); + } + + // Visit root if not a leaf + if (!isLeafNode(rootNode)) { + tracker.visitNode(rootId, rootNode.value, { + currentNode: rootId, + value: rootNode.value, + phase: "root", + }); + } + + // Left boundary (top-down, excluding leaves) + function addLeftBoundary(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node || isLeafNode(node)) return; + tracker.traverseLeft(nodeId, { currentNode: nodeId, phase: "left-boundary" }); + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + phase: "left-boundary", + }); + if (node.leftChildId) { + addLeftBoundary(node.leftChildId); + } else { + addLeftBoundary(node.rightChildId); + } + } + + // Leaf nodes (left to right) + function addLeaves(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + if (isLeafNode(node)) { + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + phase: "leaves", + }); + return; + } + addLeaves(node.leftChildId); + addLeaves(node.rightChildId); + } + + // Right boundary (bottom-up, excluding leaves) + function addRightBoundary(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node || isLeafNode(node)) return; + if (node.rightChildId) { + addRightBoundary(node.rightChildId); + } else { + addRightBoundary(node.leftChildId); + } + tracker.traverseRight(nodeId, { currentNode: nodeId, phase: "right-boundary" }); + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + phase: "right-boundary", + }); + } + + addLeftBoundary(rootNode.leftChildId); + addLeaves(rootId); + addRightBoundary(rootNode.rightChildId); + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/BSTInorderIterativePipeline.stories.tsx b/src/algorithms/trees/traversal/bst-inorder-iterative/BSTInorderIterativePipeline.stories.tsx new file mode 100644 index 00000000..ee9ab72a --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/BSTInorderIterativePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the BST In-Order Traversal (Iterative) algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstInorderIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST In-Order (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal with some nodes visited */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited in sorted order */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/bst-inorder-iterative.test.ts b/src/algorithms/trees/traversal/bst-inorder-iterative/bst-inorder-iterative.test.ts new file mode 100644 index 00000000..c009215d --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/bst-inorder-iterative.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { bstInorderIterative } from "./sources/bst-inorder-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstInorderIterative", () => { + it("traverses a balanced 7-node BST in sorted order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstInorderIterative(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("returns an empty array for a null root", () => { + expect(bstInorderIterative(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(bstInorderIterative(node(42))).toEqual([42]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(bstInorderIterative(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3, null, node(4, null, node(5))))); + expect(bstInorderIterative(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(bstInorderIterative(node(5, node(3)))).toEqual([3, 5]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(bstInorderIterative(node(5, null, node(8)))).toEqual([5, 8]); + }); +}); diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/educational.ts b/src/algorithms/trees/traversal/bst-inorder-iterative/educational.ts new file mode 100644 index 00000000..5213ce9c --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const bstInorderIterativeEducational: EducationalContent = { + overview: + "**BST In-Order Traversal (Iterative)** visits every node in ascending sorted order using an explicit stack instead of the call stack. It follows the same LNR pattern (left → root → right) as the recursive version but avoids recursion-related stack overflow risks for deep trees.", + + howItWorks: + "The algorithm uses a pointer `current` and an explicit stack:\n\n" + + "1. **Push left chain** — walk left from `current`, pushing each node onto the stack.\n" + + "2. **Pop and visit** — pop the top node, record its value.\n" + + "3. **Move right** — set `current` to the popped node's right child and repeat.\n\n" + + "The outer loop continues while `current` is non-null or the stack is non-empty, ensuring every node is visited.\n\n" + + "### Example: Iterative In-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Visit order:** `1 → 2 → 3 → 4 → 5 → 6 → 7` — identical to recursive in-order.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is pushed and popped from the stack exactly once.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The explicit stack holds at most `h` nodes — the depth of the left spine at any moment. For a balanced BST, `h = O(log n)`; for a degenerate tree, `h = O(n)`.", + + bestAndWorstCase: + "**Best case** is a perfectly balanced BST — the stack never exceeds `O(log n)` depth.\n\n" + + "**Worst case** is a left-skewed tree — the stack grows to `O(n)` depth, matching the degenerate recursive case but using heap memory instead of the call stack.\n\n" + + "The iterative version is always preferred when working with large, potentially unbalanced trees in production environments.", + + realWorldUses: [ + "**Database cursor iteration:** Iterative in-order traversal underlies range scans in B-tree cursor implementations, which must not blow the call stack on production data.", + "**BST flattening:** Convert a BST to a sorted doubly-linked list without recursion depth limits.", + "**Streaming sorted output:** Produce sorted elements one at a time from a large BST without materializing all elements first.", + "**Tree serialization:** Serialize a BST to a sorted sequence for persistence or network transfer.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — safe for arbitrarily deep trees without stack overflow risk.", + "Produces the same sorted output as the recursive variant.", + "Explicit stack enables easy pausing, resuming, or interleaving with other operations.", + ], + limitations: [ + "Slightly more complex to read and reason about than the recursive implementation.", + "Still uses O(h) heap space for the explicit stack — not O(1) space.", + "For O(1) space in-order traversal, Morris Traversal is required instead.", + ], + }, + + whenToUseIt: + "Use the iterative in-order traversal when you need sorted BST output in production environments where call-stack depth is a concern, or when you need explicit control over the traversal state (e.g., pausing mid-traversal). For O(1) extra space, use Morris Traversal instead.", +}; diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/index.ts b/src/algorithms/trees/traversal/bst-inorder-iterative/index.ts new file mode 100644 index 00000000..5618b37f --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstInorderIterative } from "./sources/bst-inorder-iterative.ts?fn"; +import { generateBstInorderIterativeSteps } from "./step-generator"; +import type { BstInorderIterativeInput } from "./step-generator"; +import { bstInorderIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-inorder-iterative.ts?raw"; +import pythonSource from "./sources/bst-inorder-iterative.py?raw"; +import javaSource from "./sources/BSTInorderIterative.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute iterative BST in-order using the ?fn source — visits nodes in ascending order */ +function executeBstInorderIterative(input: BstInorderIterativeInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return bstInorderIterative(root) as number[]; +} + +const bstInorderIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_INORDER_ITERATIVE!, + name: "BST In-Order Traversal (Iterative)", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "An iterative LNR traversal using an explicit stack — visits nodes in ascending sorted order without recursion", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeBstInorderIterative, + generateSteps: generateBstInorderIterativeSteps, + educational: bstInorderIterativeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(bstInorderIterativeDefinition); diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/sources/BSTInorderIterative.java b/src/algorithms/trees/traversal/bst-inorder-iterative/sources/BSTInorderIterative.java new file mode 100644 index 00000000..52e29627 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/sources/BSTInorderIterative.java @@ -0,0 +1,36 @@ +// BST In-Order Traversal (Iterative) — LNR using an explicit stack +import java.util.ArrayList; +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTInorderIterative { + public List bstInorderIterative(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + Deque stack = new ArrayDeque<>(); // @step:initialize + BSTNode current = root; // @step:initialize + + while (current != null || !stack.isEmpty()) { // @step:initialize + // Push all left children onto the stack + while (current != null) { // @step:push-to-stack + stack.push(current); // @step:push-to-stack + current = current.left; // @step:traverse-left + } + + // Pop the top node and visit it + current = stack.pop(); // @step:pop-from-stack + result.add(current.value); // @step:visit + + // Move to right subtree + current = current.right; // @step:traverse-right + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/sources/bst-inorder-iterative.py b/src/algorithms/trees/traversal/bst-inorder-iterative/sources/bst-inorder-iterative.py new file mode 100644 index 00000000..7786f14d --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/sources/bst-inorder-iterative.py @@ -0,0 +1,27 @@ +# BST In-Order Traversal (Iterative) — LNR using an explicit stack + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_inorder_iterative(root) -> list: + result = [] # @step:initialize + stack = [] # @step:initialize + current = root # @step:initialize + + while current is not None or len(stack) > 0: # @step:initialize + # Push all left children onto the stack + while current is not None: # @step:push-to-stack + stack.append(current) # @step:push-to-stack + current = current.left # @step:traverse-left + + # Pop the top node and visit it + current = stack.pop() # @step:pop-from-stack + result.append(current.value) # @step:visit + + # Move to right subtree + current = current.right # @step:traverse-right + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/sources/bst-inorder-iterative.ts b/src/algorithms/trees/traversal/bst-inorder-iterative/sources/bst-inorder-iterative.ts new file mode 100644 index 00000000..1f88da11 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/sources/bst-inorder-iterative.ts @@ -0,0 +1,32 @@ +// BST In-Order Traversal (Iterative) — LNR using an explicit stack + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstInorderIterative(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + const stack: BSTNode[] = []; // @step:initialize + let current: BSTNode | null = root; // @step:initialize + + while (current !== null || stack.length > 0) { + // @step:initialize + // Push all left children onto the stack + while (current !== null) { + // @step:push-to-stack + stack.push(current); // @step:push-to-stack + current = current.left; // @step:traverse-left + } + + // Pop the top node and visit it + current = stack.pop()!; // @step:pop-from-stack + result.push(current.value); // @step:visit + + // Move to right subtree + current = current.right; // @step:traverse-right + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/step-generator.test.ts b/src/algorithms/trees/traversal/bst-inorder-iterative/step-generator.test.ts new file mode 100644 index 00000000..e2b0daa7 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/step-generator.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstInorderIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstInorderIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits nodes in sorted ascending order", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("has incrementing step indices", () => { + const steps = generateBstInorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/bst-inorder-iterative/step-generator.ts b/src/algorithms/trees/traversal/bst-inorder-iterative/step-generator.ts new file mode 100644 index 00000000..728dfa23 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-inorder-iterative/step-generator.ts @@ -0,0 +1,60 @@ +/** Step generator for BST In-Order Traversal (Iterative) — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_INORDER_ITERATIVE!); + +export interface BstInorderIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstInorderIterativeSteps(input: BstInorderIterativeInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, stack: [], current: rootId }); + + const stack: string[] = []; + let currentId: string | null = rootId; + + while (currentId !== null || stack.length > 0) { + // Push all left children onto the stack + while (currentId !== null) { + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + stack.push(currentId); + tracker.traverseLeft(currentId, { currentNode: currentId, stackSize: stack.length }); + currentId = currentNode.leftChildId; + } + + // Pop the top node + const poppedId = stack.pop(); + if (!poppedId) break; + const poppedNode = nodeMap.get(poppedId); + if (!poppedNode) break; + + tracker.visitNode(poppedId, poppedNode.value, { + currentNode: poppedId, + value: poppedNode.value, + stackSize: stack.length, + }); + + // Move to right subtree + if (poppedNode.rightChildId) { + tracker.traverseRight(poppedId, { currentNode: poppedId, direction: "right" }); + currentId = poppedNode.rightChildId; + } else { + currentId = null; + } + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/BSTPostorderIterativePipeline.stories.tsx b/src/algorithms/trees/traversal/bst-postorder-iterative/BSTPostorderIterativePipeline.stories.tsx new file mode 100644 index 00000000..724544f4 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/BSTPostorderIterativePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the BST Post-Order Traversal (Iterative) algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstPostorderIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Post-Order (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — phase 1 building stack 2 */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited in post-order */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/bst-postorder-iterative.test.ts b/src/algorithms/trees/traversal/bst-postorder-iterative/bst-postorder-iterative.test.ts new file mode 100644 index 00000000..b02b2527 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/bst-postorder-iterative.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { bstPostorderIterative } from "./sources/bst-postorder-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstPostorderIterative", () => { + it("traverses a balanced 7-node BST in post-order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstPostorderIterative(root)).toEqual([1, 3, 2, 5, 7, 6, 4]); + }); + + it("returns an empty array for a null root", () => { + expect(bstPostorderIterative(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(bstPostorderIterative(node(42))).toEqual([42]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(bstPostorderIterative(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3, null, node(4, null, node(5))))); + expect(bstPostorderIterative(root)).toEqual([5, 4, 3, 2, 1]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(bstPostorderIterative(node(5, node(3)))).toEqual([3, 5]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(bstPostorderIterative(node(5, null, node(8)))).toEqual([8, 5]); + }); +}); diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/educational.ts b/src/algorithms/trees/traversal/bst-postorder-iterative/educational.ts new file mode 100644 index 00000000..d37a9c7b --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/educational.ts @@ -0,0 +1,61 @@ +import type { EducationalContent } from "@/types"; + +export const bstPostorderIterativeEducational: EducationalContent = { + overview: + "**BST Post-Order Traversal (Iterative)** visits every node in LRN order using two explicit stacks. Stack 1 builds a reversed post-order sequence; stack 2 accumulates it in the correct post-order for final output. This avoids both recursion and the complexity of a single-stack post-order approach.", + + howItWorks: + "The algorithm uses two stacks in two phases:\n\n" + + "**Phase 1 — Build reversed post-order on stack 2:**\n" + + "1. Push root onto stack 1.\n" + + "2. Pop from stack 1, push to stack 2.\n" + + "3. Push left child, then right child onto stack 1.\n" + + "4. Repeat until stack 1 is empty.\n\n" + + "**Phase 2 — Visit nodes in post-order:**\n" + + "5. Pop from stack 2 and record each value — this is the LRN order.\n\n" + + "### Example: Iterative Post-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Visit order:** `1 → 3 → 2 → 5 → 7 → 6 → 4` — identical to recursive post-order.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is pushed/popped from each stack exactly once.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "Both stacks together hold at most `n` nodes. Unlike in-order and pre-order iterative variants, post-order iterative uses `O(n)` space because all nodes must be buffered in stack 2 before any visits begin.", + + bestAndWorstCase: + "**Best case** — balanced tree: stack 1 never exceeds `O(log n)` elements at once, but stack 2 always accumulates all `n` nodes.\n\n" + + "**Worst case** — degenerate tree: same `O(n)` space for both stacks.\n\n" + + "This `O(n)` space usage is the trade-off for avoiding recursive call stack depth.", + + realWorldUses: [ + "**Safe tree deletion in garbage collectors:** Post-order ensures children are freed before parents, preventing dangling pointer issues.", + "**AST bottom-up evaluation without recursion:** Language interpreters evaluate expression trees iteratively when recursion depth is a concern.", + "**Parallel tree processing:** The two-stack approach naturally separates traversal planning from processing, enabling pipelining.", + "**Streaming post-order output:** Phase 2 can be made lazy — pop from stack 2 on demand rather than all at once.", + ], + + strengthsAndLimitations: { + strengths: [ + "No recursion — safe for arbitrarily deep trees without call-stack overflow.", + "Produces correct LRN post-order, suitable for deletion and bottom-up evaluation.", + "Conceptually clean two-phase algorithm compared to single-stack post-order variants.", + ], + limitations: [ + "Uses O(n) space for stack 2, unlike in-order/pre-order iterative variants which use O(h).", + "All nodes must be processed in phase 1 before any visits begin — not streaming-friendly as written.", + "More complex than iterative pre-order; requires managing two stacks.", + ], + }, + + whenToUseIt: + "Use iterative post-order when you need bottom-up processing without recursion, and O(n) extra memory is acceptable. For tighter memory bounds on deep trees, a single-stack approach (using a 'last visited' pointer) reduces space to O(h). For sorted output, use iterative in-order instead.", +}; diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/index.ts b/src/algorithms/trees/traversal/bst-postorder-iterative/index.ts new file mode 100644 index 00000000..14e81cba --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstPostorderIterative } from "./sources/bst-postorder-iterative.ts?fn"; +import { generateBstPostorderIterativeSteps } from "./step-generator"; +import type { BstPostorderIterativeInput } from "./step-generator"; +import { bstPostorderIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-postorder-iterative.ts?raw"; +import pythonSource from "./sources/bst-postorder-iterative.py?raw"; +import javaSource from "./sources/BSTPostorderIterative.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute iterative BST post-order using the ?fn source */ +function executeBstPostorderIterative(input: BstPostorderIterativeInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return bstPostorderIterative(root) as number[]; +} + +const bstPostorderIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_POSTORDER_ITERATIVE!, + name: "BST Post-Order Traversal (Iterative)", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "An iterative LRN traversal using two stacks — visits children before root without recursion", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeBstPostorderIterative, + generateSteps: generateBstPostorderIterativeSteps, + educational: bstPostorderIterativeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(bstPostorderIterativeDefinition); diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/sources/BSTPostorderIterative.java b/src/algorithms/trees/traversal/bst-postorder-iterative/sources/BSTPostorderIterative.java new file mode 100644 index 00000000..8fd6b455 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/sources/BSTPostorderIterative.java @@ -0,0 +1,43 @@ +// BST Post-Order Traversal (Iterative) — LRN using two stacks +import java.util.ArrayList; +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTPostorderIterative { + public List bstPostorderIterative(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + Deque stack1 = new ArrayDeque<>(); // @step:initialize + Deque stack2 = new ArrayDeque<>(); // @step:initialize + stack1.push(root); // @step:initialize + + // Phase 1: push nodes onto stack2 in reverse post-order + while (!stack1.isEmpty()) { // @step:push-to-stack + BSTNode node = stack1.pop(); // @step:pop-from-stack + stack2.push(node); // @step:push-to-stack + + if (node.left != null) { // @step:traverse-left + stack1.push(node.left); // @step:traverse-left + } + if (node.right != null) { // @step:traverse-right + stack1.push(node.right); // @step:traverse-right + } + } + + // Phase 2: pop stack2 in post-order and visit each node + while (!stack2.isEmpty()) { // @step:visit + BSTNode node = stack2.pop(); // @step:pop-from-stack + result.add(node.value); // @step:visit + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/sources/bst-postorder-iterative.py b/src/algorithms/trees/traversal/bst-postorder-iterative/sources/bst-postorder-iterative.py new file mode 100644 index 00000000..6837db47 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/sources/bst-postorder-iterative.py @@ -0,0 +1,32 @@ +# BST Post-Order Traversal (Iterative) — LRN using two stacks + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_postorder_iterative(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + stack1 = [root] # @step:initialize + stack2 = [] # @step:initialize + + # Phase 1: push nodes onto stack2 in reverse post-order + while len(stack1) > 0: # @step:push-to-stack + node = stack1.pop() # @step:pop-from-stack + stack2.append(node) # @step:push-to-stack + + if node.left is not None: # @step:traverse-left + stack1.append(node.left) # @step:traverse-left + if node.right is not None: # @step:traverse-right + stack1.append(node.right) # @step:traverse-right + + # Phase 2: pop stack2 in post-order and visit each node + while len(stack2) > 0: # @step:visit + node = stack2.pop() # @step:pop-from-stack + result.append(node.value) # @step:visit + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/sources/bst-postorder-iterative.ts b/src/algorithms/trees/traversal/bst-postorder-iterative/sources/bst-postorder-iterative.ts new file mode 100644 index 00000000..53de6e53 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/sources/bst-postorder-iterative.ts @@ -0,0 +1,40 @@ +// BST Post-Order Traversal (Iterative) — LRN using two stacks + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstPostorderIterative(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + const stack1: BSTNode[] = [root]; // @step:initialize + const stack2: BSTNode[] = []; // @step:initialize + + // Phase 1: push nodes onto stack2 in reverse post-order + while (stack1.length > 0) { + // @step:push-to-stack + const node = stack1.pop()!; // @step:pop-from-stack + stack2.push(node); // @step:push-to-stack + + if (node.left !== null) { + // @step:traverse-left + stack1.push(node.left); // @step:traverse-left + } + if (node.right !== null) { + // @step:traverse-right + stack1.push(node.right); // @step:traverse-right + } + } + + // Phase 2: pop stack2 in post-order and visit each node + while (stack2.length > 0) { + // @step:visit + const node = stack2.pop()!; // @step:pop-from-stack + result.push(node.value); // @step:visit + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/step-generator.test.ts b/src/algorithms/trees/traversal/bst-postorder-iterative/step-generator.test.ts new file mode 100644 index 00000000..b1330625 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/step-generator.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstPostorderIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstPostorderIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits nodes in post-order (LRN) sequence", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([1, 3, 2, 5, 7, 6, 4]); + }); + + it("has incrementing step indices", () => { + const steps = generateBstPostorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/bst-postorder-iterative/step-generator.ts b/src/algorithms/trees/traversal/bst-postorder-iterative/step-generator.ts new file mode 100644 index 00000000..0ed7df79 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder-iterative/step-generator.ts @@ -0,0 +1,73 @@ +/** Step generator for BST Post-Order Traversal (Iterative) — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_POSTORDER_ITERATIVE!); + +export interface BstPostorderIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstPostorderIterativeSteps( + input: BstPostorderIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, stack1: [rootId], stack2: [] }); + + // Phase 1: build post-order sequence onto stack2 + const stack1: string[] = [rootId]; + const stack2: string[] = []; + + while (stack1.length > 0) { + const poppedId = stack1.pop(); + if (!poppedId) break; + const poppedNode = nodeMap.get(poppedId); + if (!poppedNode) break; + + stack2.push(poppedId); + tracker.traverseLeft(poppedId, { + currentNode: poppedId, + stack1Size: stack1.length, + stack2Size: stack2.length, + }); + + if (poppedNode.leftChildId) { + stack1.push(poppedNode.leftChildId); + } + + if (poppedNode.rightChildId) { + stack1.push(poppedNode.rightChildId); + tracker.traverseRight(poppedId, { + currentNode: poppedId, + stack1Size: stack1.length, + stack2Size: stack2.length, + }); + } + } + + // Phase 2: pop stack2 to visit nodes in post-order + while (stack2.length > 0) { + const visitId = stack2.pop(); + if (!visitId) break; + const visitNode = nodeMap.get(visitId); + if (!visitNode) break; + + tracker.visitNode(visitId, visitNode.value, { + currentNode: visitId, + value: visitNode.value, + stack2Size: stack2.length, + }); + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/bst-postorder/BSTPostorderPipeline.stories.tsx b/src/algorithms/trees/traversal/bst-postorder/BSTPostorderPipeline.stories.tsx new file mode 100644 index 00000000..9d6908cc --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/BSTPostorderPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the BST Post-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstPostorderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Post-Order", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal with some nodes visited */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited in post-order, root last */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/bst-postorder/bst-postorder.test.ts b/src/algorithms/trees/traversal/bst-postorder/bst-postorder.test.ts new file mode 100644 index 00000000..9b7fdf69 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/bst-postorder.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { bstPostorder } from "./sources/bst-postorder.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstPostorder", () => { + it("traverses a balanced 7-node BST in post-order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstPostorder(root)).toEqual([1, 3, 2, 5, 7, 6, 4]); + }); + + it("returns an empty array for a null root", () => { + expect(bstPostorder(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(bstPostorder(node(42))).toEqual([42]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(bstPostorder(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3, null, node(4, null, node(5))))); + expect(bstPostorder(root)).toEqual([5, 4, 3, 2, 1]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(bstPostorder(node(5, node(3)))).toEqual([3, 5]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(bstPostorder(node(5, null, node(8)))).toEqual([8, 5]); + }); +}); diff --git a/src/algorithms/trees/traversal/bst-postorder/educational.ts b/src/algorithms/trees/traversal/bst-postorder/educational.ts new file mode 100644 index 00000000..996ee7da --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/educational.ts @@ -0,0 +1,57 @@ +import type { EducationalContent } from "@/types"; + +export const bstPostorderEducational: EducationalContent = { + overview: + "**BST Post-Order Traversal** visits every node in LRN order: **left subtree → right subtree → root**. The root is visited last, after all its descendants have been processed. This bottom-up pattern is essential for deletion, memory deallocation, and expression evaluation.", + + howItWorks: + "The algorithm uses a recursive helper that processes each node in three steps:\n\n" + + "1. **Recurse left** — visit all nodes in the left subtree.\n" + + "2. **Recurse right** — visit all nodes in the right subtree.\n" + + "3. **Visit root** — record the current node's value after its children.\n\n" + + "### Example: Post-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Visit order:** `1 → 3 → 2 → 5 → 7 → 6 → 4` — leaves before parents, root last.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The call stack depth equals the tree height. `O(log n)` for balanced trees, `O(n)` for degenerate.", + + bestAndWorstCase: + "**Best case** is a perfectly balanced BST — call stack depth is `O(log n)`.\n\n" + + "**Worst case** is a degenerate (chain) tree — the call stack reaches depth `O(n)`, risking overflow.\n\n" + + "Time is always `O(n)` since all nodes must be processed.", + + realWorldUses: [ + "**Tree deletion:** Post-order is the correct order to free nodes — children must be deleted before their parent.", + "**Expression evaluation:** Post-order traversal of an expression tree computes sub-expressions bottom-up, then combines at the root.", + "**Compiler code generation:** Compilers evaluate expressions bottom-up, generating code for operands before operators.", + "**Directory size calculation:** To compute a directory's total size, sum child sizes first, then add the parent — post-order logic.", + ], + + strengthsAndLimitations: { + strengths: [ + "Guarantees children are processed before their parent — essential for deletion and bottom-up computation.", + "Simple, elegant recursive implementation.", + "Post-order output uniquely identifies subtree boundaries.", + ], + limitations: [ + "Does not produce sorted output — in-order is required for sorted BST values.", + "Recursive implementation risks stack overflow on degenerate trees.", + "Iterative post-order is significantly more complex to implement than iterative pre-order or in-order.", + ], + }, + + whenToUseIt: + "Use post-order traversal whenever you need to process children before their parent — deleting a tree, evaluating an expression tree, or computing aggregate values bottom-up. For pre-order (copy/serialize), use bst-preorder; for sorted output, use bst-inorder.", +}; diff --git a/src/algorithms/trees/traversal/bst-postorder/index.ts b/src/algorithms/trees/traversal/bst-postorder/index.ts new file mode 100644 index 00000000..6d431745 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstPostorder } from "./sources/bst-postorder.ts?fn"; +import { generateBstPostorderSteps } from "./step-generator"; +import type { BstPostorderInput } from "./step-generator"; +import { bstPostorderEducational } from "./educational"; + +import typescriptSource from "./sources/bst-postorder.ts?raw"; +import pythonSource from "./sources/bst-postorder.py?raw"; +import javaSource from "./sources/BSTPostorder.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute BST post-order using the ?fn source — visits children before root */ +function executeBstPostorder(input: BstPostorderInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return bstPostorder(root) as number[]; +} + +const bstPostorderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_POSTORDER!, + name: "BST Post-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "A recursive tree traversal that visits nodes in LRN order: left subtree → right subtree → root", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeBstPostorder, + generateSteps: generateBstPostorderSteps, + educational: bstPostorderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(bstPostorderDefinition); diff --git a/src/algorithms/trees/traversal/bst-postorder/sources/BSTPostorder.java b/src/algorithms/trees/traversal/bst-postorder/sources/BSTPostorder.java new file mode 100644 index 00000000..aaabf684 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/sources/BSTPostorder.java @@ -0,0 +1,28 @@ +// BST Post-Order Traversal — left subtree, right subtree, visit root (LRN) +import java.util.ArrayList; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTPostorder { + public List bstPostorder(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + traverse(root, result); // @step:initialize + return result; // @step:complete + } + + private void traverse(BSTNode node, List result) { + if (node == null) return; // @step:initialize + + // Recurse into the left subtree first + traverse(node.left, result); // @step:traverse-left + // Recurse into the right subtree + traverse(node.right, result); // @step:traverse-right + // Visit the root last — after both children have been processed + result.add(node.value); // @step:visit + } +} diff --git a/src/algorithms/trees/traversal/bst-postorder/sources/bst-postorder.py b/src/algorithms/trees/traversal/bst-postorder/sources/bst-postorder.py new file mode 100644 index 00000000..278a1c4d --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/sources/bst-postorder.py @@ -0,0 +1,24 @@ +# BST Post-Order Traversal — left subtree, right subtree, visit root (LRN) + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_postorder(root) -> list: + result = [] # @step:initialize + + def traverse(node) -> None: + if node is None: # @step:initialize + return + + # Recurse into the left subtree first + traverse(node.left) # @step:traverse-left + # Recurse into the right subtree + traverse(node.right) # @step:traverse-right + # Visit the root last — after both children have been processed + result.append(node.value) # @step:visit + + traverse(root) # @step:initialize + return result # @step:complete diff --git a/src/algorithms/trees/traversal/bst-postorder/sources/bst-postorder.ts b/src/algorithms/trees/traversal/bst-postorder/sources/bst-postorder.ts new file mode 100644 index 00000000..8896318c --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/sources/bst-postorder.ts @@ -0,0 +1,25 @@ +// BST Post-Order Traversal — left subtree, right subtree, visit root (LRN) + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstPostorder(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + + function traverse(node: BSTNode | null): void { + if (node === null) return; // @step:initialize + + // Recurse into the left subtree first + traverse(node.left); // @step:traverse-left + // Recurse into the right subtree + traverse(node.right); // @step:traverse-right + // Visit the root last — after both children have been processed + result.push(node.value); // @step:visit + } + + traverse(root); // @step:initialize + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/bst-postorder/step-generator.test.ts b/src/algorithms/trees/traversal/bst-postorder/step-generator.test.ts new file mode 100644 index 00000000..01b1adce --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/step-generator.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstPostorderSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstPostorderSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits nodes in post-order (LRN) sequence", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([1, 3, 2, 5, 7, 6, 4]); + }); + + it("visits root node (value 4) last", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps[visitSteps.length - 1]?.variables["value"]).toBe(4); + }); + + it("has incrementing step indices", () => { + const steps = generateBstPostorderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/bst-postorder/step-generator.ts b/src/algorithms/trees/traversal/bst-postorder/step-generator.ts new file mode 100644 index 00000000..f7689e89 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-postorder/step-generator.ts @@ -0,0 +1,50 @@ +/** Step generator for BST Post-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_POSTORDER!); + +export interface BstPostorderInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstPostorderSteps(input: BstPostorderInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + function postorder(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + if (node.leftChildId) { + tracker.traverseLeft(nodeId, { currentNode: nodeId, direction: "left" }); + postorder(node.leftChildId); + } + + if (node.rightChildId) { + tracker.traverseRight(nodeId, { currentNode: nodeId, direction: "right" }); + postorder(node.rightChildId); + } + + // Visit root last — after all descendants + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + }); + } + + postorder(rootId); + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/BSTPreorderIterativePipeline.stories.tsx b/src/algorithms/trees/traversal/bst-preorder-iterative/BSTPreorderIterativePipeline.stories.tsx new file mode 100644 index 00000000..0cddd744 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/BSTPreorderIterativePipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the BST Pre-Order Traversal (Iterative) algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstPreorderIterativeSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Pre-Order (Iterative)", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal with some nodes visited */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited in pre-order */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/bst-preorder-iterative.test.ts b/src/algorithms/trees/traversal/bst-preorder-iterative/bst-preorder-iterative.test.ts new file mode 100644 index 00000000..64347f4a --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/bst-preorder-iterative.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { bstPreorderIterative } from "./sources/bst-preorder-iterative.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstPreorderIterative", () => { + it("traverses a balanced 7-node BST in pre-order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstPreorderIterative(root)).toEqual([4, 2, 1, 3, 6, 5, 7]); + }); + + it("returns an empty array for a null root", () => { + expect(bstPreorderIterative(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(bstPreorderIterative(node(42))).toEqual([42]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(bstPreorderIterative(root)).toEqual([5, 4, 3, 2, 1]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3, null, node(4, null, node(5))))); + expect(bstPreorderIterative(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(bstPreorderIterative(node(5, node(3)))).toEqual([5, 3]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(bstPreorderIterative(node(5, null, node(8)))).toEqual([5, 8]); + }); +}); diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/educational.ts b/src/algorithms/trees/traversal/bst-preorder-iterative/educational.ts new file mode 100644 index 00000000..17d0167d --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const bstPreorderIterativeEducational: EducationalContent = { + overview: + "**BST Pre-Order Traversal (Iterative)** visits every node in NLR order using an explicit stack. It pushes the root, then on each loop pops a node (visiting it), pushes its right child, then its left child — ensuring left is processed first due to LIFO stack behavior.", + + howItWorks: + "The algorithm uses a single stack:\n\n" + + "1. **Initialize** — push the root onto the stack.\n" + + "2. **Pop and visit** — pop the top node and record its value.\n" + + "3. **Push children** — push right child first, then left child (so left is popped next).\n" + + "4. **Repeat** until the stack is empty.\n\n" + + "### Example: Iterative Pre-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Visit order:** `4 → 2 → 1 → 3 → 6 → 5 → 7` — identical to recursive pre-order.\n\n" + + "The key insight: pushing right before left ensures left is on top of the stack and gets popped (visited) first.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is pushed onto and popped from the stack exactly once.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "At most `h` nodes exist on the stack at any time. `O(log n)` for balanced, `O(n)` for degenerate trees.", + + bestAndWorstCase: + "**Best case** is a balanced tree — stack never exceeds `O(log n)` depth.\n\n" + + "**Worst case** is a right-skewed tree — every node is pushed and the stack grows to `O(n)`.\n\n" + + "Pre-order iterative is very simple to implement because it only requires a single pass, unlike iterative in-order which requires tracking the current pointer separately.", + + realWorldUses: [ + "**Safe tree copying without recursion:** Iterative pre-order enables copying deep trees without stack overflow.", + "**DOM tree serialization:** Browser engines use pre-order iteration to serialize or diff DOM trees.", + "**Compiler AST printing:** Abstract syntax trees are printed in pre-order for human-readable output.", + "**Depth-first file system scanning:** Pre-order iteration maps directly to depth-first directory scanning.", + ], + + strengthsAndLimitations: { + strengths: [ + "Single-pass, straightforward stack management — simpler than iterative in-order.", + "No recursion — safe for arbitrarily deep trees.", + "Produces exact NLR pre-order output, suitable for tree reconstruction.", + ], + limitations: [ + "Does not produce sorted output — use in-order for sorted BST values.", + "Still uses O(h) heap memory for the stack.", + "Right-heavy trees cause larger stack usage than left-heavy trees.", + ], + }, + + whenToUseIt: + "Use iterative pre-order when you need root-before-children output without recursion overhead. This is the simplest iterative tree traversal to implement. For in-order (sorted) output, use the iterative in-order variant; for bottom-up processing, use iterative post-order.", +}; diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/index.ts b/src/algorithms/trees/traversal/bst-preorder-iterative/index.ts new file mode 100644 index 00000000..051dd668 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstPreorderIterative } from "./sources/bst-preorder-iterative.ts?fn"; +import { generateBstPreorderIterativeSteps } from "./step-generator"; +import type { BstPreorderIterativeInput } from "./step-generator"; +import { bstPreorderIterativeEducational } from "./educational"; + +import typescriptSource from "./sources/bst-preorder-iterative.ts?raw"; +import pythonSource from "./sources/bst-preorder-iterative.py?raw"; +import javaSource from "./sources/BSTPreorderIterative.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute iterative BST pre-order using the ?fn source */ +function executeBstPreorderIterative(input: BstPreorderIterativeInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return bstPreorderIterative(root) as number[]; +} + +const bstPreorderIterativeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_PREORDER_ITERATIVE!, + name: "BST Pre-Order Traversal (Iterative)", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "An iterative NLR traversal using an explicit stack — visits root before subtrees without recursion", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeBstPreorderIterative, + generateSteps: generateBstPreorderIterativeSteps, + educational: bstPreorderIterativeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(bstPreorderIterativeDefinition); diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/sources/BSTPreorderIterative.java b/src/algorithms/trees/traversal/bst-preorder-iterative/sources/BSTPreorderIterative.java new file mode 100644 index 00000000..cc6da901 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/sources/BSTPreorderIterative.java @@ -0,0 +1,36 @@ +// BST Pre-Order Traversal (Iterative) — NLR using an explicit stack +import java.util.ArrayList; +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTPreorderIterative { + public List bstPreorderIterative(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + Deque stack = new ArrayDeque<>(); // @step:initialize + stack.push(root); // @step:initialize + + while (!stack.isEmpty()) { // @step:initialize + BSTNode node = stack.pop(); // @step:pop-from-stack + result.add(node.value); // @step:visit + + // Push right first so left is processed first (LIFO) + if (node.right != null) { // @step:push-to-stack + stack.push(node.right); // @step:push-to-stack + } + if (node.left != null) { // @step:traverse-left + stack.push(node.left); // @step:traverse-left + } + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/sources/bst-preorder-iterative.py b/src/algorithms/trees/traversal/bst-preorder-iterative/sources/bst-preorder-iterative.py new file mode 100644 index 00000000..ebae5526 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/sources/bst-preorder-iterative.py @@ -0,0 +1,26 @@ +# BST Pre-Order Traversal (Iterative) — NLR using an explicit stack + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_preorder_iterative(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + stack = [root] # @step:initialize + + while len(stack) > 0: # @step:initialize + node = stack.pop() # @step:pop-from-stack + result.append(node.value) # @step:visit + + # Push right first so left is processed first (LIFO) + if node.right is not None: # @step:push-to-stack + stack.append(node.right) # @step:push-to-stack + if node.left is not None: # @step:traverse-left + stack.append(node.left) # @step:traverse-left + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/sources/bst-preorder-iterative.ts b/src/algorithms/trees/traversal/bst-preorder-iterative/sources/bst-preorder-iterative.ts new file mode 100644 index 00000000..c9aff349 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/sources/bst-preorder-iterative.ts @@ -0,0 +1,32 @@ +// BST Pre-Order Traversal (Iterative) — NLR using an explicit stack + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstPreorderIterative(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + const stack: BSTNode[] = [root]; // @step:initialize + + while (stack.length > 0) { + // @step:initialize + const node = stack.pop()!; // @step:pop-from-stack + result.push(node.value); // @step:visit + + // Push right first so left is processed first (LIFO) + if (node.right !== null) { + // @step:push-to-stack + stack.push(node.right); // @step:push-to-stack + } + if (node.left !== null) { + // @step:traverse-left + stack.push(node.left); // @step:traverse-left + } + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/step-generator.test.ts b/src/algorithms/trees/traversal/bst-preorder-iterative/step-generator.test.ts new file mode 100644 index 00000000..7d17d8d6 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/step-generator.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstPreorderIterativeSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstPreorderIterativeSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits nodes in pre-order (NLR) sequence", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([4, 2, 1, 3, 6, 5, 7]); + }); + + it("has incrementing step indices", () => { + const steps = generateBstPreorderIterativeSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/bst-preorder-iterative/step-generator.ts b/src/algorithms/trees/traversal/bst-preorder-iterative/step-generator.ts new file mode 100644 index 00000000..2304943e --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder-iterative/step-generator.ts @@ -0,0 +1,62 @@ +/** Step generator for BST Pre-Order Traversal (Iterative) — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_PREORDER_ITERATIVE!); + +export interface BstPreorderIterativeInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstPreorderIterativeSteps( + input: BstPreorderIterativeInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, stack: [rootId] }); + + const stack: string[] = [rootId]; + + while (stack.length > 0) { + const poppedId = stack.pop(); + if (!poppedId) break; + const poppedNode = nodeMap.get(poppedId); + if (!poppedNode) break; + + tracker.visitNode(poppedId, poppedNode.value, { + currentNode: poppedId, + value: poppedNode.value, + stackSize: stack.length, + }); + + // Push right first so left is processed first (LIFO) + if (poppedNode.rightChildId) { + stack.push(poppedNode.rightChildId); + tracker.traverseRight(poppedId, { + currentNode: poppedId, + direction: "right", + stackSize: stack.length, + }); + } + + if (poppedNode.leftChildId) { + stack.push(poppedNode.leftChildId); + tracker.traverseLeft(poppedId, { + currentNode: poppedId, + direction: "left", + stackSize: stack.length, + }); + } + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/bst-preorder/BSTPreorderPipeline.stories.tsx b/src/algorithms/trees/traversal/bst-preorder/BSTPreorderPipeline.stories.tsx new file mode 100644 index 00000000..70b40d98 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/BSTPreorderPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the BST Pre-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateBstPreorderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/BST Pre-Order", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal with some nodes visited */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited in pre-order */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/bst-preorder/bst-preorder.test.ts b/src/algorithms/trees/traversal/bst-preorder/bst-preorder.test.ts new file mode 100644 index 00000000..364bc1bc --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/bst-preorder.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { bstPreorder } from "./sources/bst-preorder.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("bstPreorder", () => { + it("traverses a balanced 7-node BST in pre-order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(bstPreorder(root)).toEqual([4, 2, 1, 3, 6, 5, 7]); + }); + + it("returns an empty array for a null root", () => { + expect(bstPreorder(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(bstPreorder(node(42))).toEqual([42]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(bstPreorder(root)).toEqual([5, 4, 3, 2, 1]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3, null, node(4, null, node(5))))); + expect(bstPreorder(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(bstPreorder(node(5, node(3)))).toEqual([5, 3]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(bstPreorder(node(5, null, node(8)))).toEqual([5, 8]); + }); +}); diff --git a/src/algorithms/trees/traversal/bst-preorder/educational.ts b/src/algorithms/trees/traversal/bst-preorder/educational.ts new file mode 100644 index 00000000..969a0af9 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const bstPreorderEducational: EducationalContent = { + overview: + "**BST Pre-Order Traversal** visits every node in NLR order: **root → left subtree → right subtree**. Unlike in-order traversal (which produces sorted output), pre-order visits the root before its children, making it ideal for copying or serializing a tree.", + + howItWorks: + "The algorithm uses a recursive helper that processes each node in three steps:\n\n" + + "1. **Visit root** — record the current node's value immediately.\n" + + "2. **Recurse left** — visit all nodes in the left subtree.\n" + + "3. **Recurse right** — visit all nodes in the right subtree.\n\n" + + "### Example: Pre-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + " style n4 fill:#06b6d4,stroke:#0891b2\n" + + "```\n\n" + + "**Visit order:** `4 → 2 → 1 → 3 → 6 → 5 → 7` — root always appears before its descendants.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once.\n\n" + + "**Space Complexity: `O(h)` where `h` is the tree height**\n\n" + + "The call stack depth equals the height of the tree. `O(log n)` for balanced, `O(n)` for degenerate trees.", + + bestAndWorstCase: + "**Best case** is a perfectly balanced BST — call stack depth is `O(log n)`.\n\n" + + "**Worst case** is a degenerate (chain) tree — the call stack reaches depth `O(n)`, risking stack overflow for very large inputs.\n\n" + + "Time is always `O(n)` since all nodes must be visited.", + + realWorldUses: [ + "**Tree copying:** Pre-order produces the exact insertion sequence needed to reconstruct the same BST structure from scratch.", + "**Tree serialization:** Pre-order output uniquely identifies a BST's structure, enabling lossless serialization.", + "**Expression tree evaluation:** Pre-order traversal of an expression tree produces prefix (Polish) notation.", + "**File system traversal:** Pre-order mirrors how directory listings show a parent folder before its children.", + ], + + strengthsAndLimitations: { + strengths: [ + "Visits the root before its descendants — ideal for tasks where parent context is needed before children.", + "Simple, elegant recursive implementation closely mirrors the mathematical definition.", + "Pre-order output can be used directly to reconstruct the exact same BST.", + ], + limitations: [ + "Does not produce sorted output — in-order traversal is required for sorted BST values.", + "Recursive implementation risks stack overflow on very deep, degenerate trees.", + "An iterative version using an explicit stack avoids the overflow risk.", + ], + }, + + whenToUseIt: + "Use pre-order traversal when you need to copy, serialize, or export the structure of a tree — parent before children. Use in-order for sorted output, and post-order when you need to process children before their parent (e.g., deleting a tree).", +}; diff --git a/src/algorithms/trees/traversal/bst-preorder/index.ts b/src/algorithms/trees/traversal/bst-preorder/index.ts new file mode 100644 index 00000000..3db8a631 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { bstPreorder } from "./sources/bst-preorder.ts?fn"; +import { generateBstPreorderSteps } from "./step-generator"; +import type { BstPreorderInput } from "./step-generator"; +import { bstPreorderEducational } from "./educational"; + +import typescriptSource from "./sources/bst-preorder.ts?raw"; +import pythonSource from "./sources/bst-preorder.py?raw"; +import javaSource from "./sources/BSTPreorder.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute BST pre-order using the ?fn source — visits root before subtrees */ +function executeBstPreorder(input: BstPreorderInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return bstPreorder(root) as number[]; +} + +const bstPreorderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BST_PREORDER!, + name: "BST Pre-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "A recursive tree traversal that visits nodes in NLR order: root → left subtree → right subtree", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(h)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeBstPreorder, + generateSteps: generateBstPreorderSteps, + educational: bstPreorderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(bstPreorderDefinition); diff --git a/src/algorithms/trees/traversal/bst-preorder/sources/BSTPreorder.java b/src/algorithms/trees/traversal/bst-preorder/sources/BSTPreorder.java new file mode 100644 index 00000000..f2823388 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/sources/BSTPreorder.java @@ -0,0 +1,28 @@ +// BST Pre-Order Traversal — visit root, then left subtree, then right subtree (NLR) +import java.util.ArrayList; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class BSTPreorder { + public List bstPreorder(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + traverse(root, result); // @step:initialize + return result; // @step:complete + } + + private void traverse(BSTNode node, List result) { + if (node == null) return; // @step:initialize + + // Visit the current node first — root before any subtrees + result.add(node.value); // @step:visit + // Recurse into the left subtree + traverse(node.left, result); // @step:traverse-left + // Recurse into the right subtree + traverse(node.right, result); // @step:traverse-right + } +} diff --git a/src/algorithms/trees/traversal/bst-preorder/sources/bst-preorder.py b/src/algorithms/trees/traversal/bst-preorder/sources/bst-preorder.py new file mode 100644 index 00000000..89fa69e5 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/sources/bst-preorder.py @@ -0,0 +1,24 @@ +# BST Pre-Order Traversal — visit root, then left subtree, then right subtree (NLR) + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def bst_preorder(root) -> list: + result = [] # @step:initialize + + def traverse(node) -> None: + if node is None: # @step:initialize + return + + # Visit the current node first — root before any subtrees + result.append(node.value) # @step:visit + # Recurse into the left subtree + traverse(node.left) # @step:traverse-left + # Recurse into the right subtree + traverse(node.right) # @step:traverse-right + + traverse(root) # @step:initialize + return result # @step:complete diff --git a/src/algorithms/trees/traversal/bst-preorder/sources/bst-preorder.ts b/src/algorithms/trees/traversal/bst-preorder/sources/bst-preorder.ts new file mode 100644 index 00000000..665eabfc --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/sources/bst-preorder.ts @@ -0,0 +1,25 @@ +// BST Pre-Order Traversal — visit root, then left subtree, then right subtree (NLR) + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function bstPreorder(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + + function traverse(node: BSTNode | null): void { + if (node === null) return; // @step:initialize + + // Visit the current node first — root before any subtrees + result.push(node.value); // @step:visit + // Recurse into the left subtree + traverse(node.left); // @step:traverse-left + // Recurse into the right subtree + traverse(node.right); // @step:traverse-right + } + + traverse(root); // @step:initialize + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/bst-preorder/step-generator.test.ts b/src/algorithms/trees/traversal/bst-preorder/step-generator.test.ts new file mode 100644 index 00000000..a0c75e38 --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/step-generator.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateBstPreorderSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateBstPreorderSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits root node (value 4) first", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps[0]?.variables["value"]).toBe(4); + }); + + it("visits nodes in pre-order (NLR) sequence", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([4, 2, 1, 3, 6, 5, 7]); + }); + + it("has incrementing step indices", () => { + const steps = generateBstPreorderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/bst-preorder/step-generator.ts b/src/algorithms/trees/traversal/bst-preorder/step-generator.ts new file mode 100644 index 00000000..e4e797ec --- /dev/null +++ b/src/algorithms/trees/traversal/bst-preorder/step-generator.ts @@ -0,0 +1,50 @@ +/** Step generator for BST Pre-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BST_PREORDER!); + +export interface BstPreorderInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateBstPreorderSteps(input: BstPreorderInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId }); + + function preorder(nodeId: string | null): void { + if (!nodeId) return; + const node = nodeMap.get(nodeId); + if (!node) return; + + // Visit root first + tracker.visitNode(nodeId, node.value, { + currentNode: nodeId, + value: node.value, + }); + + if (node.leftChildId) { + tracker.traverseLeft(nodeId, { currentNode: nodeId, direction: "left" }); + preorder(node.leftChildId); + } + + if (node.rightChildId) { + tracker.traverseRight(nodeId, { currentNode: nodeId, direction: "right" }); + preorder(node.rightChildId); + } + } + + preorder(rootId); + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/diagonal-traversal/TreeDiagonalTraversalPipeline.stories.tsx b/src/algorithms/trees/traversal/diagonal-traversal/TreeDiagonalTraversalPipeline.stories.tsx new file mode 100644 index 00000000..9f116a45 --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/TreeDiagonalTraversalPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Diagonal Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateDiagonalTraversalSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Tree Diagonal Traversal", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — second diagonal being processed */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all diagonals visited */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/diagonal-traversal/diagonal-traversal.test.ts b/src/algorithms/trees/traversal/diagonal-traversal/diagonal-traversal.test.ts new file mode 100644 index 00000000..1c64a523 --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/diagonal-traversal.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { treeDiagonalTraversal } from "./sources/tree-diagonal-traversal.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("treeDiagonalTraversal", () => { + it("traverses a balanced 7-node BST by diagonals", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + // d=0: [4,6,7], d=1: [2,5,3] (BFS order: node 2 then right child 5 then left child 3), d=2: [1] + expect(treeDiagonalTraversal(root)).toEqual([[4, 6, 7], [2, 5, 3], [1]]); + }); + + it("returns an empty array for a null root", () => { + expect(treeDiagonalTraversal(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(treeDiagonalTraversal(node(42))).toEqual([[42]]); + }); + + it("handles a right-skewed tree (all on diagonal 0)", () => { + const root = node(1, null, node(2, null, node(3))); + expect(treeDiagonalTraversal(root)).toEqual([[1, 2, 3]]); + }); + + it("handles a left-skewed tree (each node on its own diagonal)", () => { + const root = node(3, node(2, node(1))); + expect(treeDiagonalTraversal(root)).toEqual([[3], [2], [1]]); + }); +}); diff --git a/src/algorithms/trees/traversal/diagonal-traversal/educational.ts b/src/algorithms/trees/traversal/diagonal-traversal/educational.ts new file mode 100644 index 00000000..67c79121 --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const diagonalTraversalEducational: EducationalContent = { + overview: + "**Diagonal Traversal** groups nodes by their diagonal index. Moving right keeps a node on the same diagonal; moving left increments the diagonal by 1. Nodes on the same diagonal form a descending-right line in the tree's visual layout.", + + howItWorks: + "The algorithm uses BFS with a diagonal counter:\n\n" + + "1. **Initialize** — root enters the queue at diagonal 0.\n" + + "2. **BFS with diagonal** — dequeue each node, record its diagonal.\n" + + "3. **Right child** — enqueue at the same diagonal (going right is horizontal).\n" + + "4. **Left child** — enqueue at `diagonal + 1` (going left shifts to the next diagonal).\n" + + "5. **Collect result** — iterate diagonals from 0 to `maxDiagonal`.\n\n" + + "### Example: Diagonal Traversal on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4,d=0)) --> n2((2,d=1))\n" + + " n4 --> n6((6,d=0))\n" + + " n2 --> n1((1,d=2))\n" + + " n2 --> n3((3,d=1))\n" + + " n6 --> n5((5,d=1))\n" + + " n6 --> n7((7,d=0))\n" + + "```\n\n" + + "**Diagonal 0:** `[4, 6, 7]` · **Diagonal 1:** `[2, 3, 5]` · **Diagonal 2:** `[1]`", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is visited exactly once during BFS.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The diagonal map and queue together hold all `n` nodes.", + + bestAndWorstCase: + "**Best case** is a right-skewed tree — all nodes land on diagonal 0, map has one entry.\n\n" + + "**Worst case** is a left-skewed tree — each node is on its own diagonal, map has `n` entries.\n\n" + + "Time is always `O(n)` regardless of tree shape.", + + realWorldUses: [ + "**Tree layout algorithms:** Diagonal grouping maps directly to visual diagonal lines in tree drawings.", + "**Slope-based spatial analysis:** In spatial trees (k-d trees, quad-trees), diagonal grouping reflects geometric proximity.", + "**Matrix diagonal traversal analogy:** Tree diagonal traversal is the tree analog of matrix diagonal traversal.", + "**Interview problems:** A classic medium-difficulty problem testing multi-level tree BFS reasoning.", + ], + + strengthsAndLimitations: { + strengths: [ + "Intuitively maps to the visual diagonal lines visible in a tree drawing.", + "Simple BFS extension — only requires tracking a diagonal counter.", + "Collects nodes in a meaningful spatial grouping.", + ], + limitations: [ + "Requires O(n) map space for diagonal groupings.", + "Less universally useful than level-order or depth-first traversals.", + "Primarily a pedagogical and interview algorithm.", + ], + }, + + whenToUseIt: + "Use diagonal traversal when you need to group or process nodes along visual diagonal lines in the tree — useful for tree rendering, spatial analysis, and interview problems. For vertical grouping, use vertical-order traversal instead.", +}; diff --git a/src/algorithms/trees/traversal/diagonal-traversal/index.ts b/src/algorithms/trees/traversal/diagonal-traversal/index.ts new file mode 100644 index 00000000..bcc6cca6 --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { treeDiagonalTraversal } from "./sources/tree-diagonal-traversal.ts?fn"; +import { generateDiagonalTraversalSteps } from "./step-generator"; +import type { DiagonalTraversalInput } from "./step-generator"; +import { diagonalTraversalEducational } from "./educational"; + +import typescriptSource from "./sources/tree-diagonal-traversal.ts?raw"; +import pythonSource from "./sources/tree-diagonal-traversal.py?raw"; +import javaSource from "./sources/TreeDiagonalTraversal.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute diagonal traversal using the ?fn source */ +function executeDiagonalTraversal(input: DiagonalTraversalInput): number[][] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return treeDiagonalTraversal(root) as number[][]; +} + +const diagonalTraversalDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.TREE_DIAGONAL_TRAVERSAL!, + name: "Diagonal Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "Groups nodes by diagonal index — right child stays on same diagonal, left child moves to next", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeDiagonalTraversal, + generateSteps: generateDiagonalTraversalSteps, + educational: diagonalTraversalEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(diagonalTraversalDefinition); diff --git a/src/algorithms/trees/traversal/diagonal-traversal/sources/TreeDiagonalTraversal.java b/src/algorithms/trees/traversal/diagonal-traversal/sources/TreeDiagonalTraversal.java new file mode 100644 index 00000000..6ecda36b --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/sources/TreeDiagonalTraversal.java @@ -0,0 +1,43 @@ +// Diagonal Traversal — group nodes by diagonal (right = same diagonal, left = next diagonal) +import java.util.*; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class DiagonalTraversal { + public List> diagonalTraversal(BSTNode root) { + List> result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + Queue nodeQueue = new LinkedList<>(); // @step:initialize + Map diagMap = new IdentityHashMap<>(); // @step:initialize + Map> diagonalStore = new TreeMap<>(); // @step:initialize + nodeQueue.offer(root); // @step:initialize + diagMap.put(root, 0); // @step:initialize + + while (!nodeQueue.isEmpty()) { // @step:enqueue-node + BSTNode node = nodeQueue.poll(); // @step:dequeue-node + int diagonal = diagMap.get(node); // @step:dequeue-node + + diagonalStore.computeIfAbsent(diagonal, k -> new ArrayList<>()).add(node.value); // @step:visit + + // Right child stays on same diagonal + if (node.right != null) { // @step:traverse-right + diagMap.put(node.right, diagonal); // @step:traverse-right + nodeQueue.offer(node.right); // @step:traverse-right + } + // Left child moves to next diagonal + if (node.left != null) { // @step:traverse-left + diagMap.put(node.left, diagonal + 1); // @step:traverse-left + nodeQueue.offer(node.left); // @step:traverse-left + } + } + + result.addAll(diagonalStore.values()); // @step:visit + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/diagonal-traversal/sources/tree-diagonal-traversal.py b/src/algorithms/trees/traversal/diagonal-traversal/sources/tree-diagonal-traversal.py new file mode 100644 index 00000000..27fa21da --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/sources/tree-diagonal-traversal.py @@ -0,0 +1,39 @@ +# Diagonal Traversal — group nodes by diagonal (right = same diagonal, left = next diagonal) +from collections import deque, defaultdict + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def diagonal_traversal(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + # Queue of (node, diagonal) pairs + queue = deque([(root, 0)]) # @step:initialize + diagonal_map = defaultdict(list) # @step:initialize + max_diagonal = 0 # @step:initialize + + while len(queue) > 0: # @step:enqueue-node + node, diagonal = queue.popleft() # @step:dequeue-node + + diagonal_map[diagonal].append(node.value) # @step:visit + + if diagonal > max_diagonal: # @step:visit + max_diagonal = diagonal # @step:visit + + # Right child stays on same diagonal + if node.right is not None: # @step:traverse-right + queue.append((node.right, diagonal)) # @step:traverse-right + # Left child moves to next diagonal + if node.left is not None: # @step:traverse-left + queue.append((node.left, diagonal + 1)) # @step:traverse-left + + # Collect diagonals in order + for diag in range(max_diagonal + 1): # @step:visit + result.append(diagonal_map[diag]) # @step:visit + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/diagonal-traversal/sources/tree-diagonal-traversal.ts b/src/algorithms/trees/traversal/diagonal-traversal/sources/tree-diagonal-traversal.ts new file mode 100644 index 00000000..ca506976 --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/sources/tree-diagonal-traversal.ts @@ -0,0 +1,52 @@ +// Diagonal Traversal — group nodes by diagonal (right = same diagonal, left = next diagonal) + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function treeDiagonalTraversal(root: BSTNode | null): number[][] { + const result: number[][] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + // Queue of [node, diagonal] pairs + const queue: [BSTNode, number][] = [[root, 0]]; // @step:initialize + const diagonalMap = new Map(); // @step:initialize + let maxDiagonal = 0; // @step:initialize + + while (queue.length > 0) { + // @step:enqueue-node + const entry = queue.shift()!; // @step:dequeue-node + const node = entry[0]; // @step:dequeue-node + const diagonal = entry[1]; // @step:dequeue-node + + if (!diagonalMap.has(diagonal)) { + // @step:visit + diagonalMap.set(diagonal, []); // @step:visit + } + diagonalMap.get(diagonal)!.push(node.value); // @step:visit + + if (diagonal > maxDiagonal) maxDiagonal = diagonal; // @step:visit + + // Right child stays on same diagonal + if (node.right !== null) { + // @step:traverse-right + queue.push([node.right, diagonal]); // @step:traverse-right + } + // Left child moves to next diagonal + if (node.left !== null) { + // @step:traverse-left + queue.push([node.left, diagonal + 1]); // @step:traverse-left + } + } + + // Collect diagonals in order + for (let diag = 0; diag <= maxDiagonal; diag++) { + // @step:visit + const diagonalValues = diagonalMap.get(diag); // @step:visit + if (diagonalValues) result.push(diagonalValues); // @step:visit + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/diagonal-traversal/step-generator.test.ts b/src/algorithms/trees/traversal/diagonal-traversal/step-generator.test.ts new file mode 100644 index 00000000..13ca6055 --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateDiagonalTraversalSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateDiagonalTraversalSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateDiagonalTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/diagonal-traversal/step-generator.ts b/src/algorithms/trees/traversal/diagonal-traversal/step-generator.ts new file mode 100644 index 00000000..2156c79b --- /dev/null +++ b/src/algorithms/trees/traversal/diagonal-traversal/step-generator.ts @@ -0,0 +1,81 @@ +/** Step generator for Diagonal Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.TREE_DIAGONAL_TRAVERSAL!); + +export interface DiagonalTraversalInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateDiagonalTraversalSteps(input: DiagonalTraversalInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, diagonal: 0 }); + + // BFS with diagonal tracking + const queue: [string, number][] = [[rootId, 0]]; + const diagonalToNodes = new Map(); + let maxDiagonal = 0; + + while (queue.length > 0) { + const entry = queue.shift(); + if (!entry) break; + const [currentId, diagonal] = entry; + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + if (!diagonalToNodes.has(diagonal)) { + diagonalToNodes.set(diagonal, []); + } + diagonalToNodes.get(diagonal)!.push(currentId); + + if (diagonal > maxDiagonal) maxDiagonal = diagonal; + + // Right child stays on same diagonal + if (currentNode.rightChildId) { + queue.push([currentNode.rightChildId, diagonal]); + tracker.traverseRight(currentId, { + currentNode: currentId, + diagonal, + nextDiagonal: diagonal, + }); + } + + // Left child moves to next diagonal + if (currentNode.leftChildId) { + queue.push([currentNode.leftChildId, diagonal + 1]); + tracker.traverseLeft(currentId, { + currentNode: currentId, + diagonal, + nextDiagonal: diagonal + 1, + }); + } + } + + // Visit nodes in diagonal order + for (let diag = 0; diag <= maxDiagonal; diag++) { + const diagonalNodeIds = diagonalToNodes.get(diag); + if (!diagonalNodeIds) continue; + for (const nodeId of diagonalNodeIds) { + const currentNode = nodeMap.get(nodeId); + if (!currentNode) continue; + tracker.visitNode(nodeId, currentNode.value, { + currentNode: nodeId, + value: currentNode.value, + diagonal: diag, + }); + } + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/level-order-traversal/LevelOrderTraversalPipeline.stories.tsx b/src/algorithms/trees/traversal/level-order-traversal/LevelOrderTraversalPipeline.stories.tsx new file mode 100644 index 00000000..960ffe88 --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/LevelOrderTraversalPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Level-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateLevelOrderTraversalSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Level-Order Traversal", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — second level being processed */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all levels visited */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/level-order-traversal/educational.ts b/src/algorithms/trees/traversal/level-order-traversal/educational.ts new file mode 100644 index 00000000..364923ef --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const levelOrderTraversalEducational: EducationalContent = { + overview: + "**Level-Order Traversal** visits every node of a tree level by level, from the root downward, using a queue (BFS). All nodes at depth `d` are visited before any node at depth `d+1`. The result is a list of levels, each containing the values at that depth.", + + howItWorks: + "The algorithm uses a queue (FIFO) and processes nodes level by level:\n\n" + + "1. **Enqueue root** — add the root to the queue.\n" + + "2. **Process each level** — record the queue size (`levelSize`), then dequeue exactly that many nodes.\n" + + "3. **Visit and enqueue children** — for each dequeued node, record its value and enqueue its left and right children.\n" + + "4. **Repeat** until the queue is empty.\n\n" + + "### Example: Level-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Level 0:** `[4]` · **Level 1:** `[2, 6]` · **Level 2:** `[1, 3, 5, 7]`", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is enqueued and dequeued exactly once.\n\n" + + "**Space Complexity: `O(w)` where `w` is the maximum level width**\n\n" + + "The queue holds at most `w` nodes at one time — the width of the widest level. For a perfect binary tree, `w = n/2` (last level), so worst-case space is `O(n)`.", + + bestAndWorstCase: + "**Best case** is a completely skewed tree (linked list) — only one node per level, so the queue never holds more than one node: `O(1)` queue space.\n\n" + + "**Worst case** is a perfect binary tree — the last level holds `n/2` nodes simultaneously in the queue: `O(n)` queue space.\n\n" + + "Time is always `O(n)` regardless of tree shape.", + + realWorldUses: [ + "**Finding the shortest path in unweighted trees:** Level-order naturally finds the minimum depth to any node.", + "**Tree serialization/deserialization:** JSON tree representations (like LeetCode's array format) use level-order.", + "**Rendering tree UIs:** Displaying a tree layer by layer (org charts, file trees) uses level-order for layout.", + "**Finding tree diameter:** Many tree diameter algorithms use two BFS passes.", + ], + + strengthsAndLimitations: { + strengths: [ + "Guarantees left-to-right, top-down order across all levels.", + "Naturally groups nodes by depth — ideal for level-specific operations.", + "Simple queue-based implementation, easy to reason about.", + ], + limitations: [ + "Uses O(w) queue space — can reach O(n) for wide, balanced trees.", + "Not suitable for memory-constrained environments with wide trees.", + "Depth-first traversals (in-order, pre-order, post-order) are more stack-memory efficient for deep, narrow trees.", + ], + }, + + whenToUseIt: + "Use level-order traversal when you need to process a tree layer by layer — finding the minimum depth, computing level-wise sums, or printing the tree in a readable format. For depth-first processing (sorted output, copying, deletion), use in-order, pre-order, or post-order respectively.", +}; diff --git a/src/algorithms/trees/traversal/level-order-traversal/index.ts b/src/algorithms/trees/traversal/level-order-traversal/index.ts new file mode 100644 index 00000000..ac3fe728 --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { levelOrderTraversal } from "./sources/level-order-traversal.ts?fn"; +import { generateLevelOrderTraversalSteps } from "./step-generator"; +import type { LevelOrderTraversalInput } from "./step-generator"; +import { levelOrderTraversalEducational } from "./educational"; + +import typescriptSource from "./sources/level-order-traversal.ts?raw"; +import pythonSource from "./sources/level-order-traversal.py?raw"; +import javaSource from "./sources/LevelOrderTraversal.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute level-order traversal using the ?fn source */ +function executeLevelOrderTraversal(input: LevelOrderTraversalInput): number[][] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return levelOrderTraversal(root) as number[][]; +} + +const levelOrderTraversalDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.LEVEL_ORDER_TRAVERSAL!, + name: "Level-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "BFS-based traversal that visits nodes level by level from root to leaves, left to right", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeLevelOrderTraversal, + generateSteps: generateLevelOrderTraversalSteps, + educational: levelOrderTraversalEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(levelOrderTraversalDefinition); diff --git a/src/algorithms/trees/traversal/level-order-traversal/level-order-traversal.test.ts b/src/algorithms/trees/traversal/level-order-traversal/level-order-traversal.test.ts new file mode 100644 index 00000000..61b84f5b --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/level-order-traversal.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { levelOrderTraversal } from "./sources/level-order-traversal.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("levelOrderTraversal", () => { + it("traverses a balanced 7-node BST level by level", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(levelOrderTraversal(root)).toEqual([[4], [2, 6], [1, 3, 5, 7]]); + }); + + it("returns an empty array for a null root", () => { + expect(levelOrderTraversal(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(levelOrderTraversal(node(42))).toEqual([[42]]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3))); + expect(levelOrderTraversal(root)).toEqual([[5], [4], [3]]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3))); + expect(levelOrderTraversal(root)).toEqual([[1], [2], [3]]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(levelOrderTraversal(node(5, node(3)))).toEqual([[5], [3]]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(levelOrderTraversal(node(5, null, node(8)))).toEqual([[5], [8]]); + }); +}); diff --git a/src/algorithms/trees/traversal/level-order-traversal/sources/LevelOrderTraversal.java b/src/algorithms/trees/traversal/level-order-traversal/sources/LevelOrderTraversal.java new file mode 100644 index 00000000..447c86fb --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/sources/LevelOrderTraversal.java @@ -0,0 +1,42 @@ +// Level-Order Traversal — BFS visiting nodes level by level using a queue +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class LevelOrderTraversal { + public List> levelOrderTraversal(BSTNode root) { + List> result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(root); // @step:initialize + + while (!queue.isEmpty()) { // @step:enqueue-node + int levelSize = queue.size(); // @step:enqueue-node + List currentLevel = new ArrayList<>(); // @step:enqueue-node + + for (int nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { // @step:dequeue-node + BSTNode node = queue.poll(); // @step:dequeue-node + currentLevel.add(node.value); // @step:visit + + if (node.left != null) { // @step:enqueue-node + queue.offer(node.left); // @step:enqueue-node + } + if (node.right != null) { // @step:enqueue-node + queue.offer(node.right); // @step:enqueue-node + } + } + + result.add(currentLevel); // @step:visit + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/level-order-traversal/sources/level-order-traversal.py b/src/algorithms/trees/traversal/level-order-traversal/sources/level-order-traversal.py new file mode 100644 index 00000000..c70a38c8 --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/sources/level-order-traversal.py @@ -0,0 +1,32 @@ +# Level-Order Traversal — BFS visiting nodes level by level using a queue +from collections import deque + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def level_order_traversal(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + queue = deque([root]) # @step:initialize + + while len(queue) > 0: # @step:enqueue-node + level_size = len(queue) # @step:enqueue-node + current_level = [] # @step:enqueue-node + + for _ in range(level_size): # @step:dequeue-node + node = queue.popleft() # @step:dequeue-node + current_level.append(node.value) # @step:visit + + if node.left is not None: # @step:enqueue-node + queue.append(node.left) # @step:enqueue-node + if node.right is not None: # @step:enqueue-node + queue.append(node.right) # @step:enqueue-node + + result.append(current_level) # @step:visit + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/level-order-traversal/sources/level-order-traversal.ts b/src/algorithms/trees/traversal/level-order-traversal/sources/level-order-traversal.ts new file mode 100644 index 00000000..660d0d56 --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/sources/level-order-traversal.ts @@ -0,0 +1,39 @@ +// Level-Order Traversal — BFS visiting nodes level by level using a queue + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function levelOrderTraversal(root: BSTNode | null): number[][] { + const result: number[][] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + const queue: BSTNode[] = [root]; // @step:initialize + + while (queue.length > 0) { + // @step:enqueue-node + const levelSize = queue.length; // @step:enqueue-node + const currentLevel: number[] = []; // @step:enqueue-node + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + // @step:dequeue-node + const node = queue.shift()!; // @step:dequeue-node + currentLevel.push(node.value); // @step:visit + + if (node.left !== null) { + // @step:enqueue-node + queue.push(node.left); // @step:enqueue-node + } + if (node.right !== null) { + // @step:enqueue-node + queue.push(node.right); // @step:enqueue-node + } + } + + result.push(currentLevel); // @step:visit + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/level-order-traversal/step-generator.test.ts b/src/algorithms/trees/traversal/level-order-traversal/step-generator.test.ts new file mode 100644 index 00000000..c6371ee4 --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/step-generator.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateLevelOrderTraversalSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateLevelOrderTraversalSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits root node first", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps[0]?.variables["value"]).toBe(4); + }); + + it("visits nodes in level-order (BFS) sequence", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([4, 2, 6, 1, 3, 5, 7]); + }); + + it("has incrementing step indices", () => { + const steps = generateLevelOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/level-order-traversal/step-generator.ts b/src/algorithms/trees/traversal/level-order-traversal/step-generator.ts new file mode 100644 index 00000000..15d39714 --- /dev/null +++ b/src/algorithms/trees/traversal/level-order-traversal/step-generator.ts @@ -0,0 +1,71 @@ +/** Step generator for Level-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.LEVEL_ORDER_TRAVERSAL!); + +export interface LevelOrderTraversalInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateLevelOrderTraversalSteps(input: LevelOrderTraversalInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, queue: [rootId], level: 0 }); + + const queue: string[] = [rootId]; + let currentLevel = 0; + + while (queue.length > 0) { + const levelSize = queue.length; + const levelNodeIds: string[] = []; + + // Dequeue and visit each node at this level + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + const currentId = queue.shift(); + if (!currentId) break; + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + levelNodeIds.push(currentId); + tracker.visitNode(currentId, currentNode.value, { + currentNode: currentId, + value: currentNode.value, + level: currentLevel, + queueSize: queue.length, + }); + + // Enqueue children for next level + if (currentNode.leftChildId) { + queue.push(currentNode.leftChildId); + tracker.traverseLeft(currentId, { + currentNode: currentId, + nextLevel: currentLevel + 1, + queueSize: queue.length, + }); + } + + if (currentNode.rightChildId) { + queue.push(currentNode.rightChildId); + tracker.traverseRight(currentId, { + currentNode: currentId, + nextLevel: currentLevel + 1, + queueSize: queue.length, + }); + } + } + + currentLevel += 1; + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/MorrisInorderTraversalPipeline.stories.tsx b/src/algorithms/trees/traversal/morris-inorder-traversal/MorrisInorderTraversalPipeline.stories.tsx new file mode 100644 index 00000000..5638b2e7 --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/MorrisInorderTraversalPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Morris In-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateMorrisInorderTraversalSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Morris In-Order Traversal", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before threading begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — threading in progress, some nodes visited */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited in sorted order, tree restored */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/educational.ts b/src/algorithms/trees/traversal/morris-inorder-traversal/educational.ts new file mode 100644 index 00000000..8190910b --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const morrisInorderTraversalEducational: EducationalContent = { + overview: + "**Morris In-Order Traversal** performs in-order traversal in `O(1)` auxiliary space by temporarily threading the tree. It creates a temporary link from each node's in-order predecessor to the node itself, uses it to return after traversing the left subtree, then removes the thread before continuing.", + + howItWorks: + "The algorithm uses two conditions on each iteration:\n\n" + + "**Case 1 — No left child:**\n" + + "- Visit the current node and move right (following a thread if present).\n\n" + + "**Case 2 — Has left child:**\n" + + "- Find the in-order predecessor (rightmost node in the left subtree).\n" + + "- **If predecessor's right is null:** Create a thread (predecessor.right = current). Move left.\n" + + "- **If predecessor's right is current:** Remove the thread. Visit current. Move right.\n\n" + + "### Example: Morris Traversal on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Visit order:** `1 → 2 → 3 → 4 → 5 → 6 → 7` — sorted ascending, using zero extra heap memory.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each node is visited at most twice — once when creating its thread and once when the thread is used. The predecessor search is amortized `O(1)` per node.\n\n" + + "**Space Complexity: `O(1)`**\n\n" + + "No stack or queue is used. Threads are stored in the tree's existing right-pointer fields and are fully restored before the function returns.", + + bestAndWorstCase: + "**Best and worst cases** both have `O(n)` time, as every node requires exactly two passes through the predecessor-finding loop (combined across all iterations).\n\n" + + "Space is always `O(1)` regardless of tree shape — this is the key advantage over recursive and iterative stack-based variants.", + + realWorldUses: [ + "**Embedded systems with strict memory budgets:** O(1) space traversal is essential when the call stack and heap are severely constrained.", + "**Database cursor scanning:** In-memory B-tree iterators sometimes use Morris-style threading to avoid separate stack allocation.", + "**Large tree analysis:** When traversing a tree with millions of nodes, O(1) auxiliary space can be the difference between feasible and infeasible.", + "**Academic/interview showcase:** Morris traversal demonstrates deep understanding of tree structure and pointer manipulation.", + ], + + strengthsAndLimitations: { + strengths: [ + "True O(1) auxiliary space — no recursion and no explicit stack.", + "Produces correct in-order (sorted) output for a valid BST.", + "Restores the tree to its original structure after traversal — non-destructive.", + ], + limitations: [ + "Temporarily modifies the tree (threads), making it unsafe for concurrent reads.", + "More complex to understand and implement correctly than recursive or stack-based variants.", + "Cannot be easily adapted for pre-order or post-order without significant changes.", + ], + }, + + whenToUseIt: + "Use Morris traversal when O(1) space is a hard requirement — embedded systems, very large trees, or environments with no recursion support. For general-purpose in-order traversal, the recursive or iterative stack-based variant is simpler and more readable.", +}; diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/index.ts b/src/algorithms/trees/traversal/morris-inorder-traversal/index.ts new file mode 100644 index 00000000..e4d6f8e3 --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { morrisInorderTraversal } from "./sources/morris-inorder-traversal.ts?fn"; +import { generateMorrisInorderTraversalSteps } from "./step-generator"; +import type { MorrisInorderTraversalInput } from "./step-generator"; +import { morrisInorderTraversalEducational } from "./educational"; + +import typescriptSource from "./sources/morris-inorder-traversal.ts?raw"; +import pythonSource from "./sources/morris-inorder-traversal.py?raw"; +import javaSource from "./sources/MorrisInorderTraversal.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute Morris in-order traversal using the ?fn source */ +function executeMorrisInorderTraversal(input: MorrisInorderTraversalInput): number[] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return morrisInorderTraversal(root) as number[]; +} + +const morrisInorderTraversalDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MORRIS_INORDER_TRAVERSAL!, + name: "Morris In-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "In-order traversal with O(1) auxiliary space using temporary threading — no stack or recursion", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeMorrisInorderTraversal, + generateSteps: generateMorrisInorderTraversalSteps, + educational: morrisInorderTraversalEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(morrisInorderTraversalDefinition); diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/morris-inorder-traversal.test.ts b/src/algorithms/trees/traversal/morris-inorder-traversal/morris-inorder-traversal.test.ts new file mode 100644 index 00000000..ea47e2a0 --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/morris-inorder-traversal.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { morrisInorderTraversal } from "./sources/morris-inorder-traversal.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("morrisInorderTraversal", () => { + it("traverses a balanced 7-node BST in sorted order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(morrisInorderTraversal(root)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("returns an empty array for a null root", () => { + expect(morrisInorderTraversal(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(morrisInorderTraversal(node(42))).toEqual([42]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3, node(2, node(1))))); + expect(morrisInorderTraversal(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3, null, node(4, null, node(5))))); + expect(morrisInorderTraversal(root)).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles a two-node tree with only a left child", () => { + expect(morrisInorderTraversal(node(5, node(3)))).toEqual([3, 5]); + }); + + it("handles a two-node tree with only a right child", () => { + expect(morrisInorderTraversal(node(5, null, node(8)))).toEqual([5, 8]); + }); +}); diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/sources/MorrisInorderTraversal.java b/src/algorithms/trees/traversal/morris-inorder-traversal/sources/MorrisInorderTraversal.java new file mode 100644 index 00000000..2defb83c --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/sources/MorrisInorderTraversal.java @@ -0,0 +1,43 @@ +// Morris In-Order Traversal — O(1) space in-order traversal using temporary threading +import java.util.ArrayList; +import java.util.List; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class MorrisInorderTraversal { + public List morrisInorderTraversal(BSTNode root) { + List result = new ArrayList<>(); // @step:initialize + BSTNode current = root; // @step:initialize + + while (current != null) { // @step:initialize + if (current.left == null) { // @step:visit + // No left child — visit current and move right + result.add(current.value); // @step:visit + current = current.right; // @step:traverse-right + } else { + // Find the inorder predecessor (rightmost node in left subtree) + BSTNode predecessor = current.left; // @step:thread-node + while (predecessor.right != null && predecessor.right != current) { // @step:thread-node + predecessor = predecessor.right; // @step:thread-node + } + + if (predecessor.right == null) { // @step:thread-node + // Thread: make predecessor point back to current + predecessor.right = current; // @step:thread-node + current = current.left; // @step:traverse-left + } else { + // Unthread: restore predecessor's right, visit current, move right + predecessor.right = null; // @step:unthread-node + result.add(current.value); // @step:visit + current = current.right; // @step:traverse-right + } + } + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/sources/morris-inorder-traversal.py b/src/algorithms/trees/traversal/morris-inorder-traversal/sources/morris-inorder-traversal.py new file mode 100644 index 00000000..93b0965e --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/sources/morris-inorder-traversal.py @@ -0,0 +1,34 @@ +# Morris In-Order Traversal — O(1) space in-order traversal using temporary threading + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def morris_inorder_traversal(root) -> list: + result = [] # @step:initialize + current = root # @step:initialize + + while current is not None: # @step:initialize + if current.left is None: # @step:visit + # No left child — visit current and move right + result.append(current.value) # @step:visit + current = current.right # @step:traverse-right + else: + # Find the inorder predecessor (rightmost node in left subtree) + predecessor = current.left # @step:thread-node + while predecessor.right is not None and predecessor.right is not current: # @step:thread-node + predecessor = predecessor.right # @step:thread-node + + if predecessor.right is None: # @step:thread-node + # Thread: make predecessor point back to current + predecessor.right = current # @step:thread-node + current = current.left # @step:traverse-left + else: + # Unthread: restore predecessor's right, visit current, move right + predecessor.right = None # @step:unthread-node + result.append(current.value) # @step:visit + current = current.right # @step:traverse-right + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/sources/morris-inorder-traversal.ts b/src/algorithms/trees/traversal/morris-inorder-traversal/sources/morris-inorder-traversal.ts new file mode 100644 index 00000000..4dbf51a0 --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/sources/morris-inorder-traversal.ts @@ -0,0 +1,43 @@ +// Morris In-Order Traversal — O(1) space in-order traversal using temporary threading + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function morrisInorderTraversal(root: BSTNode | null): number[] { + const result: number[] = []; // @step:initialize + let current: BSTNode | null = root; // @step:initialize + + while (current !== null) { + // @step:initialize + if (current.left === null) { + // @step:visit + // No left child — visit current and move right + result.push(current.value); // @step:visit + current = current.right; // @step:traverse-right + } else { + // Find the inorder predecessor (rightmost node in left subtree) + let predecessor: BSTNode = current.left; // @step:thread-node + while (predecessor.right !== null && predecessor.right !== current) { + // @step:thread-node + predecessor = predecessor.right; // @step:thread-node + } + + if (predecessor.right === null) { + // @step:thread-node + // Thread: make predecessor point back to current + predecessor.right = current; // @step:thread-node + current = current.left; // @step:traverse-left + } else { + // Unthread: restore predecessor's right, visit current, move right + predecessor.right = null; // @step:unthread-node + result.push(current.value); // @step:visit + current = current.right; // @step:traverse-right + } + } + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/step-generator.test.ts b/src/algorithms/trees/traversal/morris-inorder-traversal/step-generator.test.ts new file mode 100644 index 00000000..aa675ac5 --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/step-generator.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateMorrisInorderTraversalSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateMorrisInorderTraversalSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits nodes in sorted ascending order", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + expect(visitedValues).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it("has incrementing step indices", () => { + const steps = generateMorrisInorderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/morris-inorder-traversal/step-generator.ts b/src/algorithms/trees/traversal/morris-inorder-traversal/step-generator.ts new file mode 100644 index 00000000..c7da15cb --- /dev/null +++ b/src/algorithms/trees/traversal/morris-inorder-traversal/step-generator.ts @@ -0,0 +1,85 @@ +/** Step generator for Morris In-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MORRIS_INORDER_TRAVERSAL!); + +export interface MorrisInorderTraversalInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateMorrisInorderTraversalSteps( + input: MorrisInorderTraversalInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, current: rootId }); + + // Simulate Morris traversal using the node IDs + // We track "threads" as a separate map so we don't mutate the original nodeMap + const threadMap = new Map(); // predecessor -> threaded-to node + + let currentId: string | null = rootId; + + while (currentId !== null) { + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + if (!currentNode.leftChildId) { + // No left child: visit and move right + tracker.visitNode(currentId, currentNode.value, { + currentNode: currentId, + value: currentNode.value, + }); + tracker.traverseRight(currentId, { currentNode: currentId, direction: "right" }); + currentId = currentNode.rightChildId ?? threadMap.get(currentId) ?? null; + } else { + // Find inorder predecessor in left subtree + let predecessorId: string = currentNode.leftChildId; + let predecessorNode = nodeMap.get(predecessorId); + + while ( + predecessorNode?.rightChildId && + predecessorNode.rightChildId !== currentId && + !threadMap.has(predecessorId) + ) { + predecessorId = predecessorNode.rightChildId; + predecessorNode = nodeMap.get(predecessorId); + } + + const isThreaded = threadMap.get(predecessorId) === currentId; + + if (!isThreaded) { + // Create thread: predecessor -> current + threadMap.set(predecessorId, currentId); + tracker.traverseLeft(currentId, { + currentNode: currentId, + predecessorId, + phase: "threading", + }); + currentId = currentNode.leftChildId; + } else { + // Remove thread and visit current + threadMap.delete(predecessorId); + tracker.visitNode(currentId, currentNode.value, { + currentNode: currentId, + value: currentNode.value, + phase: "unthreading", + }); + tracker.traverseRight(currentId, { currentNode: currentId, direction: "right" }); + currentId = currentNode.rightChildId; + } + } + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/reverse-level-order/ReverseLevelOrderPipeline.stories.tsx b/src/algorithms/trees/traversal/reverse-level-order/ReverseLevelOrderPipeline.stories.tsx new file mode 100644 index 00000000..4b2d8ce7 --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/ReverseLevelOrderPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Reverse Level-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateReverseLevelOrderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Reverse Level-Order", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — visiting bottom levels */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all levels visited bottom-up */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/reverse-level-order/educational.ts b/src/algorithms/trees/traversal/reverse-level-order/educational.ts new file mode 100644 index 00000000..49b831a5 --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/educational.ts @@ -0,0 +1,56 @@ +import type { EducationalContent } from "@/types"; + +export const reverseLevelOrderEducational: EducationalContent = { + overview: + "**Reverse Level-Order Traversal** visits nodes from the deepest level upward to the root, left to right within each level. It uses standard BFS with a queue, but prepends each completed level to the result instead of appending — producing bottom-up level groups.", + + howItWorks: + "The algorithm is identical to standard level-order traversal, with one change:\n\n" + + "1. **BFS with queue** — process levels top-down using the standard level-order approach.\n" + + "2. **Prepend each level** — instead of appending to the result, each completed level is inserted at position 0.\n" + + "3. **Result** — after BFS completes, the result is naturally in bottom-up level order.\n\n" + + "### Example: Reverse Level-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Level 2:** `[1, 3, 5, 7]` · **Level 1:** `[2, 6]` · **Level 0:** `[4]`", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is enqueued and dequeued exactly once.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The result stores all `n` node values. Queue space is `O(w)` where `w` is the maximum level width.", + + bestAndWorstCase: + "**Best and worst cases** are both `O(n)` time and `O(n)` output space since all nodes must be visited and stored.\n\n" + + "Queue memory peaks at the widest level — `O(n/2)` for a perfect binary tree, `O(1)` for a skewed tree.", + + realWorldUses: [ + "**Minimum depth calculation:** Reverse level-order directly provides leaf nodes first, useful for bottom-up depth calculations.", + "**Printing tree structure:** Display trees from leaves to root (useful for dependency graphs showing deepest dependencies first).", + "**Bottom-up tree algorithms:** Algorithms like computing subtree sums naturally work in reverse level-order.", + "**Topological-style tree processing:** Process nodes from leaves inward when parents depend on child results.", + ], + + strengthsAndLimitations: { + strengths: [ + "Simple adaptation of standard level-order — just prepend instead of append.", + "Provides natural bottom-up level grouping without additional passes.", + "Easy to implement using a deque/stack as the result accumulator.", + ], + limitations: [ + "Requires storing all levels in memory before the result is meaningful — no early termination.", + "Uses O(n) space for the result regardless of tree shape.", + "Not suitable when you need to process the deepest level immediately upon discovery.", + ], + }, + + whenToUseIt: + "Use reverse level-order traversal when you need tree levels from bottom to top — computing bottom-up aggregates, displaying dependency trees from leaves to root, or processing subtrees before their parents. For standard top-down level processing, use level-order traversal instead.", +}; diff --git a/src/algorithms/trees/traversal/reverse-level-order/index.ts b/src/algorithms/trees/traversal/reverse-level-order/index.ts new file mode 100644 index 00000000..833f1bc3 --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { reverseLevelOrder } from "./sources/reverse-level-order.ts?fn"; +import { generateReverseLevelOrderSteps } from "./step-generator"; +import type { ReverseLevelOrderInput } from "./step-generator"; +import { reverseLevelOrderEducational } from "./educational"; + +import typescriptSource from "./sources/reverse-level-order.ts?raw"; +import pythonSource from "./sources/reverse-level-order.py?raw"; +import javaSource from "./sources/ReverseLevelOrder.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute reverse level-order traversal using the ?fn source */ +function executeReverseLevelOrder(input: ReverseLevelOrderInput): number[][] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return reverseLevelOrder(root) as number[][]; +} + +const reverseLevelOrderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.REVERSE_LEVEL_ORDER!, + name: "Reverse Level-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "BFS traversal that returns levels from deepest to shallowest — bottom-up level-by-level output", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeReverseLevelOrder, + generateSteps: generateReverseLevelOrderSteps, + educational: reverseLevelOrderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(reverseLevelOrderDefinition); diff --git a/src/algorithms/trees/traversal/reverse-level-order/reverse-level-order.test.ts b/src/algorithms/trees/traversal/reverse-level-order/reverse-level-order.test.ts new file mode 100644 index 00000000..9fa7e19e --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/reverse-level-order.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { reverseLevelOrder } from "./sources/reverse-level-order.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("reverseLevelOrder", () => { + it("traverses a balanced 7-node BST in reverse level order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(reverseLevelOrder(root)).toEqual([[1, 3, 5, 7], [2, 6], [4]]); + }); + + it("returns an empty array for a null root", () => { + expect(reverseLevelOrder(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(reverseLevelOrder(node(42))).toEqual([[42]]); + }); + + it("handles a left-skewed tree", () => { + const root = node(5, node(4, node(3))); + expect(reverseLevelOrder(root)).toEqual([[3], [4], [5]]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3))); + expect(reverseLevelOrder(root)).toEqual([[3], [2], [1]]); + }); + + it("handles a two-node tree", () => { + expect(reverseLevelOrder(node(5, node(3)))).toEqual([[3], [5]]); + }); +}); diff --git a/src/algorithms/trees/traversal/reverse-level-order/sources/ReverseLevelOrder.java b/src/algorithms/trees/traversal/reverse-level-order/sources/ReverseLevelOrder.java new file mode 100644 index 00000000..363e25c3 --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/sources/ReverseLevelOrder.java @@ -0,0 +1,43 @@ +// Reverse Level-Order Traversal — BFS bottom-up: deepest level first +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class ReverseLevelOrder { + public List> reverseLevelOrder(BSTNode root) { + List> result = new LinkedList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(root); // @step:initialize + + while (!queue.isEmpty()) { // @step:enqueue-node + int levelSize = queue.size(); // @step:enqueue-node + List currentLevel = new ArrayList<>(); // @step:enqueue-node + + for (int nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { // @step:dequeue-node + BSTNode node = queue.poll(); // @step:dequeue-node + currentLevel.add(node.value); // @step:visit + + if (node.left != null) { // @step:enqueue-node + queue.offer(node.left); // @step:enqueue-node + } + if (node.right != null) { // @step:enqueue-node + queue.offer(node.right); // @step:enqueue-node + } + } + + // Prepend level to get bottom-up order + ((LinkedList>) result).addFirst(currentLevel); // @step:visit + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/reverse-level-order/sources/reverse-level-order.py b/src/algorithms/trees/traversal/reverse-level-order/sources/reverse-level-order.py new file mode 100644 index 00000000..d578f209 --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/sources/reverse-level-order.py @@ -0,0 +1,33 @@ +# Reverse Level-Order Traversal — BFS bottom-up: deepest level first +from collections import deque + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def reverse_level_order(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + queue = deque([root]) # @step:initialize + + while len(queue) > 0: # @step:enqueue-node + level_size = len(queue) # @step:enqueue-node + current_level = [] # @step:enqueue-node + + for _ in range(level_size): # @step:dequeue-node + node = queue.popleft() # @step:dequeue-node + current_level.append(node.value) # @step:visit + + if node.left is not None: # @step:enqueue-node + queue.append(node.left) # @step:enqueue-node + if node.right is not None: # @step:enqueue-node + queue.append(node.right) # @step:enqueue-node + + # Prepend level to get bottom-up order + result.insert(0, current_level) # @step:visit + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/reverse-level-order/sources/reverse-level-order.ts b/src/algorithms/trees/traversal/reverse-level-order/sources/reverse-level-order.ts new file mode 100644 index 00000000..821ab14f --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/sources/reverse-level-order.ts @@ -0,0 +1,40 @@ +// Reverse Level-Order Traversal — BFS bottom-up: deepest level first + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function reverseLevelOrder(root: BSTNode | null): number[][] { + const result: number[][] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + const queue: BSTNode[] = [root]; // @step:initialize + + while (queue.length > 0) { + // @step:enqueue-node + const levelSize = queue.length; // @step:enqueue-node + const currentLevel: number[] = []; // @step:enqueue-node + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + // @step:dequeue-node + const node = queue.shift()!; // @step:dequeue-node + currentLevel.push(node.value); // @step:visit + + if (node.left !== null) { + // @step:enqueue-node + queue.push(node.left); // @step:enqueue-node + } + if (node.right !== null) { + // @step:enqueue-node + queue.push(node.right); // @step:enqueue-node + } + } + + // Prepend level to get bottom-up order + result.unshift(currentLevel); // @step:visit + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/reverse-level-order/step-generator.test.ts b/src/algorithms/trees/traversal/reverse-level-order/step-generator.test.ts new file mode 100644 index 00000000..4eb0b5b7 --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/step-generator.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateReverseLevelOrderSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateReverseLevelOrderSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("visits leaf nodes before root (bottom-up order)", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + const visitedValues = visitSteps.map((step) => step.variables["value"] as number); + // Bottom level first: 1, 3, 5, 7, then level 1: 2, 6, then root: 4 + expect(visitedValues).toEqual([1, 3, 5, 7, 2, 6, 4]); + }); + + it("has incrementing step indices", () => { + const steps = generateReverseLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/reverse-level-order/step-generator.ts b/src/algorithms/trees/traversal/reverse-level-order/step-generator.ts new file mode 100644 index 00000000..a2585afb --- /dev/null +++ b/src/algorithms/trees/traversal/reverse-level-order/step-generator.ts @@ -0,0 +1,77 @@ +/** Step generator for Reverse Level-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.REVERSE_LEVEL_ORDER!); + +export interface ReverseLevelOrderInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateReverseLevelOrderSteps(input: ReverseLevelOrderInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, queue: [rootId] }); + + const queue: string[] = [rootId]; + // Collect levels top-down, then visit bottom-up + const allLevels: string[][] = []; + + while (queue.length > 0) { + const levelSize = queue.length; + const currentLevelIds: string[] = []; + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + const currentId = queue.shift(); + if (!currentId) break; + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + currentLevelIds.push(currentId); + + if (currentNode.leftChildId) { + queue.push(currentNode.leftChildId); + tracker.traverseLeft(currentId, { + currentNode: currentId, + queueSize: queue.length, + }); + } + + if (currentNode.rightChildId) { + queue.push(currentNode.rightChildId); + tracker.traverseRight(currentId, { + currentNode: currentId, + queueSize: queue.length, + }); + } + } + + allLevels.push(currentLevelIds); + } + + // Visit levels in reverse order (bottom-up) + for (let levelIndex = allLevels.length - 1; levelIndex >= 0; levelIndex--) { + const levelIds = allLevels[levelIndex]; + if (!levelIds) continue; + for (const nodeId of levelIds) { + const currentNode = nodeMap.get(nodeId); + if (!currentNode) continue; + tracker.visitNode(nodeId, currentNode.value, { + currentNode: nodeId, + value: currentNode.value, + level: levelIndex, + }); + } + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/VerticalOrderTraversalPipeline.stories.tsx b/src/algorithms/trees/traversal/vertical-order-traversal/VerticalOrderTraversalPipeline.stories.tsx new file mode 100644 index 00000000..e9cda745 --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/VerticalOrderTraversalPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Vertical-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateVerticalOrderTraversalSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Vertical-Order Traversal", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — BFS assigning column indices */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all nodes visited by column */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/educational.ts b/src/algorithms/trees/traversal/vertical-order-traversal/educational.ts new file mode 100644 index 00000000..7a86fd38 --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const verticalOrderTraversalEducational: EducationalContent = { + overview: + "**Vertical-Order Traversal** groups nodes by their vertical column index. The root is at column 0; going left decrements the column, going right increments it. Nodes in the same column are grouped together and collected left-to-right using BFS.", + + howItWorks: + "The algorithm uses BFS with column tracking:\n\n" + + "1. **Initialize** — root enters the queue at column 0.\n" + + "2. **BFS with column** — dequeue each node, record its column, enqueue left child at `column - 1` and right child at `column + 1`.\n" + + "3. **Group by column** — a map accumulates node values per column index.\n" + + "4. **Collect result** — iterate columns from `minColumn` to `maxColumn` to build the final list.\n\n" + + "### Example: Vertical-Order on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4,col=0)) --> n2((2,col=-1))\n" + + " n4 --> n6((6,col=1))\n" + + " n2 --> n1((1,col=-2))\n" + + " n2 --> n3((3,col=0))\n" + + " n6 --> n5((5,col=0))\n" + + " n6 --> n7((7,col=2))\n" + + "```\n\n" + + "**Col -2:** `[1]` · **Col -1:** `[2]` · **Col 0:** `[4, 3, 5]` · **Col 1:** `[6]` · **Col 2:** `[7]`", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n log n)` or `O(n)`**\n\n" + + "BFS is `O(n)`. Grouping into a sorted map is `O(n log n)` with a sorted map, or `O(n)` with a hash map plus linear scan.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The column map and BFS queue together hold all `n` nodes.", + + bestAndWorstCase: + "**Best case** is a perfectly balanced tree — columns span `O(log n)` width, reducing sort overhead.\n\n" + + "**Worst case** is a completely skewed tree — all nodes end up in distinct columns, requiring the full `O(n)` column range.\n\n" + + "BFS ensures top-down, left-to-right ordering within each column naturally.", + + realWorldUses: [ + "**Tree rendering:** Vertical columns directly map to horizontal screen positions for tree visualization.", + "**Database query plans:** Tree-shaped query plan nodes are often arranged by vertical dependency column.", + "**Binary tree to linked list conversion:** Vertical-order gives a natural left-to-right, top-to-bottom grouping.", + "**Hierarchical org chart layout:** Vertical order assigns horizontal positions to employees in an org hierarchy.", + ], + + strengthsAndLimitations: { + strengths: [ + "BFS naturally produces top-down ordering within each column.", + "Column indices are intuitively meaningful — directly map to horizontal layout positions.", + "Simple extension of standard level-order BFS with a column parameter.", + ], + limitations: [ + "Requires O(n) extra space for the column map.", + "Sorting columns requires O(n log n) unless column range is known in advance.", + "Nodes at the same column and same depth may have ambiguous ordering in some problem variants.", + ], + }, + + whenToUseIt: + "Use vertical-order traversal when you need to group or display tree nodes by their horizontal position. This is essential for tree rendering, visualization layout, and certain interview problems. For simple level-by-level processing, use standard level-order traversal.", +}; diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/index.ts b/src/algorithms/trees/traversal/vertical-order-traversal/index.ts new file mode 100644 index 00000000..e60232c2 --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { verticalOrderTraversal } from "./sources/vertical-order-traversal.ts?fn"; +import { generateVerticalOrderTraversalSteps } from "./step-generator"; +import type { VerticalOrderTraversalInput } from "./step-generator"; +import { verticalOrderTraversalEducational } from "./educational"; + +import typescriptSource from "./sources/vertical-order-traversal.ts?raw"; +import pythonSource from "./sources/vertical-order-traversal.py?raw"; +import javaSource from "./sources/VerticalOrderTraversal.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute vertical order traversal using the ?fn source */ +function executeVerticalOrderTraversal(input: VerticalOrderTraversalInput): number[][] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return verticalOrderTraversal(root) as number[][]; +} + +const verticalOrderTraversalDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.VERTICAL_ORDER_TRAVERSAL!, + name: "Vertical-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "BFS traversal grouping nodes by vertical column index — left decrements, right increments column", + timeComplexity: { + best: "O(n)", + average: "O(n log n)", + worst: "O(n log n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeVerticalOrderTraversal, + generateSteps: generateVerticalOrderTraversalSteps, + educational: verticalOrderTraversalEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(verticalOrderTraversalDefinition); diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/sources/VerticalOrderTraversal.java b/src/algorithms/trees/traversal/vertical-order-traversal/sources/VerticalOrderTraversal.java new file mode 100644 index 00000000..72237674 --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/sources/VerticalOrderTraversal.java @@ -0,0 +1,50 @@ +// Vertical-Order Traversal — BFS grouping nodes by vertical column index +import java.util.*; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class VerticalOrderTraversal { + public List> verticalOrderTraversal(BSTNode root) { + List> result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + // Queue stores int[] {nodeRef index, column} but we track via map + Queue queue = new LinkedList<>(); // @step:initialize + Map nodeInfo = new IdentityHashMap<>(); // @step:initialize + Map> columnMap = new TreeMap<>(); // @step:initialize + queue.offer(new int[]{0}); // @step:initialize + nodeInfo.put(root, new int[]{0}); // @step:initialize + + Queue nodeQueue = new LinkedList<>(); // @step:initialize + nodeQueue.offer(root); // @step:initialize + int[] colTracker = {0}; // @step:initialize + Map colMap = new IdentityHashMap<>(); // @step:initialize + colMap.put(root, 0); // @step:initialize + + while (!nodeQueue.isEmpty()) { // @step:enqueue-node + BSTNode node = nodeQueue.poll(); // @step:dequeue-node + int column = colMap.get(node); // @step:dequeue-node + + // Record this node's value in its column + columnMap.computeIfAbsent(column, k -> new ArrayList<>()).add(node.value); // @step:visit + + if (node.left != null) { // @step:enqueue-node + colMap.put(node.left, column - 1); // @step:enqueue-node + nodeQueue.offer(node.left); // @step:enqueue-node + } + if (node.right != null) { // @step:enqueue-node + colMap.put(node.right, column + 1); // @step:enqueue-node + nodeQueue.offer(node.right); // @step:enqueue-node + } + } + + // TreeMap already sorted by column key + result.addAll(columnMap.values()); // @step:visit + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/sources/vertical-order-traversal.py b/src/algorithms/trees/traversal/vertical-order-traversal/sources/vertical-order-traversal.py new file mode 100644 index 00000000..ad7bd46e --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/sources/vertical-order-traversal.py @@ -0,0 +1,41 @@ +# Vertical-Order Traversal — BFS grouping nodes by vertical column index +from collections import deque, defaultdict + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def vertical_order_traversal(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + # Queue stores (node, column) pairs + queue = deque([(root, 0)]) # @step:initialize + column_map = defaultdict(list) # @step:initialize + min_column = 0 # @step:initialize + max_column = 0 # @step:initialize + + while len(queue) > 0: # @step:enqueue-node + node, column = queue.popleft() # @step:dequeue-node + + # Record this node's value in its column + column_map[column].append(node.value) # @step:visit + + if column < min_column: # @step:visit + min_column = column # @step:visit + if column > max_column: # @step:visit + max_column = column # @step:visit + + if node.left is not None: # @step:enqueue-node + queue.append((node.left, column - 1)) # @step:enqueue-node + if node.right is not None: # @step:enqueue-node + queue.append((node.right, column + 1)) # @step:enqueue-node + + # Collect columns in order from leftmost to rightmost + for col in range(min_column, max_column + 1): # @step:visit + result.append(column_map[col]) # @step:visit + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/sources/vertical-order-traversal.ts b/src/algorithms/trees/traversal/vertical-order-traversal/sources/vertical-order-traversal.ts new file mode 100644 index 00000000..887a5228 --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/sources/vertical-order-traversal.ts @@ -0,0 +1,53 @@ +// Vertical-Order Traversal — BFS grouping nodes by vertical column index + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function verticalOrderTraversal(root: BSTNode | null): number[][] { + const result: number[][] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + // Queue stores [node, column] pairs + const queue: [BSTNode, number][] = [[root, 0]]; // @step:initialize + const columnMap = new Map(); // @step:initialize + let minColumn = 0; // @step:initialize + let maxColumn = 0; // @step:initialize + + while (queue.length > 0) { + // @step:enqueue-node + const entry = queue.shift()!; // @step:dequeue-node + const node = entry[0]; // @step:dequeue-node + const column = entry[1]; // @step:dequeue-node + + // Record this node's value in its column + if (!columnMap.has(column)) { + // @step:visit + columnMap.set(column, []); // @step:visit + } + columnMap.get(column)!.push(node.value); // @step:visit + + if (column < minColumn) minColumn = column; // @step:visit + if (column > maxColumn) maxColumn = column; // @step:visit + + if (node.left !== null) { + // @step:enqueue-node + queue.push([node.left, column - 1]); // @step:enqueue-node + } + if (node.right !== null) { + // @step:enqueue-node + queue.push([node.right, column + 1]); // @step:enqueue-node + } + } + + // Collect columns in order from leftmost to rightmost + for (let col = minColumn; col <= maxColumn; col++) { + // @step:visit + const columnValues = columnMap.get(col); // @step:visit + if (columnValues) result.push(columnValues); // @step:visit + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/step-generator.test.ts b/src/algorithms/trees/traversal/vertical-order-traversal/step-generator.test.ts new file mode 100644 index 00000000..a9ac859a --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateVerticalOrderTraversalSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateVerticalOrderTraversalSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateVerticalOrderTraversalSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/step-generator.ts b/src/algorithms/trees/traversal/vertical-order-traversal/step-generator.ts new file mode 100644 index 00000000..e938d6ab --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/step-generator.ts @@ -0,0 +1,84 @@ +/** Step generator for Vertical-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.VERTICAL_ORDER_TRAVERSAL!); + +export interface VerticalOrderTraversalInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateVerticalOrderTraversalSteps( + input: VerticalOrderTraversalInput, +): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, column: 0 }); + + // BFS with column tracking + const queue: [string, number][] = [[rootId, 0]]; + const columnToNodes = new Map(); + let minColumn = 0; + let maxColumn = 0; + + while (queue.length > 0) { + const entry = queue.shift(); + if (!entry) break; + const [currentId, column] = entry; + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + // Record column assignment + if (!columnToNodes.has(column)) { + columnToNodes.set(column, []); + } + columnToNodes.get(column)!.push(currentId); + + if (column < minColumn) minColumn = column; + if (column > maxColumn) maxColumn = column; + + if (currentNode.leftChildId) { + queue.push([currentNode.leftChildId, column - 1]); + tracker.traverseLeft(currentId, { + currentNode: currentId, + column, + nextColumn: column - 1, + }); + } + + if (currentNode.rightChildId) { + queue.push([currentNode.rightChildId, column + 1]); + tracker.traverseRight(currentId, { + currentNode: currentId, + column, + nextColumn: column + 1, + }); + } + } + + // Visit nodes in vertical column order (left to right) + for (let col = minColumn; col <= maxColumn; col++) { + const columnNodeIds = columnToNodes.get(col); + if (!columnNodeIds) continue; + for (const nodeId of columnNodeIds) { + const currentNode = nodeMap.get(nodeId); + if (!currentNode) continue; + tracker.visitNode(nodeId, currentNode.value, { + currentNode: nodeId, + value: currentNode.value, + column: col, + }); + } + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/vertical-order-traversal/vertical-order-traversal.test.ts b/src/algorithms/trees/traversal/vertical-order-traversal/vertical-order-traversal.test.ts new file mode 100644 index 00000000..e6a3460f --- /dev/null +++ b/src/algorithms/trees/traversal/vertical-order-traversal/vertical-order-traversal.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { verticalOrderTraversal } from "./sources/vertical-order-traversal.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("verticalOrderTraversal", () => { + it("traverses a balanced 7-node BST by vertical columns", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + // col -2: [1], col -1: [2], col 0: [4,3,5], col 1: [6], col 2: [7] + expect(verticalOrderTraversal(root)).toEqual([[1], [2], [4, 3, 5], [6], [7]]); + }); + + it("returns an empty array for a null root", () => { + expect(verticalOrderTraversal(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(verticalOrderTraversal(node(42))).toEqual([[42]]); + }); + + it("handles a right-skewed tree", () => { + const root = node(1, null, node(2, null, node(3))); + expect(verticalOrderTraversal(root)).toEqual([[1], [2], [3]]); + }); + + it("handles a two-node tree with left child", () => { + expect(verticalOrderTraversal(node(5, node(3)))).toEqual([[3], [5]]); + }); +}); diff --git a/src/algorithms/trees/traversal/zigzag-level-order/ZigzagLevelOrderPipeline.stories.tsx b/src/algorithms/trees/traversal/zigzag-level-order/ZigzagLevelOrderPipeline.stories.tsx new file mode 100644 index 00000000..c3fa1142 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/ZigzagLevelOrderPipeline.stories.tsx @@ -0,0 +1,113 @@ +/** + * Storybook stories for the Zigzag Level-Order Traversal algorithm pipeline. + * Uses the real step generator with a balanced 7-node BST, + * rendering the TreeVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { TreeVisualState, TreeNode } from "@/types"; +import { generateZigzagLevelOrderSteps } from "./step-generator"; +import TreeVisualizer from "@/components/visualization/TreeVisualizer"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Zigzag Level-Order", + component: TreeVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — tree shown before traversal begins */ +export const InitialState: Story = { + args: { + visualState: steps[0]!.visualState as TreeVisualState, + }, +}; + +/** Mid-traversal — second level being processed in reverse */ +export const MidTraversal: Story = { + args: { + visualState: steps[Math.floor(steps.length / 2)]!.visualState as TreeVisualState, + }, +}; + +/** Completed traversal — all levels visited in zigzag pattern */ +export const TraversalComplete: Story = { + args: { + visualState: steps[steps.length - 1]!.visualState as TreeVisualState, + }, +}; diff --git a/src/algorithms/trees/traversal/zigzag-level-order/educational.ts b/src/algorithms/trees/traversal/zigzag-level-order/educational.ts new file mode 100644 index 00000000..6c3ae7f2 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const zigzagLevelOrderEducational: EducationalContent = { + overview: + "**Zigzag Level-Order Traversal** visits a tree level by level, but alternates direction: odd levels are read left-to-right and even levels right-to-left (or vice versa), producing a zigzag pattern. It uses BFS with a direction flag toggled after each level.", + + howItWorks: + "The algorithm extends standard level-order BFS with an index-placement trick:\n\n" + + "1. **BFS with queue** — standard level-by-level processing.\n" + + "2. **Direction flag** — `leftToRight` toggles after each level.\n" + + "3. **Index calculation** — when `leftToRight = true`, insert at `nodeIndex`; when false, insert at `levelSize - 1 - nodeIndex`.\n" + + "4. **Result** — each level is naturally placed in the correct zigzag order.\n\n" + + "### Example: Zigzag on a 7-Node BST\n\n" + + "```mermaid\n" + + "graph TD\n" + + " n4((4)) --> n2((2))\n" + + " n4 --> n6((6))\n" + + " n2 --> n1((1))\n" + + " n2 --> n3((3))\n" + + " n6 --> n5((5))\n" + + " n6 --> n7((7))\n" + + "```\n\n" + + "**Level 0 (L→R):** `[4]` · **Level 1 (R→L):** `[6, 2]` · **Level 2 (L→R):** `[1, 3, 5, 7]`", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Every node is enqueued and dequeued exactly once.\n\n" + + "**Space Complexity: `O(w)`**\n\n" + + "The queue holds at most the widest level width `w` nodes simultaneously. For a perfect binary tree, `w = n/2`.", + + bestAndWorstCase: + "**Best case** is a skewed tree — one node per level, `O(1)` queue space.\n\n" + + "**Worst case** is a perfect binary tree — the last level fills the queue with `n/2` nodes.\n\n" + + "Time is always `O(n)`. The index-calculation trick avoids any extra reversal passes.", + + realWorldUses: [ + "**Interview staple:** Zigzag traversal appears frequently in software engineering interviews (LeetCode #103).", + "**Alternating-direction rendering:** Display tree layers in alternating directions for visual variety or symmetry.", + "**Hierarchical data display:** Org charts or tournament brackets sometimes use alternating direction to reduce visual crossing.", + "**Pattern-based tree analysis:** Zigzag structure can reveal symmetry properties of a tree.", + ], + + strengthsAndLimitations: { + strengths: [ + "Elegant single-pass BFS — no extra reversal step needed with the index-placement trick.", + "Produces a visually distinctive and interview-ready traversal pattern.", + "Easy to adapt from standard level-order traversal.", + ], + limitations: [ + "Requires pre-allocating a fixed-size array per level (when using the index-placement approach).", + "More complex than standard level-order due to direction management.", + "Rarely needed in production — primarily a pedagogical and interview algorithm.", + ], + }, + + whenToUseIt: + "Use zigzag level-order when you specifically need alternating left-right traversal across levels. In practice, this appears in interview problems and visualization scenarios. For standard level processing, use level-order traversal.", +}; diff --git a/src/algorithms/trees/traversal/zigzag-level-order/index.ts b/src/algorithms/trees/traversal/zigzag-level-order/index.ts new file mode 100644 index 00000000..5b2f70b1 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/index.ts @@ -0,0 +1,133 @@ +import type { AlgorithmDefinition, TreeNode } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { zigzagLevelOrder } from "./sources/zigzag-level-order.ts?fn"; +import { generateZigzagLevelOrderSteps } from "./step-generator"; +import type { ZigzagLevelOrderInput } from "./step-generator"; +import { zigzagLevelOrderEducational } from "./educational"; + +import typescriptSource from "./sources/zigzag-level-order.ts?raw"; +import pythonSource from "./sources/zigzag-level-order.py?raw"; +import javaSource from "./sources/ZigzagLevelOrder.java?raw"; + +/** Build a balanced 7-node BST: [4,2,6,1,3,5,7] */ +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +/** Execute zigzag level-order traversal using the ?fn source */ +function executeZigzagLevelOrder(input: ZigzagLevelOrderInput): number[][] { + interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; + } + const nodeMap = new Map(input.nodes.map((node) => [node.id, node])); + function toBSTNode(id: string | null): BSTNode | null { + if (!id) return null; + const node = nodeMap.get(id); + if (!node) return null; + return { + value: node.value, + left: toBSTNode(node.leftChildId), + right: toBSTNode(node.rightChildId), + }; + } + const root = toBSTNode(input.rootId); + return zigzagLevelOrder(root) as number[][]; +} + +const zigzagLevelOrderDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.ZIGZAG_LEVEL_ORDER!, + name: "Zigzag Level-Order Traversal", + category: CATEGORY.TREES!, + technique: "traversal", + description: + "BFS traversal that alternates direction per level — left-to-right then right-to-left, producing a zigzag pattern", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + nodes: defaultNodes, + rootId: "n4", + }, + }, + execute: executeZigzagLevelOrder, + generateSteps: generateZigzagLevelOrderSteps, + educational: zigzagLevelOrderEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(zigzagLevelOrderDefinition); diff --git a/src/algorithms/trees/traversal/zigzag-level-order/sources/ZigzagLevelOrder.java b/src/algorithms/trees/traversal/zigzag-level-order/sources/ZigzagLevelOrder.java new file mode 100644 index 00000000..4eaae208 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/sources/ZigzagLevelOrder.java @@ -0,0 +1,49 @@ +// Zigzag Level-Order Traversal — BFS with alternating left-right direction per level +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +class BSTNode { + int value; + BSTNode left, right; + BSTNode(int value) { this.value = value; } +} + +class ZigzagLevelOrder { + public List> zigzagLevelOrder(BSTNode root) { + List> result = new ArrayList<>(); // @step:initialize + if (root == null) return result; // @step:initialize + + Queue queue = new LinkedList<>(); // @step:initialize + queue.offer(root); // @step:initialize + boolean leftToRight = true; // @step:initialize + + while (!queue.isEmpty()) { // @step:enqueue-node + int levelSize = queue.size(); // @step:enqueue-node + Integer[] currentLevel = new Integer[levelSize]; // @step:enqueue-node + + for (int nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { // @step:dequeue-node + BSTNode node = queue.poll(); // @step:dequeue-node + + // Insert at front or back based on current direction + int insertIndex = leftToRight ? nodeIndex : levelSize - 1 - nodeIndex; // @step:visit + currentLevel[insertIndex] = node.value; // @step:visit + + if (node.left != null) { // @step:enqueue-node + queue.offer(node.left); // @step:enqueue-node + } + if (node.right != null) { // @step:enqueue-node + queue.offer(node.right); // @step:enqueue-node + } + } + + List levelList = new ArrayList<>(); // @step:visit + for (int value : currentLevel) levelList.add(value); // @step:visit + result.add(levelList); // @step:visit + leftToRight = !leftToRight; // @step:visit + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/trees/traversal/zigzag-level-order/sources/zigzag-level-order.py b/src/algorithms/trees/traversal/zigzag-level-order/sources/zigzag-level-order.py new file mode 100644 index 00000000..3ab8ec82 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/sources/zigzag-level-order.py @@ -0,0 +1,37 @@ +# Zigzag Level-Order Traversal — BFS with alternating left-right direction per level +from collections import deque + +class BSTNode: + def __init__(self, value: int): + self.value = value + self.left = None + self.right = None + +def zigzag_level_order(root) -> list: + result = [] # @step:initialize + if root is None: # @step:initialize + return result + + queue = deque([root]) # @step:initialize + left_to_right = True # @step:initialize + + while len(queue) > 0: # @step:enqueue-node + level_size = len(queue) # @step:enqueue-node + current_level = [0] * level_size # @step:enqueue-node + + for node_index in range(level_size): # @step:dequeue-node + node = queue.popleft() # @step:dequeue-node + + # Insert at front or back based on current direction + insert_index = node_index if left_to_right else level_size - 1 - node_index # @step:visit + current_level[insert_index] = node.value # @step:visit + + if node.left is not None: # @step:enqueue-node + queue.append(node.left) # @step:enqueue-node + if node.right is not None: # @step:enqueue-node + queue.append(node.right) # @step:enqueue-node + + result.append(current_level) # @step:visit + left_to_right = not left_to_right # @step:visit + + return result # @step:complete diff --git a/src/algorithms/trees/traversal/zigzag-level-order/sources/zigzag-level-order.ts b/src/algorithms/trees/traversal/zigzag-level-order/sources/zigzag-level-order.ts new file mode 100644 index 00000000..4b27f01d --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/sources/zigzag-level-order.ts @@ -0,0 +1,44 @@ +// Zigzag Level-Order Traversal — BFS with alternating left-right direction per level + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function zigzagLevelOrder(root: BSTNode | null): number[][] { + const result: number[][] = []; // @step:initialize + if (root === null) return result; // @step:initialize + + const queue: BSTNode[] = [root]; // @step:initialize + let leftToRight = true; // @step:initialize + + while (queue.length > 0) { + // @step:enqueue-node + const levelSize = queue.length; // @step:enqueue-node + const currentLevel: number[] = new Array(levelSize) as number[]; // @step:enqueue-node + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + // @step:dequeue-node + const node = queue.shift()!; // @step:dequeue-node + + // Insert at front or back based on current direction + const insertIndex = leftToRight ? nodeIndex : levelSize - 1 - nodeIndex; // @step:visit + currentLevel[insertIndex] = node.value; // @step:visit + + if (node.left !== null) { + // @step:enqueue-node + queue.push(node.left); // @step:enqueue-node + } + if (node.right !== null) { + // @step:enqueue-node + queue.push(node.right); // @step:enqueue-node + } + } + + result.push(currentLevel); // @step:visit + leftToRight = !leftToRight; // @step:visit + } + + return result; // @step:complete +} diff --git a/src/algorithms/trees/traversal/zigzag-level-order/step-generator.test.ts b/src/algorithms/trees/traversal/zigzag-level-order/step-generator.test.ts new file mode 100644 index 00000000..3f1ea66a --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/step-generator.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import type { TreeNode } from "@/types"; +import { generateZigzagLevelOrderSteps } from "./step-generator"; + +const defaultNodes: TreeNode[] = [ + { + id: "n4", + value: 4, + parentId: null, + leftChildId: "n2", + rightChildId: "n6", + state: "default", + position: { x: 200, y: 60 }, + }, + { + id: "n2", + value: 2, + parentId: "n4", + leftChildId: "n1", + rightChildId: "n3", + state: "default", + position: { x: 100, y: 160 }, + }, + { + id: "n6", + value: 6, + parentId: "n4", + leftChildId: "n5", + rightChildId: "n7", + state: "default", + position: { x: 300, y: 160 }, + }, + { + id: "n1", + value: 1, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 50, y: 260 }, + }, + { + id: "n3", + value: 3, + parentId: "n2", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 150, y: 260 }, + }, + { + id: "n5", + value: 5, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 250, y: 260 }, + }, + { + id: "n7", + value: 7, + parentId: "n6", + leftChildId: null, + rightChildId: null, + state: "default", + position: { x: 350, y: 260 }, + }, +]; + +describe("generateZigzagLevelOrderSteps", () => { + it("produces steps for a 7-node BST", () => { + const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces tree visual states", () => { + const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("tree"); + } + }); + + it("visits all 7 nodes exactly once", () => { + const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("has incrementing step indices", () => { + const steps = generateZigzagLevelOrderSteps({ nodes: defaultNodes, rootId: "n4" }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); +}); diff --git a/src/algorithms/trees/traversal/zigzag-level-order/step-generator.ts b/src/algorithms/trees/traversal/zigzag-level-order/step-generator.ts new file mode 100644 index 00000000..e1bff986 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/step-generator.ts @@ -0,0 +1,78 @@ +/** Step generator for Zigzag Level-Order Traversal — produces ExecutionStep[] using TreeTracker. */ + +import type { ExecutionStep, TreeNode } from "@/types"; +import { TreeTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BST_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.ZIGZAG_LEVEL_ORDER!); + +export interface ZigzagLevelOrderInput { + nodes: TreeNode[]; + rootId: string; +} + +export function generateZigzagLevelOrderSteps(input: ZigzagLevelOrderInput): ExecutionStep[] { + const { nodes, rootId } = input; + const tracker = new TreeTracker(nodes, rootId, BST_LINE_MAP); + + const nodeMap = new Map(nodes.map((node) => [node.id, node])); + + tracker.initialize({ rootId, queue: [rootId], leftToRight: true }); + + const queue: string[] = [rootId]; + let leftToRight = true; + let currentLevel = 0; + + while (queue.length > 0) { + const levelSize = queue.length; + const levelIds: string[] = []; + + for (let nodeIndex = 0; nodeIndex < levelSize; nodeIndex++) { + const currentId = queue.shift(); + if (!currentId) break; + const currentNode = nodeMap.get(currentId); + if (!currentNode) break; + + levelIds.push(currentId); + + if (currentNode.leftChildId) { + queue.push(currentNode.leftChildId); + tracker.traverseLeft(currentId, { + currentNode: currentId, + direction: leftToRight ? "left-to-right" : "right-to-left", + queueSize: queue.length, + }); + } + + if (currentNode.rightChildId) { + queue.push(currentNode.rightChildId); + tracker.traverseRight(currentId, { + currentNode: currentId, + direction: leftToRight ? "left-to-right" : "right-to-left", + queueSize: queue.length, + }); + } + } + + // Visit in zigzag order: left-to-right or right-to-left + const orderedIds = leftToRight ? levelIds : [...levelIds].reverse(); + for (const nodeId of orderedIds) { + const currentNode = nodeMap.get(nodeId); + if (!currentNode) continue; + tracker.visitNode(nodeId, currentNode.value, { + currentNode: nodeId, + value: currentNode.value, + level: currentLevel, + direction: leftToRight ? "left-to-right" : "right-to-left", + }); + } + + leftToRight = !leftToRight; + currentLevel += 1; + } + + tracker.complete({ result: tracker.getSteps().length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/trees/traversal/zigzag-level-order/zigzag-level-order.test.ts b/src/algorithms/trees/traversal/zigzag-level-order/zigzag-level-order.test.ts new file mode 100644 index 00000000..16e66a53 --- /dev/null +++ b/src/algorithms/trees/traversal/zigzag-level-order/zigzag-level-order.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { zigzagLevelOrder } from "./sources/zigzag-level-order.ts?fn"; + +interface BSTNode { + value: number; + left: BSTNode | null; + right: BSTNode | null; +} + +function node(value: number, left: BSTNode | null = null, right: BSTNode | null = null): BSTNode { + return { value, left, right }; +} + +describe("zigzagLevelOrder", () => { + it("traverses a balanced 7-node BST in zigzag order", () => { + const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7))); + expect(zigzagLevelOrder(root)).toEqual([[4], [6, 2], [1, 3, 5, 7]]); + }); + + it("returns an empty array for a null root", () => { + expect(zigzagLevelOrder(null)).toEqual([]); + }); + + it("handles a single-node tree", () => { + expect(zigzagLevelOrder(node(42))).toEqual([[42]]); + }); + + it("handles a two-level tree with left and right children", () => { + const root = node(1, node(2), node(3)); + expect(zigzagLevelOrder(root)).toEqual([[1], [3, 2]]); + }); + + it("handles a left-skewed tree", () => { + const root = node(3, node(2, node(1))); + expect(zigzagLevelOrder(root)).toEqual([[3], [2], [1]]); + }); +}); diff --git a/src/components/visualization/TreeVisualizer.tsx b/src/components/visualization/TreeVisualizer.tsx index ae524544..38afc60f 100644 --- a/src/components/visualization/TreeVisualizer.tsx +++ b/src/components/visualization/TreeVisualizer.tsx @@ -1,6 +1,6 @@ import { motion, useReducedMotion } from "framer-motion"; -import type { TreeVisualState, TreeNodeState } from "@/types"; +import type { TreeVisualState, TreeNodeState, TreeNode } from "@/types"; interface TreeVisualizerProps { visualState: TreeVisualState; @@ -8,56 +8,83 @@ interface TreeVisualizerProps { const NODE_RADIUS = 22; +/** Horizontal offset applied to secondary trees so they render to the right of the primary. */ +const SECONDARY_TREE_X_OFFSET = 440; + const NODE_COLORS: Record = { default: "var(--color-viz-default)", visiting: "var(--color-viz-current)", visited: "var(--color-viz-sorted)", current: "var(--color-viz-swapping)", + found: "var(--color-viz-found)", + comparing: "var(--color-viz-comparing)", + target: "var(--color-viz-current)", + highlighted: "var(--color-viz-visiting)", }; -export default function TreeVisualizer({ visualState }: TreeVisualizerProps) { - const shouldReduceMotion = useReducedMotion(); - const { nodes, visitOrder } = visualState; - +/** Renders edges and nodes for a single tree (primary or secondary). */ +function TreeGroup({ + nodes, + xOffset, + shouldReduceMotion, +}: { + nodes: TreeNode[]; + xOffset: number; + shouldReduceMotion: boolean | null; +}) { return ( -
- - {/* Edges — draw lines from each node to its children */} - {nodes.map((node) => { - const lines: React.ReactNode[] = []; + <> + {/* Edges — draw lines from each node to its children */} + {nodes.map((node) => { + const edges: React.ReactNode[] = []; - if (node.leftChildId) { - const child = nodes.find((n) => n.id === node.leftChildId); - if (child) { - lines.push( - , - ); - } + if (node.leftChildId) { + const child = nodes.find((candidate) => candidate.id === node.leftChildId); + if (child) { + edges.push( + , + ); } + } - if (node.rightChildId) { - const child = nodes.find((n) => n.id === node.rightChildId); + if (node.rightChildId) { + const child = nodes.find((candidate) => candidate.id === node.rightChildId); + if (child) { + edges.push( + , + ); + } + } + + // N-ary tree edges: render edges to all children when childrenIds is present + if (node.childrenIds) { + node.childrenIds.forEach((childId) => { + const child = nodes.find((candidate) => candidate.id === childId); if (child) { - lines.push( + edges.push( , ); } - } + }); + } + + return edges; + })} + + {/* Nodes */} + {nodes.map((node) => ( + + + + {node.value} + + + ))} + + ); +} + +export default function TreeVisualizer({ visualState }: TreeVisualizerProps) { + const shouldReduceMotion = useReducedMotion(); + const { nodes, visitOrder, operationLabel, secondaryTree } = visualState; + + const hasSecondaryTree = secondaryTree != null; + const viewBoxWidth = hasSecondaryTree ? 860 : 420; + + return ( +
+ {/* Operation label rendered above the tree when present */} + {operationLabel != null && operationLabel.length > 0 && ( +
+ {operationLabel} +
+ )} + + + - return lines; - })} - - {/* Nodes */} - {nodes.map((node) => ( - - - - {node.value} - - - ))} + {/* Secondary tree rendered offset to the right for dual-tree algorithms */} + {hasSecondaryTree && ( + + )}
diff --git a/src/trackers/advanced-tree-tracker.ts b/src/trackers/advanced-tree-tracker.ts new file mode 100644 index 00000000..61741e92 --- /dev/null +++ b/src/trackers/advanced-tree-tracker.ts @@ -0,0 +1,322 @@ +/** + * Advanced tree tracker — builds execution steps for AVL rotations, Red-Black tree + * recoloring, segment tree range queries/updates, binary indexed tree prefix sums, + * and Huffman encoding operations. + */ +import type { TreeNode, TreeNodeState, TreeVisualState } from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class AdvancedTreeTracker extends BaseTracker { + private nodes: TreeNode[]; + private rootId: string; + private visitOrder: string[] = []; + private currentNodeId: string | null = null; + private activeQueryRange: [number, number] | null = null; + private scalarResult: number | null = null; + private phase: string = "processing"; + private operationLabel: string | null = null; + + constructor(nodes: TreeNode[], rootId: string, lineMap: LineMap) { + super(lineMap); + this.nodes = nodes.map((node) => ({ ...node })); + this.rootId = rootId; + } + + /** Deep-copy current state for an immutable step snapshot. */ + private snapshot(): TreeVisualState { + return { + kind: "tree", + nodes: this.nodes.map((node) => ({ ...node })), + rootId: this.rootId, + visitOrder: [...this.visitOrder], + currentNodeId: this.currentNodeId, + queryRange: this.activeQueryRange, + scalarResult: this.scalarResult, + phase: this.phase, + operationLabel: this.operationLabel ?? undefined, + }; + } + + /** Mutate the visual state of a single node by its ID. */ + private setNodeState(nodeId: string, state: TreeNodeState): void { + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) node.state = state; + } + + /** Replace the internal node list and root (used by incremental-build algorithms). */ + updateNodes(nodes: TreeNode[], rootId: string): void { + this.nodes = nodes.map((node) => ({ ...node })); + this.rootId = rootId; + } + + initialize(label: string, variables: Record): void { + this.operationLabel = label; + this.pushStep({ + type: "initialize", + description: `Initialize advanced tree operation: ${label}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Perform a left rotation around the given pivot node. + * The caller is responsible for updating node references after rotation. + */ + rotateLeft(pivotNodeId: string, variables: Record): void { + this.currentNodeId = pivotNodeId; + this.setNodeState(pivotNodeId, "current"); + this.pushStep({ + type: "rotate-left", + description: `Rotate left around node ${pivotNodeId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Perform a right rotation around the given pivot node. + * The caller is responsible for updating node references after rotation. + */ + rotateRight(pivotNodeId: string, variables: Record): void { + this.currentNodeId = pivotNodeId; + this.setNodeState(pivotNodeId, "current"); + this.pushStep({ + type: "rotate-right", + description: `Rotate right around node ${pivotNodeId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Recolor a node in a Red-Black tree (toggle between red and black). + * The new color is conveyed via variables and the node is visually highlighted. + */ + recolorNode(nodeId: string, newColor: "red" | "black", variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, newColor === "red" ? "comparing" : "visited"); + this.pushStep({ + type: "recolor-node", + description: `Recolor node ${nodeId} to ${newColor}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Update a segment tree node's value for a range update or point update. + * Marks the node as current during the update. + */ + updateSegment(nodeId: string, newValue: number, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "current"); + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) { + node.value = newValue; + } + this.pushStep({ + type: "update-segment", + description: `Update segment node ${nodeId} to value ${String(newValue)}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "visited"); + } + + /** + * Perform a range query on the segment tree. + * Records the query bounds and marks nodes being checked during traversal. + */ + queryRange( + nodeId: string, + rangeLow: number, + rangeHigh: number, + variables: Record, + ): void { + this.currentNodeId = nodeId; + this.activeQueryRange = [rangeLow, rangeHigh]; + this.setNodeState(nodeId, "comparing"); + this.pushStep({ + type: "query-range", + description: `Query range [${String(rangeLow)}, ${String(rangeHigh)}] at node ${nodeId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Compute a prefix sum in a Binary Indexed Tree (Fenwick Tree). + * Records the partial sum accumulated so far. + */ + computePrefix(index: number, partialSum: number, variables: Record): void { + this.scalarResult = partialSum; + this.pushStep({ + type: "compute-prefix", + description: `Compute prefix sum at index ${String(index)}: partial sum = ${String(partialSum)}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Insert a new node (for AVL/RB incremental build algorithms). */ + insertNode(nodeId: string, value: number, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "current"); + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.pushStep({ + type: "insert-child", + description: `Insert node with value ${String(value)}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "visited"); + } + + /** Check the balance factor of a node (AVL). */ + checkBalance(nodeId: string, balance: number, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "comparing"); + this.pushStep({ + type: "check-balance", + description: `Check balance factor at node ${nodeId}: ${String(balance)}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "default"); + } + + /** Visit a node (for traversal algorithms). */ + visitNode(nodeId: string, value: number, variables: Record): void { + this.currentNodeId = nodeId; + this.visitOrder.push(nodeId); + this.setNodeState(nodeId, "visited"); + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.pushStep({ + type: "visit", + description: `Visit node ${nodeId} (value: ${String(value)})`, + variables, + visualState: this.snapshot(), + }); + } + + /** Traverse to the next child in n-ary tree traversal. */ + traverseNext(fromId: string, toId: string, variables: Record): void { + this.currentNodeId = fromId; + this.setNodeState(fromId, "current"); + this.pushStep({ + type: "traverse-next", + description: `Traverse from node ${fromId} to child ${toId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Build a new segment/BIT/Huffman node. */ + buildNode(nodeId: string, value: number, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "current"); + const existingNode = this.nodes.find((candidate) => candidate.id === nodeId); + if (existingNode) existingNode.value = value; + this.pushStep({ + type: "build-node", + description: `Build node ${nodeId} with value ${String(value)}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "visited"); + } + + /** Connect a child to a parent node (Huffman/expression tree construction). */ + connectChild(parentId: string, childId: string, variables: Record): void { + this.currentNodeId = parentId; + this.setNodeState(childId, "current"); + this.pushStep({ + type: "connect-child", + description: `Connect child ${childId} to parent ${parentId}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(childId, "visited"); + } + + /** Mark a character encoding result (Huffman). */ + encodeChar( + nodeId: string, + char: string, + encoding: string, + variables: Record, + ): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "found"); + this.pushStep({ + type: "encode-char", + description: `Encode character '${char}' as '${encoding}'`, + variables, + visualState: this.snapshot(), + }); + } + + /** Select the minimum frequency node from the priority queue (Huffman). */ + selectMinFreq(nodeId: string, freq: number, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "comparing"); + this.pushStep({ + type: "select-min-freq", + description: `Select minimum frequency node ${nodeId} (freq: ${String(freq)})`, + variables, + visualState: this.snapshot(), + }); + } + + /** Check a node for pruning (binary tree pruning). */ + checkNode(nodeId: string, value: number, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "comparing"); + this.pushStep({ + type: "check-balance", + description: `Check node ${nodeId} (value: ${String(value)}) — should it be pruned?`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "default"); + } + + /** Detach a node (remove subtree during pruning). */ + detachNode(nodeId: string, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "target"); + this.pushStep({ + type: "detach-node", + description: `Detach node ${nodeId} (subtree pruned — contains no 1s)`, + variables, + visualState: this.snapshot(), + }); + this.nodes = this.nodes.filter((candidate) => candidate.id !== nodeId); + } + + /** Mark a node as processed (tree-to-DLL, pruning post-processing). */ + markProcessed(nodeId: string, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "visited"); + this.pushStep({ + type: "visit", + description: `Mark node ${nodeId} as processed`, + variables, + visualState: this.snapshot(), + }); + } + + complete(variables: Record): void { + this.currentNodeId = null; + this.phase = "complete"; + this.pushStep({ + type: "complete", + description: "Advanced tree operation complete", + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/trackers/bst-operation-tracker.ts b/src/trackers/bst-operation-tracker.ts new file mode 100644 index 00000000..5c0aa7d6 --- /dev/null +++ b/src/trackers/bst-operation-tracker.ts @@ -0,0 +1,155 @@ +/** + * BST operation tracker — builds execution steps for BST search, insert, delete, + * and related operations. Tracks comparisons, path traversal, and structural mutations. + */ +import type { TreeNode, TreeNodeState, TreeVisualState } from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class BSTOperationTracker extends BaseTracker { + private nodes: TreeNode[]; + private rootId: string; + private visitOrder: string[] = []; + private currentNodeId: string | null = null; + private currentPath: string[] = []; + private targetValue: number | null = null; + private phase: string = "searching"; + + constructor(nodes: TreeNode[], rootId: string, lineMap: LineMap) { + super(lineMap); + this.nodes = nodes.map((node) => ({ ...node })); + this.rootId = rootId; + } + + /** Deep-copy the current node array for an immutable step snapshot. */ + private snapshot(): TreeVisualState { + return { + kind: "tree", + nodes: this.nodes.map((node) => ({ ...node })), + rootId: this.rootId, + visitOrder: [...this.visitOrder], + currentNodeId: this.currentNodeId, + currentPath: [...this.currentPath], + targetValue: this.targetValue, + phase: this.phase, + }; + } + + /** Mutate the visual state of a single node by its ID. */ + private setNodeState(nodeId: string, state: TreeNodeState): void { + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) node.state = state; + } + + initialize(targetVal: number | null, variables: Record): void { + this.targetValue = targetVal; + this.pushStep({ + type: "initialize", + description: "Initialize BST operation", + variables, + visualState: this.snapshot(), + }); + } + + /** + * Compare the target value against the current node's value, deciding + * whether to go left, go right, or declare a match. + */ + compareNode(nodeId: string, nodeValue: number, variables: Record): void { + this.metrics = { ...this.metrics, comparisons: this.metrics.comparisons + 1 }; + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "comparing"); + this.currentPath.push(nodeId); + this.pushStep({ + type: "search-node", + description: `Compare target with node ${nodeId} (value: ${String(nodeValue)})`, + variables, + visualState: this.snapshot(), + }); + } + + /** Mark the node at which the search target was found. */ + searchFound(nodeId: string, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "found"); + this.visitOrder.push(nodeId); + this.pushStep({ + type: "found", + description: `Found target at node ${nodeId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Insert a new node as a child of an existing node. */ + insertNode(parentId: string | null, newNodeId: string, variables: Record): void { + this.currentNodeId = newNodeId; + const newNode = this.nodes.find((candidate) => candidate.id === newNodeId); + if (newNode) { + newNode.state = "current"; + } + this.pushStep({ + type: "insert-child", + description: + parentId != null + ? `Insert node ${newNodeId} as child of ${parentId}` + : `Insert node ${newNodeId} as new root`, + variables, + visualState: this.snapshot(), + }); + if (newNode) { + newNode.state = "visited"; + } + } + + /** Mark a node as deleted and detach it from the tree. */ + deleteNode(nodeId: string, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "target"); + this.pushStep({ + type: "delete-child", + description: `Delete node ${nodeId} from BST`, + variables, + visualState: this.snapshot(), + }); + this.nodes = this.nodes.filter((candidate) => candidate.id !== nodeId); + } + + /** Highlight the in-order successor node used during BST deletion. */ + findSuccessor(successorId: string, variables: Record): void { + this.currentNodeId = successorId; + this.setNodeState(successorId, "found"); + this.pushStep({ + type: "search-node", + description: `Find in-order successor: node ${successorId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Highlight all nodes along the current traversal path. */ + markPath(pathNodeIds: string[], variables: Record): void { + this.currentPath = [...pathNodeIds]; + pathNodeIds.forEach((nodeId) => { + this.setNodeState(nodeId, "visiting"); + }); + this.pushStep({ + type: "traverse-left", + description: `Mark traversal path: ${pathNodeIds.join(" → ")}`, + variables, + visualState: this.snapshot(), + }); + } + + complete(variables: Record): void { + this.currentNodeId = null; + this.phase = "complete"; + this.pushStep({ + type: "complete", + description: "BST operation complete", + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/trackers/index.ts b/src/trackers/index.ts index c339547d..85ab3be0 100644 --- a/src/trackers/index.ts +++ b/src/trackers/index.ts @@ -30,3 +30,8 @@ export { SetGenerationTracker } from "./set-generation-tracker"; export { SetMembershipTracker } from "./set-membership-tracker"; export { DisjointSetTracker } from "./disjoint-set-tracker"; export { SetCoverTracker } from "./set-cover-tracker"; +export { BSTOperationTracker } from "./bst-operation-tracker"; +export { TreePropertyTracker } from "./tree-property-tracker"; +export { TreeConstructionTracker } from "./tree-construction-tracker"; +export { TreeManipulationTracker } from "./tree-manipulation-tracker"; +export { AdvancedTreeTracker } from "./advanced-tree-tracker"; diff --git a/src/trackers/tree-construction-tracker.ts b/src/trackers/tree-construction-tracker.ts new file mode 100644 index 00000000..be8b46ff --- /dev/null +++ b/src/trackers/tree-construction-tracker.ts @@ -0,0 +1,159 @@ +/** + * Tree construction tracker — builds execution steps for algorithms that construct + * a binary tree from serialized input (preorder+inorder arrays, level-order, serialized string). + * Tracks element selection, node creation, child linking, and array partitioning. + */ +import type { TreeNode, TreeNodeState, TreeVisualState } from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class TreeConstructionTracker extends BaseTracker { + private nodes: TreeNode[]; + private rootId: string; + private visitOrder: string[] = []; + private currentNodeId: string | null = null; + private phase: string = "building"; + + constructor(nodes: TreeNode[], rootId: string, lineMap: LineMap) { + super(lineMap); + this.nodes = nodes.map((node) => ({ ...node })); + this.rootId = rootId; + } + + /** Deep-copy current state for an immutable step snapshot. */ + private snapshot(): TreeVisualState { + return { + kind: "tree", + nodes: this.nodes.map((node) => ({ ...node })), + rootId: this.rootId, + visitOrder: [...this.visitOrder], + currentNodeId: this.currentNodeId, + phase: this.phase, + }; + } + + /** Mutate the visual state of a single node by its ID. */ + private setNodeState(nodeId: string, state: TreeNodeState): void { + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) node.state = state; + } + + initialize(variables: Record): void { + this.pushStep({ + type: "initialize", + description: "Initialize tree construction", + variables, + visualState: this.snapshot(), + }); + } + + /** + * Highlight an element being selected from the input array + * as the root/value for the next node to be created. + */ + selectElement(elementValue: number, variables: Record): void { + this.pushStep({ + type: "search-node", + description: `Select element ${String(elementValue)} from input array`, + variables, + visualState: this.snapshot(), + }); + } + + /** Add a newly created node to the tree and mark it as current. */ + buildNode(nodeId: string, nodeValue: number, variables: Record): void { + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.currentNodeId = nodeId; + const existingNode = this.nodes.find((candidate) => candidate.id === nodeId); + if (existingNode) { + existingNode.state = "current"; + } else { + // Node doesn't exist yet — add it (construction algorithms may add nodes incrementally) + this.nodes.push({ + id: nodeId, + value: nodeValue, + parentId: null, + leftChildId: null, + rightChildId: null, + state: "current", + position: { x: 0, y: 0 }, + }); + } + this.pushStep({ + type: "build-node", + description: `Build node ${nodeId} with value ${String(nodeValue)}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "visiting"); + } + + /** Link a newly built node to its parent as left or right child. */ + connectChild( + parentId: string, + childId: string, + side: "left" | "right", + variables: Record, + ): void { + const parentNode = this.nodes.find((candidate) => candidate.id === parentId); + if (parentNode) { + if (side === "left") { + parentNode.leftChildId = childId; + } else { + parentNode.rightChildId = childId; + } + } + const childNode = this.nodes.find((candidate) => candidate.id === childId); + if (childNode) { + childNode.parentId = parentId; + } + this.pushStep({ + type: "connect-child", + description: `Connect node ${childId} as ${side} child of ${parentId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Record an array partition step — used when splitting inorder/preorder arrays + * into left and right sub-arrays during recursive construction. + */ + partitionArray( + leftBound: number, + rightBound: number, + pivotIndex: number, + variables: Record, + ): void { + this.pushStep({ + type: "partition-array", + description: `Partition array [${String(leftBound)}..${String(rightBound)}] at pivot index ${String(pivotIndex)}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Mark a node as fully built (all children connected). */ + markBuilt(nodeId: string, variables: Record): void { + this.setNodeState(nodeId, "visited"); + this.visitOrder.push(nodeId); + this.pushStep({ + type: "visit", + description: `Node ${nodeId} fully constructed`, + variables, + visualState: this.snapshot(), + }); + } + + complete(variables: Record): void { + this.currentNodeId = null; + this.phase = "complete"; + this.pushStep({ + type: "complete", + description: "Tree construction complete", + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/trackers/tree-manipulation-tracker.ts b/src/trackers/tree-manipulation-tracker.ts new file mode 100644 index 00000000..d9427a4d --- /dev/null +++ b/src/trackers/tree-manipulation-tracker.ts @@ -0,0 +1,215 @@ +/** + * Tree manipulation tracker — builds execution steps for algorithms that structurally + * transform or compare trees, such as invert-binary-tree, flatten, same-tree, merge, + * and lowest-common-ancestor. + */ +import type { TreeNode, TreeNodeState, TreeVisualState } from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class TreeManipulationTracker extends BaseTracker { + private nodes: TreeNode[]; + private rootId: string; + private visitOrder: string[] = []; + private currentNodeId: string | null = null; + private secondaryTree: { nodes: TreeNode[]; rootId: string } | null = null; + private phase: string = "processing"; + private booleanResult: boolean | null = null; + private operationLabel: string | null = null; + + constructor( + nodes: TreeNode[], + rootId: string, + lineMap: LineMap, + secondaryNodes?: TreeNode[], + secondaryRootId?: string, + ) { + super(lineMap); + this.nodes = nodes.map((node) => ({ ...node })); + this.rootId = rootId; + if (secondaryNodes != null && secondaryRootId != null) { + this.secondaryTree = { + nodes: secondaryNodes.map((node) => ({ ...node })), + rootId: secondaryRootId, + }; + } + } + + /** Deep-copy current state for an immutable step snapshot. */ + private snapshot(): TreeVisualState { + return { + kind: "tree", + nodes: this.nodes.map((node) => ({ ...node })), + rootId: this.rootId, + visitOrder: [...this.visitOrder], + currentNodeId: this.currentNodeId, + secondaryTree: + this.secondaryTree != null + ? { + nodes: this.secondaryTree.nodes.map((node) => ({ ...node })), + rootId: this.secondaryTree.rootId, + } + : null, + phase: this.phase, + booleanResult: this.booleanResult, + operationLabel: this.operationLabel ?? undefined, + }; + } + + /** Mutate the visual state of a single node in the primary tree. */ + private setNodeState(nodeId: string, state: TreeNodeState): void { + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) node.state = state; + } + + /** Mutate the visual state of a single node in the secondary tree. */ + private setSecondaryNodeState(nodeId: string, state: TreeNodeState): void { + if (this.secondaryTree == null) return; + const node = this.secondaryTree.nodes.find((candidate) => candidate.id === nodeId); + if (node) node.state = state; + } + + initialize(label: string, variables: Record): void { + this.operationLabel = label; + this.pushStep({ + type: "initialize", + description: `Initialize tree manipulation: ${label}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Swap the left and right children of a node (used by invert-binary-tree). */ + swapChildren(nodeId: string, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "current"); + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) { + const leftId = node.leftChildId; + node.leftChildId = node.rightChildId; + node.rightChildId = leftId; + } + this.pushStep({ + type: "swap-children", + description: `Swap children of node ${nodeId}`, + variables, + visualState: this.snapshot(), + }); + this.setNodeState(nodeId, "visited"); + } + + /** + * Move a node to a new position in the tree + * (used by flatten-to-linked-list and similar structural transforms). + */ + moveNode( + nodeId: string, + newParentId: string | null, + side: "left" | "right", + variables: Record, + ): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "current"); + const parentNode = + newParentId != null + ? this.nodes.find((candidate) => candidate.id === newParentId) + : undefined; + if (parentNode) { + if (side === "left") { + parentNode.leftChildId = nodeId; + } else { + parentNode.rightChildId = nodeId; + } + } + const movedNode = this.nodes.find((candidate) => candidate.id === nodeId); + if (movedNode) { + movedNode.parentId = newParentId; + } + this.pushStep({ + type: "connect-child", + description: `Move node ${nodeId} to ${side} child of ${newParentId ?? "null"}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Compare a node in the primary tree against its counterpart in the secondary tree. */ + compareNodes( + primaryNodeId: string, + secondaryNodeId: string, + variables: Record, + ): void { + this.metrics = { ...this.metrics, comparisons: this.metrics.comparisons + 1 }; + this.currentNodeId = primaryNodeId; + this.setNodeState(primaryNodeId, "comparing"); + this.setSecondaryNodeState(secondaryNodeId, "comparing"); + this.pushStep({ + type: "search-node", + description: `Compare node ${primaryNodeId} against node ${secondaryNodeId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Merge values from two nodes into the primary tree node. */ + mergeNodes( + primaryNodeId: string, + secondaryNodeId: string, + mergedValue: number, + variables: Record, + ): void { + this.currentNodeId = primaryNodeId; + this.setNodeState(primaryNodeId, "current"); + this.setSecondaryNodeState(secondaryNodeId, "visited"); + const primaryNode = this.nodes.find((candidate) => candidate.id === primaryNodeId); + if (primaryNode) { + primaryNode.value = mergedValue; + } + this.pushStep({ + type: "merge-node", + description: `Merge node ${primaryNodeId} and node ${secondaryNodeId} → value ${String(mergedValue)}`, + variables, + visualState: this.snapshot(), + }); + } + + /** + * Detach a node from the tree by removing its parent reference + * (used when flattening or restructuring). + */ + detachNode(nodeId: string, variables: Record): void { + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "target"); + this.pushStep({ + type: "detach-node", + description: `Detach node ${nodeId} from parent`, + variables, + visualState: this.snapshot(), + }); + } + + /** Mark a node as fully processed in the current manipulation pass. */ + markProcessed(nodeId: string, variables: Record): void { + this.setNodeState(nodeId, "visited"); + this.visitOrder.push(nodeId); + this.pushStep({ + type: "visit", + description: `Mark node ${nodeId} as processed`, + variables, + visualState: this.snapshot(), + }); + } + + complete(result: boolean | null, variables: Record): void { + this.currentNodeId = null; + this.booleanResult = result; + this.phase = "complete"; + this.pushStep({ + type: "complete", + description: `Tree manipulation complete${result != null ? ` — result: ${String(result)}` : ""}`, + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/trackers/tree-property-tracker.ts b/src/trackers/tree-property-tracker.ts new file mode 100644 index 00000000..266ceb42 --- /dev/null +++ b/src/trackers/tree-property-tracker.ts @@ -0,0 +1,156 @@ +/** + * Tree property tracker — builds execution steps for property-checking algorithms such as + * maximum depth, is-balanced, is-symmetric, path-sum, and diameter. + * Tracks node validation, depth/height updates, path tracing, and scalar/boolean results. + */ +import type { TreeNode, TreeNodeState, TreeVisualState } from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class TreePropertyTracker extends BaseTracker { + private nodes: TreeNode[]; + private rootId: string; + private visitOrder: string[] = []; + private currentNodeId: string | null = null; + private currentPath: string[] = []; + private scalarResult: number | null = null; + private booleanResult: boolean | null = null; + private phase: string = "checking"; + + constructor(nodes: TreeNode[], rootId: string, lineMap: LineMap) { + super(lineMap); + this.nodes = nodes.map((node) => ({ ...node })); + this.rootId = rootId; + } + + /** Deep-copy current state for an immutable step snapshot. */ + private snapshot(): TreeVisualState { + return { + kind: "tree", + nodes: this.nodes.map((node) => ({ ...node })), + rootId: this.rootId, + visitOrder: [...this.visitOrder], + currentNodeId: this.currentNodeId, + currentPath: [...this.currentPath], + scalarResult: this.scalarResult, + booleanResult: this.booleanResult, + phase: this.phase, + }; + } + + /** Mutate the visual state of a single node by its ID. */ + private setNodeState(nodeId: string, state: TreeNodeState): void { + const node = this.nodes.find((candidate) => candidate.id === nodeId); + if (node) node.state = state; + } + + initialize(variables: Record): void { + this.pushStep({ + type: "initialize", + description: "Initialize tree property check", + variables, + visualState: this.snapshot(), + }); + } + + /** Mark the given node as currently being checked. */ + checkNode(nodeId: string, variables: Record): void { + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.currentNodeId = nodeId; + this.setNodeState(nodeId, "visiting"); + this.currentPath.push(nodeId); + this.pushStep({ + type: "visit", + description: `Check node ${nodeId}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Record a depth value update at the given node. */ + updateDepth(nodeId: string, depth: number, variables: Record): void { + this.scalarResult = depth; + this.setNodeState(nodeId, "current"); + this.pushStep({ + type: "update-height", + description: `Update depth at node ${nodeId} to ${String(depth)}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Record a height value update at the given node. */ + updateHeight(nodeId: string, height: number, variables: Record): void { + this.scalarResult = height; + this.setNodeState(nodeId, "current"); + this.pushStep({ + type: "update-height", + description: `Update height at node ${nodeId} to ${String(height)}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Mark the node as passing its property check. */ + markValid(nodeId: string, variables: Record): void { + this.booleanResult = true; + this.setNodeState(nodeId, "visited"); + this.pushStep({ + type: "check-balance", + description: `Node ${nodeId} passes property check`, + variables, + visualState: this.snapshot(), + }); + } + + /** Mark the node as failing its property check. */ + markInvalid(nodeId: string, variables: Record): void { + this.booleanResult = false; + this.setNodeState(nodeId, "target"); + this.pushStep({ + type: "check-balance", + description: `Node ${nodeId} fails property check`, + variables, + visualState: this.snapshot(), + }); + } + + /** Update the tracked path of node IDs (e.g. root-to-leaf path for path-sum). */ + trackPath(pathNodeIds: string[], variables: Record): void { + this.currentPath = [...pathNodeIds]; + pathNodeIds.forEach((nodeId) => this.setNodeState(nodeId, "highlighted")); + this.pushStep({ + type: "traverse-left", + description: `Track path: ${pathNodeIds.join(" → ")}`, + variables, + visualState: this.snapshot(), + }); + } + + /** Record the final scalar result (e.g. max depth, diameter, path sum). */ + recordResult(result: number | boolean, variables: Record): void { + if (typeof result === "boolean") { + this.booleanResult = result; + } else { + this.scalarResult = result; + } + this.pushStep({ + type: "add-to-result", + description: `Record result: ${String(result)}`, + variables, + visualState: this.snapshot(), + }); + } + + complete(variables: Record): void { + this.currentNodeId = null; + this.phase = "complete"; + this.pushStep({ + type: "complete", + description: "Tree property check complete", + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/types/execution.ts b/src/types/execution.ts index f4d88270..50a224a6 100644 --- a/src/types/execution.ts +++ b/src/types/execution.ts @@ -148,6 +148,31 @@ export type StepType = | "hash-compute" | "hash-match" | "skip-char" + | "search-node" + | "insert-child" + | "delete-child" + | "rotate-left" + | "rotate-right" + | "recolor-node" + | "check-balance" + | "update-height" + | "build-node" + | "connect-child" + | "partition-array" + | "swap-children" + | "merge-node" + | "detach-node" + | "query-range" + | "update-segment" + | "compute-prefix" + | "encode-char" + | "select-min-freq" + | "push-to-stack" + | "pop-from-stack" + | "enqueue-node" + | "dequeue-node" + | "thread-node" + | "unthread-node" | "complete"; /** Maps a language to the source lines highlighted for this step. */ @@ -379,7 +404,15 @@ export interface DPTableVisualState { /* Tree Structure */ /* -------------------------------------------------------------------------- */ -export type TreeNodeState = "default" | "visiting" | "visited" | "current"; +export type TreeNodeState = + | "default" + | "visiting" + | "visited" + | "current" + | "found" + | "comparing" + | "target" + | "highlighted"; export interface TreeNode { id: string; @@ -387,6 +420,8 @@ export interface TreeNode { parentId: string | null; leftChildId: string | null; rightChildId: string | null; + /** Child IDs for n-ary, expression, and huffman trees */ + childrenIds?: string[]; state: TreeNodeState; /** Pre-computed position for SVG rendering (level-based hierarchical layout) */ position: { x: number; y: number }; @@ -401,6 +436,24 @@ export interface TreeVisualState { visitOrder: string[]; /** ID of the node currently being processed */ currentNodeId: string | null; + /** Target value being searched/operated on */ + targetValue?: number | null; + /** Node IDs forming the current active path */ + currentPath?: string[]; + /** Current phase label (e.g., "searching", "inserting", "deleting") */ + phase?: string; + /** Current BFS level being processed */ + currentLevel?: number; + /** Secondary tree for dual-tree algorithms (same-tree, merge, subtree-of) */ + secondaryTree?: { nodes: TreeNode[]; rootId: string } | null; + /** Label describing the current operation */ + operationLabel?: string; + /** Range being queried in segment tree operations */ + queryRange?: [number, number] | null; + /** Scalar result value (depth, sum, diameter, etc.) */ + scalarResult?: number | null; + /** Boolean result (is balanced, is symmetric, etc.) */ + booleanResult?: boolean | null; } /* -------------------------------------------------------------------------- */ diff --git a/src/types/fn-import.d.ts b/src/types/fn-import.d.ts index 26ca99fd..64fe556e 100644 --- a/src/types/fn-import.d.ts +++ b/src/types/fn-import.d.ts @@ -192,6 +192,98 @@ declare module "*.ts?fn" { export const unionFindCycle: (...args: any[]) => any; // Trees export const bstInorder: (...args: any[]) => any; + export const bstInorderIterative: (...args: any[]) => any; + export const bstPreorder: (...args: any[]) => any; + export const bstPreorderIterative: (...args: any[]) => any; + export const bstPostorder: (...args: any[]) => any; + export const bstPostorderIterative: (...args: any[]) => any; + export const levelOrderTraversal: (...args: any[]) => any; + export const reverseLevelOrder: (...args: any[]) => any; + export const zigzagLevelOrder: (...args: any[]) => any; + export const verticalOrderTraversal: (...args: any[]) => any; + export const boundaryTraversal: (...args: any[]) => any; + export const treeDiagonalTraversal: (...args: any[]) => any; + export const morrisInorderTraversal: (...args: any[]) => any; + // BST Operations + export const bstSearch: (...args: any[]) => any; + export const bstSearchIterative: (...args: any[]) => any; + export const bstFloorCeil: (...args: any[]) => any; + export const bstFloorCeilIterative: (...args: any[]) => any; + export const bstInsert: (...args: any[]) => any; + export const bstInsertIterative: (...args: any[]) => any; + export const bstDelete: (...args: any[]) => any; + export const bstDeleteIterative: (...args: any[]) => any; + export const bstValidation: (...args: any[]) => any; + export const bstValidationIterative: (...args: any[]) => any; + export const bstKthSmallest: (...args: any[]) => any; + export const bstKthSmallestIterative: (...args: any[]) => any; + export const bstRangeSum: (...args: any[]) => any; + export const bstRangeSumIterative: (...args: any[]) => any; + export const bstLowestCommonAncestor: (...args: any[]) => any; + export const bstLowestCommonAncestorIterative: (...args: any[]) => any; + export const bstFromSortedArray: (...args: any[]) => any; + export const bstIterator: (...args: any[]) => any; + export const bstToGreaterTree: (...args: any[]) => any; + export const bstToGreaterTreeIterative: (...args: any[]) => any; + export const bstRecoverSwapped: (...args: any[]) => any; + // Tree Properties + export const maximumDepth: (...args: any[]) => any; + export const maximumDepthIterative: (...args: any[]) => any; + export const minimumDepth: (...args: any[]) => any; + export const minimumDepthIterative: (...args: any[]) => any; + export const diameterOfBinaryTree: (...args: any[]) => any; + export const isBalancedTree: (...args: any[]) => any; + export const isBalancedTreeIterative: (...args: any[]) => any; + export const isSymmetricTree: (...args: any[]) => any; + export const isSymmetricTreeIterative: (...args: any[]) => any; + export const countCompleteTreeNodes: (...args: any[]) => any; + export const binaryTreeTilt: (...args: any[]) => any; + export const pathSum: (...args: any[]) => any; + export const pathSumIterative: (...args: any[]) => any; + export const maximumPathSum: (...args: any[]) => any; + export const sumOfLeftLeaves: (...args: any[]) => any; + export const sumOfLeftLeavesIterative: (...args: any[]) => any; + export const allRootToLeafPaths: (...args: any[]) => any; + export const allRootToLeafPathsIterative: (...args: any[]) => any; + export const sumRootToLeafNumbers: (...args: any[]) => any; + export const sumRootToLeafNumbersIterative: (...args: any[]) => any; + export const cousinsInBinaryTree: (...args: any[]) => any; + // Tree Manipulation + export const invertBinaryTree: (...args: any[]) => any; + export const invertBinaryTreeIterative: (...args: any[]) => any; + export const flattenToLinkedList: (...args: any[]) => any; + export const flattenToLinkedListIterative: (...args: any[]) => any; + export const rightSideView: (...args: any[]) => any; + export const rightSideViewRecursive: (...args: any[]) => any; + export const sameTree: (...args: any[]) => any; + export const sameTreeIterative: (...args: any[]) => any; + export const mergeBinaryTrees: (...args: any[]) => any; + export const mergeBinaryTreesIterative: (...args: any[]) => any; + export const subtreeOfAnotherTree: (...args: any[]) => any; + export const lowestCommonAncestor: (...args: any[]) => any; + export const lowestCommonAncestorIterative: (...args: any[]) => any; + export const deleteLeavesWithValue: (...args: any[]) => any; + export const distributeCoins: (...args: any[]) => any; + export const flipEquivalentTrees: (...args: any[]) => any; + // Tree Advanced + export const avlInsertRotation: (...args: any[]) => any; + export const redBlackInsert: (...args: any[]) => any; + export const segmentTreeRangeSum: (...args: any[]) => any; + export const segmentTreeRangeMin: (...args: any[]) => any; + export const binaryIndexedTree: (...args: any[]) => any; + export const treeToDoublyLinkedList: (...args: any[]) => any; + export const binaryTreePruning: (...args: any[]) => any; + export const nAryTreeTraversal: (...args: any[]) => any; + export const huffmanCodingTree: (...args: any[]) => any; + export const expressionTreeEvaluation: (...args: any[]) => any; + // Tree Construction + export const buildFromPreorderInorder: (...args: any[]) => any; + export const buildFromPreorderInorderIterative: (...args: any[]) => any; + export const buildFromPostorderInorder: (...args: any[]) => any; + export const buildFromPostorderInorderIterative: (...args: any[]) => any; + export const serializeTree: (...args: any[]) => any; + export const deserializeTree: (...args: any[]) => any; + export const buildFromLevelOrder: (...args: any[]) => any; // Pathfinding export const dijkstra: (...args: any[]) => any; export const bfsShortestPath: (...args: any[]) => any;