From 4654ff76378292dafde8a14cb18158d4cef72939 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Fri, 12 Sep 2025 15:08:18 +0100 Subject: [PATCH] feat(viz): trie visualisation --- .../visualizations-new/algorithms/trie.tsx | 54 ++ .../data-structures/trie-view.tsx | 252 +++++++ .../primitives/trie-node.tsx | 107 +++ .../src/content/algorithms/implement-trie.ts | 701 ++++++++++++++++++ 4 files changed, 1114 insertions(+) create mode 100644 frontend/src/components/visualizations-new/algorithms/trie.tsx create mode 100644 frontend/src/components/visualizations-new/data-structures/trie-view.tsx create mode 100644 frontend/src/components/visualizations-new/primitives/trie-node.tsx create mode 100644 frontend/src/content/algorithms/implement-trie.ts diff --git a/frontend/src/components/visualizations-new/algorithms/trie.tsx b/frontend/src/components/visualizations-new/algorithms/trie.tsx new file mode 100644 index 0000000..766556f --- /dev/null +++ b/frontend/src/components/visualizations-new/algorithms/trie.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { useVisualization } from '@/lib/visualizations/use-visualization'; +import type { AlgorithmDefinition } from '@/lib/visualizations/types'; +import { VisualizationContainer } from '../core/visualization-container'; +import { TrieView } from '../data-structures/trie-view'; + +interface TrieVisualizationProps { + algorithm: AlgorithmDefinition; + className?: string; +} + +export function TrieVisualization({ + algorithm, + className, +}: TrieVisualizationProps) { + const { + currentStep, + currentStepIndex, + totalSteps, + playback, + controls, + currentPhase, + progress, + } = useVisualization(algorithm); + + const { dataState } = currentStep; + const trie = dataState.tries?.[0] ?? null; + + return ( + +
+ {trie && } +
+
+ ); +} diff --git a/frontend/src/components/visualizations-new/data-structures/trie-view.tsx b/frontend/src/components/visualizations-new/data-structures/trie-view.tsx new file mode 100644 index 0000000..1cdd103 --- /dev/null +++ b/frontend/src/components/visualizations-new/data-structures/trie-view.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import type { TrieState, TrieNodeState } from '@/lib/visualizations/types'; +import { TrieNode } from '../primitives/trie-node'; + +interface TrieViewProps { + trie: TrieState; + className?: string; +} + +const NODE_RADIUS = 20; +const LEVEL_HEIGHT = 64; +const MIN_NODE_SPACING = 48; +const SVG_PADDING = 40; + +interface NodePosition { + node: TrieNodeState; + x: number; + y: number; + parentX?: number; + parentY?: number; +} + +function buildNodeMap(nodes: TrieNodeState[]): Map { + const map = new Map(); + for (const node of nodes) { + map.set(node.id, node); + } + return map; +} + +/** + * Calculate the width needed for a subtree rooted at the given node. + * Leaf nodes need MIN_NODE_SPACING, internal nodes need sum of children widths. + */ +function calculateSubtreeWidth( + nodeId: string, + nodeMap: Map +): number { + const node = nodeMap.get(nodeId); + if (!node) return MIN_NODE_SPACING; + + if (node.children.length === 0) { + return MIN_NODE_SPACING; + } + + let totalWidth = 0; + for (const childId of node.children) { + totalWidth += calculateSubtreeWidth(childId, nodeMap); + } + + return Math.max(totalWidth, MIN_NODE_SPACING); +} + +/** + * Calculate the maximum depth of the trie. + */ +function calculateTrieDepth( + nodeId: string, + nodeMap: Map +): number { + const node = nodeMap.get(nodeId); + if (!node) return 0; + + if (node.children.length === 0) { + return 1; + } + + let maxChildDepth = 0; + for (const childId of node.children) { + const childDepth = calculateTrieDepth(childId, nodeMap); + maxChildDepth = Math.max(maxChildDepth, childDepth); + } + + return 1 + maxChildDepth; +} + +/** + * Calculate positions for all nodes using N-ary tree layout. + * Each node is centered above its children. + */ +function calculatePositions( + trie: TrieState, + totalWidth: number +): NodePosition[] { + const nodeMap = buildNodeMap(trie.nodes); + const positions: NodePosition[] = []; + + // Pre-calculate subtree widths for all nodes + const subtreeWidths = new Map(); + for (const node of trie.nodes) { + subtreeWidths.set(node.id, calculateSubtreeWidth(node.id, nodeMap)); + } + + function traverse( + nodeId: string, + level: number, + left: number, + right: number, + parentX?: number, + parentY?: number + ) { + const node = nodeMap.get(nodeId); + if (!node) return; + + const x = (left + right) / 2; + const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_RADIUS; + + positions.push({ node, x, y, parentX, parentY }); + + if (node.children.length > 0) { + // Calculate total children width + const childrenWidth = node.children.reduce( + (sum, childId) => sum + (subtreeWidths.get(childId) ?? MIN_NODE_SPACING), + 0 + ); + + // Start position for first child + let childLeft = x - childrenWidth / 2; + + for (const childId of node.children) { + const childWidth = subtreeWidths.get(childId) ?? MIN_NODE_SPACING; + traverse( + childId, + level + 1, + childLeft, + childLeft + childWidth, + x, + y + ); + childLeft += childWidth; + } + } + } + + traverse(trie.rootId, 0, 0, totalWidth); + return positions; +} + +export function TrieView({ trie, className }: TrieViewProps) { + const { positions, svgWidth, svgHeight } = useMemo(() => { + const nodeMap = buildNodeMap(trie.nodes); + const depth = calculateTrieDepth(trie.rootId, nodeMap); + const rootWidth = calculateSubtreeWidth(trie.rootId, nodeMap); + + const width = Math.max(rootWidth + SVG_PADDING * 2, 200); + const height = depth * LEVEL_HEIGHT + SVG_PADDING * 2; + const pos = calculatePositions(trie, width); + + return { positions: pos, svgWidth: width, svgHeight: height }; + }, [trie]); + + // Build a set of highlighted path nodes for edge styling + const pathNodeIds = new Set(trie.currentPath ?? []); + + return ( +
+ {trie.label && ( + + {trie.label} + + )} + + {/* Draw edges first (behind nodes) */} + {positions.map(({ node, x, y, parentX, parentY }) => { + if (parentX === undefined || parentY === undefined) return null; + + // Determine if this edge is on the current path + const isOnPath = pathNodeIds.has(node.id); + + // Calculate midpoint for character label + const midX = (parentX + x) / 2; + const midY = (parentY + NODE_RADIUS + y - NODE_RADIUS) / 2; + + return ( + + + {/* Edge character label */} + + {node.char} + + + ); + })} + + {/* Draw nodes */} + {positions.map(({ node, x, y }) => ( + + ))} + + {/* Current operation indicator */} + {trie.searchWord && ( + + + {trie.searchIndex !== undefined + ? `"${trie.searchWord}" [${trie.searchIndex}/${trie.searchWord.length}]` + : `"${trie.searchWord}"`} + + + )} + +
+ ); +} diff --git a/frontend/src/components/visualizations-new/primitives/trie-node.tsx b/frontend/src/components/visualizations-new/primitives/trie-node.tsx new file mode 100644 index 0000000..2c231e4 --- /dev/null +++ b/frontend/src/components/visualizations-new/primitives/trie-node.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import type { TrieNodeState } from '@/lib/visualizations/types'; + +interface TrieNodeProps { + node: TrieNodeState; + x: number; + y: number; + radius?: number; + className?: string; +} + +const STATE_CLASSES = { + normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]', + current: 'fill-[var(--primary)]/20 stroke-[var(--primary)]', + found: 'fill-[var(--success)]/20 stroke-[var(--success)]', + notfound: 'fill-[var(--error)]/20 stroke-[var(--error)]', + highlighted: 'fill-[var(--primary)]/30 stroke-[var(--primary)]', + creating: 'fill-[var(--info)]/20 stroke-[var(--info)]', +} as const; + +const TEXT_CLASSES = { + normal: 'fill-[var(--foreground)]', + current: 'fill-[var(--primary)]', + found: 'fill-[var(--success)]', + notfound: 'fill-[var(--error)]', + highlighted: 'fill-[var(--primary)]', + creating: 'fill-[var(--info)]', +} as const; + +export function TrieNode({ + node, + x, + y, + radius = 20, + className, +}: TrieNodeProps) { + const isActive = node.state === 'current' || node.state === 'highlighted' || node.state === 'creating'; + const isRoot = node.char === ''; + + return ( + + {/* Main circle */} + + + {/* End-of-word indicator: inner circle */} + {node.isEndOfWord && ( + + )} + + {/* Character label */} + + {isRoot ? '\u2205' : node.char} + + + ); +} diff --git a/frontend/src/content/algorithms/implement-trie.ts b/frontend/src/content/algorithms/implement-trie.ts new file mode 100644 index 0000000..007faf9 --- /dev/null +++ b/frontend/src/content/algorithms/implement-trie.ts @@ -0,0 +1,701 @@ +import type { AlgorithmDefinition, TrieNodeState, TrieState } from '@/lib/visualizations/types'; + +/** + * Implement Trie (Prefix Tree) - LeetCode 208 + * + * Trie structure after inserting "app" and "apple": + * + * (root) + * | + * a + * | + * p + * | + * p* (* = end of word) + * | + * l + * | + * e* + */ + +// Node IDs +const ROOT = 'root'; +const NODE_A = 'a'; +const NODE_P1 = 'p1'; +const NODE_P2 = 'p2'; +const NODE_L = 'l'; +const NODE_E = 'e'; + +type NodeState = 'normal' | 'current' | 'found' | 'notfound' | 'highlighted' | 'creating'; + +// Helper to create a single trie node +function createNode( + id: string, + char: string, + state: NodeState, + isEndOfWord: boolean, + children: string[] +): TrieNodeState { + return { id, char, state, isEndOfWord, children }; +} + +// Helper to create trie state with given nodes +function createTrieState( + nodes: TrieNodeState[], + currentPath?: string[], + searchWord?: string, + searchIndex?: number +): TrieState { + return { + id: 'trie', + nodes, + rootId: ROOT, + label: 'Trie', + currentPath, + searchWord, + searchIndex, + }; +} + +// Empty trie (just root) +function emptyTrie(rootState: NodeState = 'normal'): TrieNodeState[] { + return [createNode(ROOT, '', rootState, false, [])]; +} + +// Trie with just 'a' node +function trieWithA( + states: Partial> = {}, + endOfWords: Partial> = {} +): TrieNodeState[] { + return [ + createNode(ROOT, '', states[ROOT] ?? 'normal', false, [NODE_A]), + createNode(NODE_A, 'a', states[NODE_A] ?? 'normal', endOfWords[NODE_A] ?? false, []), + ]; +} + +// Trie with 'a' -> 'p' +function trieWithAP( + states: Partial> = {}, + endOfWords: Partial> = {} +): TrieNodeState[] { + return [ + createNode(ROOT, '', states[ROOT] ?? 'normal', false, [NODE_A]), + createNode(NODE_A, 'a', states[NODE_A] ?? 'normal', endOfWords[NODE_A] ?? false, [NODE_P1]), + createNode(NODE_P1, 'p', states[NODE_P1] ?? 'normal', endOfWords[NODE_P1] ?? false, []), + ]; +} + +// Trie with 'a' -> 'p' -> 'p' (for "app") +function trieWithApp( + states: Partial> = {}, + endOfWords: Partial> = {} +): TrieNodeState[] { + return [ + createNode(ROOT, '', states[ROOT] ?? 'normal', false, [NODE_A]), + createNode(NODE_A, 'a', states[NODE_A] ?? 'normal', endOfWords[NODE_A] ?? false, [NODE_P1]), + createNode(NODE_P1, 'p', states[NODE_P1] ?? 'normal', endOfWords[NODE_P1] ?? false, [NODE_P2]), + createNode(NODE_P2, 'p', states[NODE_P2] ?? 'normal', endOfWords[NODE_P2] ?? true, []), + ]; +} + +// Trie with 'a' -> 'p' -> 'p' -> 'l' -> 'e' (for "app" and "apple") +function trieWithAppApple( + states: Partial> = {}, + endOfWords: Partial> = {} +): TrieNodeState[] { + return [ + createNode(ROOT, '', states[ROOT] ?? 'normal', false, [NODE_A]), + createNode(NODE_A, 'a', states[NODE_A] ?? 'normal', endOfWords[NODE_A] ?? false, [NODE_P1]), + createNode(NODE_P1, 'p', states[NODE_P1] ?? 'normal', endOfWords[NODE_P1] ?? false, [NODE_P2]), + createNode(NODE_P2, 'p', states[NODE_P2] ?? 'normal', endOfWords[NODE_P2] ?? true, [NODE_L]), + createNode(NODE_L, 'l', states[NODE_L] ?? 'normal', endOfWords[NODE_L] ?? false, [NODE_E]), + createNode(NODE_E, 'e', states[NODE_E] ?? 'normal', endOfWords[NODE_E] ?? true, []), + ]; +} + +export const implementTrieAlgorithm: AlgorithmDefinition = { + id: 'implement-trie', + title: 'Implement Trie (Prefix Tree)', + slug: 'implement-trie', + pattern: { + name: 'Trie', + description: + 'A tree-like data structure for efficient prefix-based operations on strings, where each node represents a character and paths from root represent words or prefixes.', + }, + problemStatement: + 'Implement a trie with insert, search, and startsWith methods. insert(word) inserts a word into the trie. search(word) returns true if the word exists. startsWith(prefix) returns true if any word starts with the given prefix.', + intuition: + 'Think of a trie like a filing cabinet where each drawer is labeled with a letter. To find a word, you open drawers one letter at a time. Words sharing the same prefix share the same path through the drawers, making prefix operations very efficient.', + code: { + language: 'python', + code: `class TrieNode: + def __init__(self): + self.children = {} # char -> TrieNode + self.is_end = False + +class Trie: + def __init__(self): + self.root = TrieNode() + + def insert(self, word: str) -> None: + node = self.root + for char in word: + if char not in node.children: + node.children[char] = TrieNode() + node = node.children[char] + node.is_end = True + + def search(self, word: str) -> bool: + node = self._traverse(word) + return node is not None and node.is_end + + def startsWith(self, prefix: str) -> bool: + return self._traverse(prefix) is not None + + def _traverse(self, s: str) -> TrieNode: + node = self.root + for char in s: + if char not in node.children: + return None + node = node.children[char] + return node`, + }, + initialExample: { + input: { operations: ['insert("app")', 'insert("apple")', 'search("app")', 'search("apple")', 'startsWith("ap")'] }, + expected: [null, null, true, true, true], + }, + steps: [ + // ========================================== + // Phase 1: Problem (2 steps) + // ========================================== + { + id: 'problem-1', + phase: 'problem', + explanation: + 'We need to implement a Trie (prefix tree) that supports three operations: insert(word), search(word), and startsWith(prefix). Each operation should run in O(m) time where m is the length of the input string.', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + { + id: 'problem-2', + phase: 'problem', + explanation: + 'We\'ll demonstrate with: insert("app"), insert("apple"), search("app"), search("apple"), and startsWith("ap"). Notice how "app" and "apple" share the prefix "app".', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'ops', name: 'operations', value: '["app", "apple", ...]' }, + ], + calculations: [], + tries: [createTrieState(emptyTrie('highlighted'))], + }, + }, + + // ========================================== + // Phase 2: Intuition (3 steps) + // ========================================== + { + id: 'intuition-1', + phase: 'intuition', + explanation: + 'Imagine a filing cabinet where each drawer is labeled with a single letter. To store the word "app", you open drawer "a", then find drawer "p" inside it, then drawer "p" inside that. The final drawer is marked as "end of word".', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + { + id: 'intuition-2', + phase: 'intuition', + explanation: + 'When adding "apple" after "app", we reuse the existing path a→p→p and only create new drawers for "l" and "e". This sharing of prefixes is what makes tries efficient for dictionary operations.', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'insight', name: 'Key Insight', value: 'Shared prefixes = shared nodes' }, + ], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + { + id: 'intuition-3', + phase: 'intuition', + explanation: + 'To search, we follow the path character by character. If we reach the end and the final node is marked "end of word", the word exists. For startsWith, we just need to confirm the path exists—no end-of-word check needed.', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + + // ========================================== + // Phase 3: Pattern (2 steps) + // ========================================== + { + id: 'pattern-1', + phase: 'pattern', + explanation: + 'Each TrieNode has two properties: a dictionary mapping characters to child nodes, and a boolean is_end flag. The root node is always empty and serves as the starting point.', + codeLine: 1, + codeHighlightLines: [1, 2, 3, 4], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'struct', name: 'TrieNode', value: '{children: {}, is_end: bool}' }, + ], + calculations: [], + tries: [createTrieState(emptyTrie('highlighted'))], + }, + }, + { + id: 'pattern-2', + phase: 'pattern', + explanation: + 'All three operations share the same traversal pattern: start at root, follow character edges one by one. Insert creates missing nodes; search/startsWith just follow existing edges.', + codeLine: 11, + codeHighlightLines: [11, 12, 13, 14, 15, 16], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'complexity', name: 'Time', value: 'O(m) per operation' }, + ], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + + // ========================================== + // Phase 4: Code (3 steps) + // ========================================== + { + id: 'code-1', + phase: 'code', + explanation: + 'Initialize the Trie with an empty root node. This root doesn\'t represent any character—it\'s just the starting point for all operations.', + codeLine: 8, + codeHighlightLines: [6, 7, 8], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'root', name: 'root', value: 'TrieNode()' }, + ], + calculations: [], + tries: [createTrieState(emptyTrie('current'))], + }, + }, + { + id: 'code-2', + phase: 'code', + explanation: + 'insert(word): Traverse from root, creating new nodes for characters that don\'t exist. After processing all characters, mark the final node as end-of-word.', + codeLine: 11, + codeHighlightLines: [10, 11, 12, 13, 14, 15, 16], + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + { + id: 'code-3', + phase: 'code', + explanation: + 'search and startsWith both use _traverse to follow the path. search additionally checks is_end; startsWith only checks if the path exists.', + codeLine: 18, + codeHighlightLines: [18, 19, 20, 22, 23], + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + tries: [createTrieState(emptyTrie())], + }, + }, + + // ========================================== + // Phase 5: Execution - insert("app") (4 steps) + // ========================================== + { + id: 'exec-insert-app-1', + phase: 'execution', + explanation: + 'insert("app"): Start at root. Character \'a\' not in children, so create new node for \'a\'.', + codeLine: 13, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"app"' }, + { id: 'char', name: 'char', value: '"a"' }, + { id: 'node', name: 'node', value: 'root' }, + ], + calculations: [], + tries: [createTrieState( + trieWithA({ [ROOT]: 'current', [NODE_A]: 'creating' }), + [ROOT], + 'app', + 0 + )], + }, + }, + { + id: 'exec-insert-app-2', + phase: 'execution', + explanation: + 'Move to node \'a\'. Character \'p\' not in children, create new node for first \'p\'.', + codeLine: 13, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"app"' }, + { id: 'char', name: 'char', value: '"p"' }, + { id: 'node', name: 'node', value: 'node_a' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAP({ [NODE_A]: 'current', [NODE_P1]: 'creating' }), + [ROOT, NODE_A], + 'app', + 1 + )], + }, + }, + { + id: 'exec-insert-app-3', + phase: 'execution', + explanation: + 'Move to first \'p\' node. Character \'p\' not in children, create new node for second \'p\'.', + codeLine: 13, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"app"' }, + { id: 'char', name: 'char', value: '"p"' }, + { id: 'node', name: 'node', value: 'node_p1' }, + ], + calculations: [], + tries: [createTrieState( + trieWithApp({ [NODE_P1]: 'current', [NODE_P2]: 'creating' }, { [NODE_P2]: false }), + [ROOT, NODE_A, NODE_P1], + 'app', + 2 + )], + }, + }, + { + id: 'exec-insert-app-4', + phase: 'execution', + explanation: + 'All characters processed. Mark second \'p\' as end-of-word. "app" is now in the trie!', + codeLine: 16, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"app"' }, + { id: 'result', name: 'is_end', value: 'true' }, + ], + calculations: [], + tries: [createTrieState( + trieWithApp({ [NODE_P2]: 'found' }, { [NODE_P2]: true }), + [ROOT, NODE_A, NODE_P1, NODE_P2], + 'app', + 3 + )], + }, + }, + + // ========================================== + // Phase 5: Execution - insert("apple") (3 steps) + // ========================================== + { + id: 'exec-insert-apple-1', + phase: 'execution', + explanation: + 'insert("apple"): Start at root. Traverse existing path a→p→p (nodes already exist from "app").', + codeLine: 15, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"apple"' }, + { id: 'node', name: 'node', value: 'node_p2' }, + ], + calculations: [], + tries: [createTrieState( + trieWithApp({ [ROOT]: 'highlighted', [NODE_A]: 'highlighted', [NODE_P1]: 'highlighted', [NODE_P2]: 'current' }), + [ROOT, NODE_A, NODE_P1, NODE_P2], + 'apple', + 2 + )], + }, + }, + { + id: 'exec-insert-apple-2', + phase: 'execution', + explanation: + 'At second \'p\'. Character \'l\' not in children, create node for \'l\'. Then \'e\' not in children, create node for \'e\'.', + codeLine: 13, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"apple"' }, + { id: 'char', name: 'char', value: '"l", "e"' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_P2]: 'highlighted', [NODE_L]: 'creating', [NODE_E]: 'creating' }, { [NODE_E]: false }), + [ROOT, NODE_A, NODE_P1, NODE_P2], + 'apple', + 4 + )], + }, + }, + { + id: 'exec-insert-apple-3', + phase: 'execution', + explanation: + 'All characters processed. Mark \'e\' as end-of-word. "apple" is now in the trie! Notice "app" and "apple" share nodes a→p→p.', + codeLine: 16, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"apple"' }, + { id: 'result', name: 'is_end', value: 'true' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_E]: 'found' }), + [ROOT, NODE_A, NODE_P1, NODE_P2, NODE_L, NODE_E], + 'apple', + 5 + )], + }, + }, + + // ========================================== + // Phase 5: Execution - search("app") (3 steps) + // ========================================== + { + id: 'exec-search-app-1', + phase: 'execution', + explanation: + 'search("app"): Traverse path a→p→p. All characters exist in the trie.', + codeLine: 26, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"app"' }, + { id: 'node', name: 'node', value: 'traversing...' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [ROOT]: 'highlighted', [NODE_A]: 'highlighted', [NODE_P1]: 'highlighted', [NODE_P2]: 'current' }), + [ROOT, NODE_A, NODE_P1, NODE_P2], + 'app', + 2 + )], + }, + }, + { + id: 'exec-search-app-2', + phase: 'execution', + explanation: + 'Reached end of word. Check is_end on second \'p\': it\'s True (we marked it when inserting "app").', + codeLine: 19, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"app"' }, + { id: 'is_end', name: 'is_end', value: 'true' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_P2]: 'current' }), + [ROOT, NODE_A, NODE_P1, NODE_P2], + 'app', + 3 + )], + }, + }, + { + id: 'exec-search-app-3', + phase: 'execution', + explanation: + 'search("app") returns True. The word exists in the trie.', + codeLine: 19, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'result', name: 'return', value: 'True' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_P2]: 'found' }), + [ROOT, NODE_A, NODE_P1, NODE_P2], + 'app' + )], + }, + }, + + // ========================================== + // Phase 5: Execution - search("apple") (3 steps) + // ========================================== + { + id: 'exec-search-apple-1', + phase: 'execution', + explanation: + 'search("apple"): Traverse path a→p→p→l→e. All characters exist.', + codeLine: 26, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"apple"' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [ROOT]: 'highlighted', [NODE_A]: 'highlighted', [NODE_P1]: 'highlighted', [NODE_P2]: 'highlighted', [NODE_L]: 'highlighted', [NODE_E]: 'current' }), + [ROOT, NODE_A, NODE_P1, NODE_P2, NODE_L, NODE_E], + 'apple', + 4 + )], + }, + }, + { + id: 'exec-search-apple-2', + phase: 'execution', + explanation: + 'Check is_end on \'e\': it\'s True (we marked it when inserting "apple").', + codeLine: 19, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'word', name: 'word', value: '"apple"' }, + { id: 'is_end', name: 'is_end', value: 'true' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_E]: 'current' }), + [ROOT, NODE_A, NODE_P1, NODE_P2, NODE_L, NODE_E], + 'apple', + 5 + )], + }, + }, + { + id: 'exec-search-apple-3', + phase: 'execution', + explanation: + 'search("apple") returns True. The word exists in the trie.', + codeLine: 19, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'result', name: 'return', value: 'True' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_E]: 'found' }), + [ROOT, NODE_A, NODE_P1, NODE_P2, NODE_L, NODE_E], + 'apple' + )], + }, + }, + + // ========================================== + // Phase 5: Execution - startsWith("ap") (2 steps) + // ========================================== + { + id: 'exec-starts-ap-1', + phase: 'execution', + explanation: + 'startsWith("ap"): Traverse path a→p. Both nodes exist.', + codeLine: 26, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prefix', name: 'prefix', value: '"ap"' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [ROOT]: 'highlighted', [NODE_A]: 'highlighted', [NODE_P1]: 'current' }), + [ROOT, NODE_A, NODE_P1], + 'ap', + 1 + )], + }, + }, + { + id: 'exec-starts-ap-2', + phase: 'execution', + explanation: + 'startsWith("ap") returns True. We found the prefix—no need to check is_end. Both "app" and "apple" start with "ap".', + codeLine: 23, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prefix', name: 'prefix', value: '"ap"' }, + { id: 'result', name: 'return', value: 'True' }, + ], + calculations: [], + tries: [createTrieState( + trieWithAppApple({ [NODE_P1]: 'found' }), + [ROOT, NODE_A, NODE_P1], + 'ap' + )], + }, + }, + + // ========================================== + // Final Summary + // ========================================== + { + id: 'exec-done', + phase: 'execution', + explanation: + 'Complete! The trie stores "app" and "apple" efficiently by sharing the prefix a→p→p. All operations run in O(m) time where m is the string length.', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'words', name: 'stored', value: '["app", "apple"]' }, + { id: 'time', name: 'complexity', value: 'O(m) per operation' }, + { id: 'space', name: 'space', value: 'O(total chars)' }, + ], + calculations: [], + tries: [createTrieState(trieWithAppApple())], + }, + }, + ], +};