From e7ba79e49c3d25402e3aca3c77fbbb73c8e1f3ec Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sun, 24 Aug 2025 16:33:51 +0100 Subject: [PATCH] linked list visualisations --- frontend/src/app/patterns/[slug]/page.tsx | 8 +- .../algorithms/linked-list.tsx | 66 +++ .../core/visualization-container.tsx | 2 +- .../data-structures/array-view.tsx | 26 + .../data-structures/linked-list-view.tsx | 80 +++ .../components/visualizations-new/index.ts | 4 + .../primitives/linked-list-node.tsx | 64 +++ .../primitives/linked-list-pointer.tsx | 59 ++ .../visualizations-new/primitives/pointer.tsx | 14 +- .../content/algorithms/fast-slow-pointers.ts | 389 +++++++++++++ .../content/algorithms/linkedlist-reversal.ts | 515 ++++++++++++++++++ frontend/src/lib/visualizations/types.ts | 51 ++ 12 files changed, 1272 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/visualizations-new/algorithms/linked-list.tsx create mode 100644 frontend/src/components/visualizations-new/data-structures/linked-list-view.tsx create mode 100644 frontend/src/components/visualizations-new/primitives/linked-list-node.tsx create mode 100644 frontend/src/components/visualizations-new/primitives/linked-list-pointer.tsx create mode 100644 frontend/src/content/algorithms/fast-slow-pointers.ts create mode 100644 frontend/src/content/algorithms/linkedlist-reversal.ts diff --git a/frontend/src/app/patterns/[slug]/page.tsx b/frontend/src/app/patterns/[slug]/page.tsx index ddd2bad..c1b30ab 100644 --- a/frontend/src/app/patterns/[slug]/page.tsx +++ b/frontend/src/app/patterns/[slug]/page.tsx @@ -14,11 +14,13 @@ import { RelatedPatterns, } from "@/components/patterns"; import { PatternVisualization } from "@/components/visualization"; -import { TwoPointersVisualization, PrefixSumVisualization } from "@/components/visualizations-new"; +import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization } from "@/components/visualizations-new"; import { twoSumAlgorithm } from "@/content/algorithms/two-sum"; import { slidingWindowAlgorithm } from "@/content/algorithms/sliding-window"; import { binarySearchAlgorithm } from "@/content/algorithms/binary-search"; import { prefixSumAlgorithm } from "@/content/algorithms/prefix-sum"; +import { fastSlowPointersAlgorithm } from "@/content/algorithms/fast-slow-pointers"; +import { linkedListReversalAlgorithm } from "@/content/algorithms/linkedlist-reversal"; interface PageProps { params: Promise<{ slug: string }>; @@ -119,6 +121,10 @@ export default async function PatternDetailPage({ params }: PageProps) { ) : slug === "prefix-sum" ? ( + ) : slug === "fast-slow-pointers" ? ( + + ) : slug === "linkedlist-reversal" ? ( + ) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? ( diff --git a/frontend/src/components/visualizations-new/algorithms/linked-list.tsx b/frontend/src/components/visualizations-new/algorithms/linked-list.tsx new file mode 100644 index 0000000..9379f88 --- /dev/null +++ b/frontend/src/components/visualizations-new/algorithms/linked-list.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useVisualization } from '@/lib/visualizations/use-visualization'; +import type { AlgorithmDefinition, LinkedListState, LinkedListPointerState } from '@/lib/visualizations/types'; +import { VisualizationContainer } from '../core/visualization-container'; +import { LinkedListView } from '../data-structures/linked-list-view'; + +interface LinkedListVisualizationProps { + algorithm: AlgorithmDefinition; + className?: string; +} + +export function LinkedListVisualization({ + algorithm, + className, +}: LinkedListVisualizationProps) { + const { + currentStep, + currentStepIndex, + totalSteps, + playback, + controls, + currentPhase, + progress, + } = useVisualization(algorithm); + + const { dataState } = currentStep; + + // Extract linked list and pointers from dataState + // We store linkedLists in a custom field, falling back to empty + const linkedList: LinkedListState = (dataState as { linkedLists?: LinkedListState[] }).linkedLists?.[0] ?? { + id: 'empty', + nodes: [], + }; + + const linkedListPointers: LinkedListPointerState[] = + (dataState as { linkedListPointers?: LinkedListPointerState[] }).linkedListPointers ?? []; + + return ( + +
+ +
+
+ ); +} diff --git a/frontend/src/components/visualizations-new/core/visualization-container.tsx b/frontend/src/components/visualizations-new/core/visualization-container.tsx index 6c6a4b5..0398001 100644 --- a/frontend/src/components/visualizations-new/core/visualization-container.tsx +++ b/frontend/src/components/visualizations-new/core/visualization-container.tsx @@ -81,7 +81,7 @@ export function VisualizationContainer({
diff --git a/frontend/src/components/visualizations-new/data-structures/array-view.tsx b/frontend/src/components/visualizations-new/data-structures/array-view.tsx index 6432692..41f9fd3 100644 --- a/frontend/src/components/visualizations-new/data-structures/array-view.tsx +++ b/frontend/src/components/visualizations-new/data-structures/array-view.tsx @@ -39,6 +39,30 @@ export function ArrayView({ // Calculate total array width for proper pointer alignment const totalWidth = array.elements.length * elementWidth + (array.elements.length - 1) * gap; + // Check if any pointers are too close together (within 1 index) - hide values to save space + const hasClosePointers = pointers.length > 1 && pointers.some((p, i) => { + return pointers.some((other, j) => i !== j && Math.abs(p.index - other.index) <= 1); + }); + + // Group pointers by index to calculate offsets for overlapping pointers at same position + const pointersByIndex = pointers.reduce((acc, pointer) => { + const idx = pointer.index; + if (!acc[idx]) acc[idx] = []; + acc[idx].push(pointer); + return acc; + }, {} as Record); + + // Calculate horizontal offset for pointers at the same index + const getPointerOffset = (pointer: PointerState): number => { + const pointersAtIndex = pointersByIndex[pointer.index] || []; + if (pointersAtIndex.length <= 1) return 0; + + const pointerIdx = pointersAtIndex.findIndex(p => p.id === pointer.id); + const totalPointers = pointersAtIndex.length; + const spacing = 36; // px between overlapping pointers + return (pointerIdx - (totalPointers - 1) / 2) * spacing; + }; + return (
{array.label && ( @@ -58,6 +82,8 @@ export function ArrayView({ elementWidth={elementWidth} gap={gap} value={array.elements[pointer.index]?.value} + hideValue={hasClosePointers} + horizontalOffset={getPointerOffset(pointer)} /> ))}
diff --git a/frontend/src/components/visualizations-new/data-structures/linked-list-view.tsx b/frontend/src/components/visualizations-new/data-structures/linked-list-view.tsx new file mode 100644 index 0000000..c198d63 --- /dev/null +++ b/frontend/src/components/visualizations-new/data-structures/linked-list-view.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { cn } from '@/lib/utils'; +import type { LinkedListState, LinkedListPointerState } from '@/lib/visualizations/types'; +import { LinkedListNode } from '../primitives/linked-list-node'; +import { LinkedListPointer } from '../primitives/linked-list-pointer'; + +interface LinkedListViewProps { + list: LinkedListState; + pointers: LinkedListPointerState[]; + className?: string; +} + +const NODE_WIDTH = 40; // w-10 = 40px +const ARROW_WIDTH = 22; // 16px line + 6px arrow head + +export function LinkedListView({ + list, + pointers, + className, +}: LinkedListViewProps) { + // Calculate total width for proper pointer alignment + const totalWidth = list.nodes.length * NODE_WIDTH + (list.nodes.length - 1) * ARROW_WIDTH; + + // Group pointers by nodeIndex to calculate offsets for overlapping pointers + const pointersByIndex = pointers.reduce((acc, pointer) => { + const idx = pointer.nodeIndex; + if (!acc[idx]) acc[idx] = []; + acc[idx].push(pointer); + return acc; + }, {} as Record); + + // Calculate horizontal offset for each pointer + const getPointerOffset = (pointer: LinkedListPointerState): number => { + const pointersAtIndex = pointersByIndex[pointer.nodeIndex] || []; + if (pointersAtIndex.length <= 1) return 0; + + const pointerIdx = pointersAtIndex.findIndex(p => p.id === pointer.id); + const totalPointers = pointersAtIndex.length; + const spacing = 28; // px between overlapping pointers + // Center the group: offset from center based on position + return (pointerIdx - (totalPointers - 1) / 2) * spacing; + }; + + return ( +
+ {list.label && ( + + {list.label} + + )} + +
+ {/* Pointers row */} +
+ {pointers.map((pointer) => ( + + ))} +
+ + {/* Linked list nodes */} +
+ {list.nodes.map((node, index) => ( + + ))} +
+
+
+ ); +} diff --git a/frontend/src/components/visualizations-new/index.ts b/frontend/src/components/visualizations-new/index.ts index 25831ab..192c549 100644 --- a/frontend/src/components/visualizations-new/index.ts +++ b/frontend/src/components/visualizations-new/index.ts @@ -9,10 +9,14 @@ export { VariableInspector } from "./core/variable-inspector"; export { ArrayElement } from "./primitives/array-element"; export { Pointer } from "./primitives/pointer"; export { CalculationBubble } from "./primitives/calculation-bubble"; +export { LinkedListNode } from "./primitives/linked-list-node"; +export { LinkedListPointer } from "./primitives/linked-list-pointer"; // Data structures export { ArrayView } from "./data-structures/array-view"; +export { LinkedListView } from "./data-structures/linked-list-view"; // Algorithm visualizations export { PrefixSumVisualization } from "./algorithms/prefix-sum"; export { TwoPointersVisualization } from "./algorithms/two-pointers"; +export { LinkedListVisualization } from "./algorithms/linked-list"; diff --git a/frontend/src/components/visualizations-new/primitives/linked-list-node.tsx b/frontend/src/components/visualizations-new/primitives/linked-list-node.tsx new file mode 100644 index 0000000..4a46a74 --- /dev/null +++ b/frontend/src/components/visualizations-new/primitives/linked-list-node.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import type { LinkedListNodeState } from '@/lib/visualizations/types'; + +interface LinkedListNodeProps { + node: LinkedListNodeState; + isLast?: boolean; + className?: string; +} + +const STATE_CLASSES = { + normal: 'bg-surface-variant border-border text-foreground', + highlighted: 'bg-primary/20 border-primary text-primary', + dimmed: 'bg-surface-variant/50 border-border/50 text-foreground-muted opacity-40', + success: 'bg-viz-success/20 border-viz-success text-viz-success', + comparing: 'bg-viz-compare/20 border-viz-compare text-viz-compare', +} as const; + +export function LinkedListNode({ + node, + isLast = false, + className, +}: LinkedListNodeProps) { + if (node.isNull) { + return ( +
+
+ null +
+
+ ); + } + + return ( +
+ + {node.value} + + {!isLast && ( + +
+
+ + )} +
+ ); +} diff --git a/frontend/src/components/visualizations-new/primitives/linked-list-pointer.tsx b/frontend/src/components/visualizations-new/primitives/linked-list-pointer.tsx new file mode 100644 index 0000000..791d5d9 --- /dev/null +++ b/frontend/src/components/visualizations-new/primitives/linked-list-pointer.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import type { LinkedListPointerState } from '@/lib/visualizations/types'; + +interface LinkedListPointerProps { + pointer: LinkedListPointerState; + nodeWidth: number; + arrowWidth: number; + /** Horizontal offset when multiple pointers at same position */ + horizontalOffset?: number; +} + +const COLOR_CLASSES = { + slow: 'text-viz-pointer-left', + fast: 'text-viz-pointer-right', + prev: 'text-viz-pointer-mid', + curr: 'text-viz-pointer-left', + next: 'text-viz-pointer-right', + default: 'text-viz-pointer-default', +} as const; + +export function LinkedListPointer({ + pointer, + nodeWidth, + arrowWidth, + horizontalOffset = 0, +}: LinkedListPointerProps) { + // Calculate x position based on node index, with offset for overlapping pointers + const x = pointer.nodeIndex * (nodeWidth + arrowWidth) + nodeWidth / 2 + horizontalOffset; + + return ( + + {pointer.name} + + + + + ); +} diff --git a/frontend/src/components/visualizations-new/primitives/pointer.tsx b/frontend/src/components/visualizations-new/primitives/pointer.tsx index 3a8d5bb..6daa200 100644 --- a/frontend/src/components/visualizations-new/primitives/pointer.tsx +++ b/frontend/src/components/visualizations-new/primitives/pointer.tsx @@ -9,6 +9,10 @@ interface PointerProps { elementWidth: number; gap: number; value?: number; + /** Hide the value to save space when pointers are close */ + hideValue?: boolean; + /** Horizontal offset when multiple pointers at same position */ + horizontalOffset?: number; className?: string; } @@ -24,10 +28,12 @@ export function Pointer({ elementWidth, gap, value, + hideValue = false, + horizontalOffset = 0, className, }: PointerProps) { - // Calculate center of element: index * (width + gap) + width / 2 - const offset = pointer.index * (elementWidth + gap) + elementWidth / 2; + // Calculate center of element: index * (width + gap) + width / 2, plus any offset for overlapping + const offset = pointer.index * (elementWidth + gap) + elementWidth / 2 + horizontalOffset; return ( - {/* Label - centered above the point */} + {/* Label - centered above the arrow */} {pointer.name} - {pointer.showValue && value !== undefined && ( + {!hideValue && pointer.showValue && value !== undefined && ( = {value} )} diff --git a/frontend/src/content/algorithms/fast-slow-pointers.ts b/frontend/src/content/algorithms/fast-slow-pointers.ts new file mode 100644 index 0000000..d0f5619 --- /dev/null +++ b/frontend/src/content/algorithms/fast-slow-pointers.ts @@ -0,0 +1,389 @@ +import type { AlgorithmDefinition } from '@/lib/visualizations/types'; + +export const fastSlowPointersAlgorithm: AlgorithmDefinition = { + id: 'fast-slow-pointers', + title: 'Find Middle of Linked List', + slug: 'fast-slow-pointers', + pattern: { + name: 'Fast & Slow Pointers', + description: 'Use two pointers moving at different speeds to find cycle or middle', + }, + problemStatement: + 'Given a linked list, find the middle node. If there are two middle nodes, return the second one.', + intuition: + 'If fast pointer moves twice as fast as slow, when fast reaches the end, slow will be at the middle.', + code: { + language: 'python', + code: `def find_middle(head): + slow = fast = head + + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + return slow`, + }, + steps: [ + // Problem phase + { + id: 'step-1', + phase: 'problem', + explanation: 'We have a linked list: 1 → 2 → 3 → 4 → 5 → null. Find the middle node.', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'normal' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [], + }, + }, + // Intuition phase + { + id: 'step-2', + phase: 'intuition', + explanation: + 'The key insight: if one pointer moves 2x faster than another, when the fast one finishes, the slow one is halfway.', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'normal' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'highlighted' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [], + }, + }, + // Code phase + { + id: 'step-3', + phase: 'code', + explanation: 'Initialize both pointers at the head of the list.', + codeLine: 2, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'head' }, + { id: 'fast', name: 'fast', value: 'head' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'highlighted' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 0, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 0, color: 'fast' }, + ], + }, + }, + // Execution phase - iteration 1 + { + id: 'step-4', + phase: 'execution', + explanation: 'Check: fast (node 1) exists and fast.next (node 2) exists. Enter loop.', + codeLine: 4, + decision: { + question: 'Is fast and fast.next valid?', + answer: 'Yes, both exist', + action: 'Enter the while loop', + }, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 1' }, + { id: 'fast', name: 'fast', value: 'node 1' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'highlighted' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 0, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 0, color: 'fast' }, + ], + }, + }, + { + id: 'step-5', + phase: 'execution', + explanation: 'Move slow one step forward (1 → 2).', + codeLine: 5, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 2', previousValue: 'node 1' }, + { id: 'fast', name: 'fast', value: 'node 1' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'highlighted' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 1, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 0, color: 'fast' }, + ], + }, + }, + { + id: 'step-6', + phase: 'execution', + explanation: 'Move fast two steps forward (1 → 2 → 3).', + codeLine: 6, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 2' }, + { id: 'fast', name: 'fast', value: 'node 3', previousValue: 'node 1' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'highlighted' }, + { id: 'n3', value: 3, index: 2, state: 'comparing' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 1, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 2, color: 'fast' }, + ], + }, + }, + // Iteration 2 + { + id: 'step-7', + phase: 'execution', + explanation: 'Check: fast (node 3) exists and fast.next (node 4) exists. Continue loop.', + codeLine: 4, + decision: { + question: 'Is fast and fast.next valid?', + answer: 'Yes, both exist', + action: 'Continue the while loop', + }, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 2' }, + { id: 'fast', name: 'fast', value: 'node 3' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'highlighted' }, + { id: 'n3', value: 3, index: 2, state: 'comparing' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 1, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 2, color: 'fast' }, + ], + }, + }, + { + id: 'step-8', + phase: 'execution', + explanation: 'Move slow one step forward (2 → 3).', + codeLine: 5, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 3', previousValue: 'node 2' }, + { id: 'fast', name: 'fast', value: 'node 3' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'dimmed' }, + { id: 'n3', value: 3, index: 2, state: 'highlighted' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'n5', value: 5, index: 4, state: 'normal' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 2, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 2, color: 'fast' }, + ], + }, + }, + { + id: 'step-9', + phase: 'execution', + explanation: 'Move fast two steps forward (3 → 4 → 5).', + codeLine: 6, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 3' }, + { id: 'fast', name: 'fast', value: 'node 5', previousValue: 'node 3' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'dimmed' }, + { id: 'n3', value: 3, index: 2, state: 'highlighted' }, + { id: 'n4', value: 4, index: 3, state: 'dimmed' }, + { id: 'n5', value: 5, index: 4, state: 'comparing' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 2, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 4, color: 'fast' }, + ], + }, + }, + // Check and exit + { + id: 'step-10', + phase: 'execution', + explanation: 'Check: fast (node 5) exists but fast.next is null. Exit loop.', + codeLine: 4, + decision: { + question: 'Is fast and fast.next valid?', + answer: 'No, fast.next is null', + action: 'Exit the while loop', + }, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 3' }, + { id: 'fast', name: 'fast', value: 'node 5' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'dimmed' }, + { id: 'n3', value: 3, index: 2, state: 'highlighted' }, + { id: 'n4', value: 4, index: 3, state: 'dimmed' }, + { id: 'n5', value: 5, index: 4, state: 'comparing' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 2, color: 'slow' }, + { id: 'fast', name: 'fast', nodeIndex: 4, color: 'fast' }, + ], + }, + }, + // Return result + { + id: 'step-11', + phase: 'execution', + explanation: 'Return slow, which points to node 3 — the middle of the linked list!', + codeLine: 8, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'slow', name: 'slow', value: 'node 3' }, + { id: 'fast', name: 'fast', value: 'node 5' }, + { id: 'result', name: 'result', value: 3 }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'dimmed' }, + { id: 'n2', value: 2, index: 1, state: 'dimmed' }, + { id: 'n3', value: 3, index: 2, state: 'success' }, + { id: 'n4', value: 4, index: 3, state: 'dimmed' }, + { id: 'n5', value: 5, index: 4, state: 'dimmed' }, + { id: 'null', value: 0, index: 5, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'slow', name: 'slow', nodeIndex: 2, color: 'slow' }, + ], + }, + }, + ], +}; diff --git a/frontend/src/content/algorithms/linkedlist-reversal.ts b/frontend/src/content/algorithms/linkedlist-reversal.ts new file mode 100644 index 0000000..abfb253 --- /dev/null +++ b/frontend/src/content/algorithms/linkedlist-reversal.ts @@ -0,0 +1,515 @@ +import type { AlgorithmDefinition } from '@/lib/visualizations/types'; + +export const linkedListReversalAlgorithm: AlgorithmDefinition = { + id: 'linkedlist-reversal', + title: 'Reverse a Linked List', + slug: 'linkedlist-reversal', + pattern: { + name: 'LinkedList Reversal', + description: 'Reverse links between nodes using three pointers', + }, + problemStatement: + 'Given a singly linked list, reverse it in-place and return the new head.', + intuition: + 'Use three pointers (prev, curr, next) to reverse each link. Save next before breaking the link, then move forward.', + code: { + language: 'python', + code: `def reverse_list(head): + prev = None + curr = head + + while curr: + next_node = curr.next + curr.next = prev + prev = curr + curr = next_node + + return prev`, + }, + steps: [ + // Problem phase + { + id: 'step-1', + phase: 'problem', + explanation: 'We have a linked list: 1 → 2 → 3 → 4 → null. Reverse it to get 4 → 3 → 2 → 1 → null.', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'normal' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [], + }, + }, + // Intuition phase + { + id: 'step-2', + phase: 'intuition', + explanation: + 'Key insight: We need to reverse each arrow. To do this safely, save the next node before changing the link.', + dataState: { + arrays: [], + pointers: [], + variables: [], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'highlighted' }, + { id: 'n2', value: 2, index: 1, state: 'comparing' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [], + }, + }, + // Code phase - initialization + { + id: 'step-3', + phase: 'code', + explanation: 'Initialize prev = None (nothing before head yet) and curr = head.', + codeLine: 2, + codeHighlightLines: [2, 3], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'None' }, + { id: 'curr', name: 'curr', value: 'node 1' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'highlighted' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'curr', name: 'curr', nodeIndex: 0, color: 'curr' }, + ], + }, + }, + // Execution - iteration 1 + { + id: 'step-4', + phase: 'execution', + explanation: 'Check: curr (node 1) exists. Enter the while loop.', + codeLine: 5, + decision: { + question: 'Is curr not None?', + answer: 'Yes, curr is node 1', + action: 'Enter the while loop', + }, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'None' }, + { id: 'curr', name: 'curr', value: 'node 1' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'highlighted' }, + { id: 'n2', value: 2, index: 1, state: 'normal' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'curr', name: 'curr', nodeIndex: 0, color: 'curr' }, + ], + }, + }, + { + id: 'step-5', + phase: 'execution', + explanation: 'Save next_node = curr.next (node 2). This preserves our forward reference.', + codeLine: 6, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'None' }, + { id: 'curr', name: 'curr', value: 'node 1' }, + { id: 'next', name: 'next', value: 'node 2' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'highlighted' }, + { id: 'n2', value: 2, index: 1, state: 'comparing' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'curr', name: 'curr', nodeIndex: 0, color: 'curr' }, + { id: 'next', name: 'next', nodeIndex: 1, color: 'next' }, + ], + }, + }, + { + id: 'step-6', + phase: 'execution', + explanation: 'Reverse the link: curr.next = prev. Node 1 now points to None (backwards!).', + codeLine: 7, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'None' }, + { id: 'curr', name: 'curr', value: 'node 1' }, + { id: 'next', name: 'next', value: 'node 2' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'comparing' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'curr', name: 'curr', nodeIndex: 0, color: 'curr' }, + { id: 'next', name: 'next', nodeIndex: 1, color: 'next' }, + ], + }, + }, + { + id: 'step-7', + phase: 'execution', + explanation: 'Move prev forward: prev = curr. Now prev points to node 1.', + codeLine: 8, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 1', previousValue: 'None' }, + { id: 'curr', name: 'curr', value: 'node 1' }, + { id: 'next', name: 'next', value: 'node 2' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'comparing' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 0, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 0, color: 'curr' }, + { id: 'next', name: 'next', nodeIndex: 1, color: 'next' }, + ], + }, + }, + { + id: 'step-8', + phase: 'execution', + explanation: 'Move curr forward: curr = next_node. Now curr points to node 2.', + codeLine: 9, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 1' }, + { id: 'curr', name: 'curr', value: 'node 2', previousValue: 'node 1' }, + { id: 'next', name: 'next', value: 'node 2' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'highlighted' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 0, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 1, color: 'curr' }, + ], + }, + }, + // Iteration 2 + { + id: 'step-9', + phase: 'execution', + explanation: 'Check: curr (node 2) exists. Continue loop.', + codeLine: 5, + decision: { + question: 'Is curr not None?', + answer: 'Yes, curr is node 2', + action: 'Continue the while loop', + }, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 1' }, + { id: 'curr', name: 'curr', value: 'node 2' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'highlighted' }, + { id: 'n3', value: 3, index: 2, state: 'normal' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 0, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 1, color: 'curr' }, + ], + }, + }, + { + id: 'step-10', + phase: 'execution', + explanation: 'Save next_node = curr.next (node 3).', + codeLine: 6, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 1' }, + { id: 'curr', name: 'curr', value: 'node 2' }, + { id: 'next', name: 'next', value: 'node 3' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'highlighted' }, + { id: 'n3', value: 3, index: 2, state: 'comparing' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 0, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 1, color: 'curr' }, + { id: 'next', name: 'next', nodeIndex: 2, color: 'next' }, + ], + }, + }, + { + id: 'step-11', + phase: 'execution', + explanation: 'Reverse the link: curr.next = prev. Node 2 now points to node 1.', + codeLine: 7, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 1' }, + { id: 'curr', name: 'curr', value: 'node 2' }, + { id: 'next', name: 'next', value: 'node 3' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'success' }, + { id: 'n3', value: 3, index: 2, state: 'comparing' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 0, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 1, color: 'curr' }, + { id: 'next', name: 'next', nodeIndex: 2, color: 'next' }, + ], + }, + }, + { + id: 'step-12', + phase: 'execution', + explanation: 'Move prev = curr, curr = next_node. Advance to process node 3.', + codeLine: 8, + codeHighlightLines: [8, 9], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 2', previousValue: 'node 1' }, + { id: 'curr', name: 'curr', value: 'node 3', previousValue: 'node 2' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'success' }, + { id: 'n3', value: 3, index: 2, state: 'highlighted' }, + { id: 'n4', value: 4, index: 3, state: 'normal' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 1, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 2, color: 'curr' }, + ], + }, + }, + // Iteration 3 (condensed) + { + id: 'step-13', + phase: 'execution', + explanation: 'Process node 3: save next (node 4), reverse link (3→2), advance pointers.', + codeLine: 6, + codeHighlightLines: [6, 7, 8, 9], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 3', previousValue: 'node 2' }, + { id: 'curr', name: 'curr', value: 'node 4', previousValue: 'node 3' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'success' }, + { id: 'n3', value: 3, index: 2, state: 'success' }, + { id: 'n4', value: 4, index: 3, state: 'highlighted' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 2, color: 'prev' }, + { id: 'curr', name: 'curr', nodeIndex: 3, color: 'curr' }, + ], + }, + }, + // Iteration 4 (last node) + { + id: 'step-14', + phase: 'execution', + explanation: 'Process node 4: save next (null), reverse link (4→3), advance pointers.', + codeLine: 6, + codeHighlightLines: [6, 7, 8, 9], + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 4', previousValue: 'node 3' }, + { id: 'curr', name: 'curr', value: 'None', previousValue: 'node 4' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + nodes: [ + { id: 'n1', value: 1, index: 0, state: 'success' }, + { id: 'n2', value: 2, index: 1, state: 'success' }, + { id: 'n3', value: 3, index: 2, state: 'success' }, + { id: 'n4', value: 4, index: 3, state: 'success' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [ + { id: 'prev', name: 'prev', nodeIndex: 3, color: 'prev' }, + ], + }, + }, + // Exit and return + { + id: 'step-15', + phase: 'execution', + explanation: 'Check: curr is None. Exit loop. Return prev (node 4), the new head!', + codeLine: 11, + decision: { + question: 'Is curr not None?', + answer: 'No, curr is None', + action: 'Exit loop, return prev', + }, + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'prev', name: 'prev', value: 'node 4' }, + { id: 'curr', name: 'curr', value: 'None' }, + { id: 'result', name: 'result', value: 'node 4 (new head)' }, + ], + calculations: [], + linkedLists: [ + { + id: 'list', + label: 'Reversed: 4 → 3 → 2 → 1 → null', + nodes: [ + { id: 'n4', value: 4, index: 0, state: 'success' }, + { id: 'n3', value: 3, index: 1, state: 'success' }, + { id: 'n2', value: 2, index: 2, state: 'success' }, + { id: 'n1', value: 1, index: 3, state: 'success' }, + { id: 'null', value: 0, index: 4, state: 'normal', isNull: true }, + ], + }, + ], + linkedListPointers: [], + }, + }, + ], +}; diff --git a/frontend/src/lib/visualizations/types.ts b/frontend/src/lib/visualizations/types.ts index a8eafec..5639aae 100644 --- a/frontend/src/lib/visualizations/types.ts +++ b/frontend/src/lib/visualizations/types.ts @@ -77,6 +77,11 @@ export interface DataState { pointers: PointerState[]; variables: VariableState[]; calculations: CalculationState[]; + // LinkedList support + linkedLists?: LinkedListState[]; + linkedListPointers?: LinkedListPointerState[]; + // Stack support + stacks?: StackState[]; } /** Single step in the visualization */ @@ -152,3 +157,49 @@ export interface UseVisualizationReturn { currentPhase: VisualizationPhase; progress: number; } + +// ============================================ +// LinkedList Types +// ============================================ + +/** State of a linked list node */ +export interface LinkedListNodeState { + id: string; + value: number; + index: number; + state: 'normal' | 'highlighted' | 'dimmed' | 'success' | 'comparing'; + isNull?: boolean; +} + +/** Complete linked list state */ +export interface LinkedListState { + id: string; + nodes: LinkedListNodeState[]; + label?: string; +} + +/** Pointer for linked list visualization */ +export interface LinkedListPointerState { + id: string; + name: string; + nodeIndex: number; + color: 'slow' | 'fast' | 'prev' | 'curr' | 'next' | 'default'; +} + +// ============================================ +// Stack Types +// ============================================ + +/** State of a stack element */ +export interface StackElementState { + id: string; + value: number | string; + state: 'normal' | 'highlighted' | 'popped' | 'pushing'; +} + +/** Complete stack state */ +export interface StackState { + id: string; + elements: StackElementState[]; + label?: string; +}