feat(viz): trie visualisation

This commit is contained in:
2025-09-12 15:08:18 +01:00
parent f23455c481
commit b61c6d1613
4 changed files with 1114 additions and 0 deletions

View File

@@ -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 (
<VisualizationContainer
title={algorithm.title}
pattern={algorithm.pattern}
code={algorithm.code}
currentLine={currentStep.codeLine}
highlightLines={currentStep.codeHighlightLines}
explanation={currentStep.explanation}
decision={currentStep.decision}
phase={currentPhase}
variables={dataState.variables}
currentStepIndex={currentStepIndex}
totalSteps={totalSteps}
isPlaying={playback.isPlaying}
speed={playback.speed}
controls={controls}
progress={progress}
className={className}
>
<div className="flex items-start justify-center">
{trie && <TrieView trie={trie} />}
</div>
</VisualizationContainer>
);
}

View File

@@ -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<string, TrieNodeState> {
const map = new Map<string, TrieNodeState>();
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<string, TrieNodeState>
): 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<string, TrieNodeState>
): 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<string, number>();
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 (
<div className={cn('flex flex-col items-center', className)}>
{trie.label && (
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
{trie.label}
</span>
)}
<svg
width={svgWidth}
height={svgHeight}
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
className="overflow-visible"
>
{/* 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 (
<g key={`edge-${node.id}`}>
<motion.line
x1={parentX}
y1={parentY + NODE_RADIUS}
x2={x}
y2={y - NODE_RADIUS}
className={cn(
'stroke-[var(--border)]',
isOnPath && 'stroke-[var(--primary)]'
)}
strokeWidth={isOnPath ? 2.5 : 2}
initial={false}
animate={{
opacity: node.state === 'notfound' ? 0.3 : 1,
}}
transition={{ duration: 0.2 }}
/>
{/* Edge character label */}
<motion.text
x={midX}
y={midY}
textAnchor="middle"
dominantBaseline="central"
className={cn(
'pointer-events-none select-none font-mono text-xs font-medium',
isOnPath ? 'fill-[var(--primary)]' : 'fill-[var(--muted-foreground)]'
)}
initial={false}
animate={{
opacity: node.state === 'notfound' ? 0.3 : 1,
}}
transition={{ duration: 0.2 }}
>
{node.char}
</motion.text>
</g>
);
})}
{/* Draw nodes */}
{positions.map(({ node, x, y }) => (
<TrieNode
key={node.id}
node={node}
x={x}
y={y}
radius={NODE_RADIUS}
/>
))}
{/* Current operation indicator */}
{trie.searchWord && (
<g>
<text
x={svgWidth / 2}
y={svgHeight - 10}
textAnchor="middle"
className="fill-[var(--muted-foreground)] text-xs"
>
{trie.searchIndex !== undefined
? `"${trie.searchWord}" [${trie.searchIndex}/${trie.searchWord.length}]`
: `"${trie.searchWord}"`}
</text>
</g>
)}
</svg>
</div>
);
}

View File

@@ -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 (
<motion.g
initial={false}
animate={{
scale: isActive ? 1.1 : 1,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
style={{ transformOrigin: `${x}px ${y}px` }}
className={cn(className)}
>
{/* Main circle */}
<motion.circle
cx={x}
cy={y}
r={radius}
strokeWidth={2}
className={cn(
'transition-colors duration-200',
STATE_CLASSES[node.state]
)}
initial={false}
animate={{
filter: isActive ? 'drop-shadow(0 0 8px var(--primary))' : 'none',
}}
transition={{ duration: 0.2 }}
/>
{/* End-of-word indicator: inner circle */}
{node.isEndOfWord && (
<motion.circle
cx={x}
cy={y}
r={radius - 5}
strokeWidth={2}
fill="none"
className={cn(
'transition-colors duration-200',
STATE_CLASSES[node.state]
)}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
/>
)}
{/* Character label */}
<text
x={x}
y={y}
textAnchor="middle"
dominantBaseline="central"
className={cn(
'pointer-events-none select-none font-mono text-sm font-medium',
TEXT_CLASSES[node.state]
)}
>
{isRoot ? '\u2205' : node.char}
</text>
</motion.g>
);
}

View File

@@ -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<Record<string, NodeState>> = {},
endOfWords: Partial<Record<string, boolean>> = {}
): 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<Record<string, NodeState>> = {},
endOfWords: Partial<Record<string, boolean>> = {}
): 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<Record<string, NodeState>> = {},
endOfWords: Partial<Record<string, boolean>> = {}
): 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<Record<string, NodeState>> = {},
endOfWords: Partial<Record<string, boolean>> = {}
): 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())],
},
},
],
};