feat(viz): heap pattern with kth largest

This commit is contained in:
2025-09-03 21:30:46 +01:00
parent cf0c2153db
commit 4311e97d24
7 changed files with 1153 additions and 1 deletions

View File

@@ -14,7 +14,7 @@ import {
RelatedPatterns,
} from "@/components/patterns";
import { PatternVisualization } from "@/components/visualization";
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization } from "@/components/visualizations-new";
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization, BacktrackingVisualization, HeapVisualization } 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";
@@ -26,6 +26,8 @@ import { treeTraversalAlgorithm } from "@/content/algorithms/tree-traversal";
import { bfsAlgorithm } from "@/content/algorithms/bfs";
import { dfsAlgorithm } from "@/content/algorithms/dfs";
import { coinChangeAlgorithm } from "@/content/algorithms/coin-change";
import { subsetsAlgorithm } from "@/content/algorithms/subsets";
import { kthLargestAlgorithm } from "@/content/algorithms/kth-largest";
interface PageProps {
params: Promise<{ slug: string }>;
@@ -140,6 +142,10 @@ export default async function PatternDetailPage({ params }: PageProps) {
<DFSVisualization algorithm={dfsAlgorithm} />
) : slug === "dynamic-programming" ? (
<CoinChangeVisualization algorithm={coinChangeAlgorithm} />
) : slug === "backtracking" ? (
<BacktrackingVisualization algorithm={subsetsAlgorithm} />
) : slug === "heap" ? (
<HeapVisualization algorithm={kthLargestAlgorithm} />
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
<Card>
<CardHeader>

View File

@@ -0,0 +1,71 @@
'use client';
import { useVisualization } from '@/lib/visualizations/use-visualization';
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
import { VisualizationContainer } from '../core/visualization-container';
import { HeapView } from '../data-structures/heap-view';
import { ArrayView } from '../data-structures/array-view';
interface HeapVisualizationProps {
algorithm: AlgorithmDefinition;
className?: string;
}
export function HeapVisualization({
algorithm,
className,
}: HeapVisualizationProps) {
const {
currentStep,
currentStepIndex,
totalSteps,
playback,
controls,
currentPhase,
progress,
} = useVisualization(algorithm);
const { dataState } = currentStep;
const heap = dataState.heaps?.[0] ?? null;
const inputArray = dataState.arrays?.[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 flex-col items-center gap-6">
{/* Input array */}
{inputArray && (
<ArrayView
array={inputArray}
pointers={dataState.pointers}
elementSize="sm"
/>
)}
{/* Heap visualization */}
{heap && (
<HeapView
heap={heap}
showArrayView={true}
/>
)}
</div>
</VisualizationContainer>
);
}

View File

@@ -0,0 +1,195 @@
'use client';
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { HeapState, HeapNodeState } from '@/lib/visualizations/types';
import { HeapNode } from '../primitives/heap-node';
interface HeapViewProps {
heap: HeapState;
className?: string;
showArrayView?: boolean;
}
const NODE_RADIUS = 24;
const LEVEL_HEIGHT = 72;
const MIN_NODE_SPACING = 60;
const SVG_PADDING = 32;
const ARRAY_HEIGHT = 60;
interface NodePosition {
node: HeapNodeState;
x: number;
y: number;
parentX?: number;
parentY?: number;
}
function calculateHeapLayout(elements: HeapNodeState[], totalWidth: number): NodePosition[] {
const positions: NodePosition[] = [];
if (elements.length === 0) return positions;
for (let i = 0; i < elements.length; i++) {
const level = Math.floor(Math.log2(i + 1));
const posInLevel = i - (Math.pow(2, level) - 1);
const nodesInLevel = Math.pow(2, level);
const levelWidth = totalWidth / nodesInLevel;
const x = levelWidth * (posInLevel + 0.5);
const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_RADIUS;
let parentX: number | undefined;
let parentY: number | undefined;
if (i > 0) {
const parentIndex = Math.floor((i - 1) / 2);
const parentLevel = Math.floor(Math.log2(parentIndex + 1));
const parentPosInLevel = parentIndex - (Math.pow(2, parentLevel) - 1);
const parentNodesInLevel = Math.pow(2, parentLevel);
const parentLevelWidth = totalWidth / parentNodesInLevel;
parentX = parentLevelWidth * (parentPosInLevel + 0.5);
parentY = SVG_PADDING + parentLevel * LEVEL_HEIGHT + NODE_RADIUS;
}
positions.push({
node: elements[i],
x,
y,
parentX,
parentY,
});
}
return positions;
}
export function HeapView({
heap,
className,
showArrayView = true,
}: HeapViewProps) {
const { positions, svgWidth, svgHeight, treeHeight } = useMemo(() => {
if (heap.elements.length === 0) {
return { positions: [], svgWidth: 200, svgHeight: 100, treeHeight: 50 };
}
const depth = Math.floor(Math.log2(heap.elements.length)) + 1;
const maxNodesAtBottom = Math.pow(2, depth - 1);
const width = Math.max(
maxNodesAtBottom * MIN_NODE_SPACING + SVG_PADDING * 2,
200
);
const treeH = depth * LEVEL_HEIGHT + SVG_PADDING;
const height = treeH + (showArrayView ? ARRAY_HEIGHT : SVG_PADDING);
const pos = calculateHeapLayout(heap.elements, width);
return { positions: pos, svgWidth: width, svgHeight: height, treeHeight: treeH };
}, [heap.elements, showArrayView]);
const heapTypeLabel = heap.heapType === 'min' ? 'Min-Heap' : 'Max-Heap';
const sizeLabel = heap.maxSize ? ` (size ≤ ${heap.maxSize})` : '';
return (
<div className={cn('flex flex-col items-center', className)}>
{(heap.label || true) && (
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
{heap.label || heapTypeLabel}{sizeLabel}
</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 }) =>
parentX !== undefined && parentY !== undefined ? (
<motion.line
key={`edge-${node.id}`}
x1={parentX}
y1={parentY + NODE_RADIUS}
x2={x}
y2={y - NODE_RADIUS}
className="stroke-[var(--border)]"
strokeWidth={2}
initial={false}
animate={{
opacity: node.state === 'removing' ? 0.3 : 1,
}}
transition={{ duration: 0.2 }}
/>
) : null
)}
{/* Draw nodes */}
{positions.map(({ node, x, y }) => (
<HeapNode
key={node.id}
node={node}
x={x}
y={y}
radius={NODE_RADIUS}
/>
))}
{/* Root indicator (kth largest for min-heap top-k) */}
{positions.length > 0 && positions[0].node.state === 'highlighted' && (
<g>
<motion.polygon
points={`${positions[0].x},${positions[0].y - NODE_RADIUS - 8} ${positions[0].x - 6},${positions[0].y - NODE_RADIUS - 18} ${positions[0].x + 6},${positions[0].y - NODE_RADIUS - 18}`}
className="fill-[var(--primary)]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
<motion.text
x={positions[0].x}
y={positions[0].y - NODE_RADIUS - 24}
textAnchor="middle"
className="fill-[var(--primary)] text-xs font-medium"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
root
</motion.text>
</g>
)}
{/* Array representation below tree */}
{showArrayView && heap.elements.length > 0 && (
<g transform={`translate(0, ${treeHeight})`}>
<text
x={SVG_PADDING}
y={12}
className="fill-[var(--muted-foreground)] text-xs"
>
Array: [{heap.elements.map(e => e.value).join(', ')}]
</text>
{/* Index markers */}
<text
x={SVG_PADDING}
y={28}
className="fill-[var(--muted-foreground)] text-[10px] opacity-70"
>
idx: {heap.elements.map((_, i) => i).join(' ')}
</text>
</g>
)}
{/* Empty state indicator */}
{heap.elements.length === 0 && (
<text
x={svgWidth / 2}
y={svgHeight / 2}
textAnchor="middle"
className="fill-[var(--muted-foreground)] text-sm italic"
>
(empty)
</text>
)}
</svg>
</div>
);
}

View File

@@ -15,6 +15,8 @@ export { StackElement } from "./primitives/stack-element";
export { QueueElement } from "./primitives/queue-element";
export { TreeNode } from "./primitives/tree-node";
export { GridCell } from "./primitives/grid-cell";
export { DecisionNode } from "./primitives/decision-node";
export { HeapNode } from "./primitives/heap-node";
// Data structures
export { ArrayView } from "./data-structures/array-view";
@@ -23,6 +25,8 @@ export { StackView } from "./data-structures/stack-view";
export { QueueView } from "./data-structures/queue-view";
export { BinaryTreeView } from "./data-structures/binary-tree-view";
export { GridView } from "./data-structures/grid-view";
export { DecisionTreeView } from "./data-structures/decision-tree-view";
export { HeapView } from "./data-structures/heap-view";
// Algorithm visualizations
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
@@ -33,3 +37,5 @@ export { DFSVisualization } from "./algorithms/dfs";
export { TwoPointersVisualization } from "./algorithms/two-pointers";
export { LinkedListVisualization } from "./algorithms/linked-list";
export { CoinChangeVisualization } from "./algorithms/coin-change";
export { BacktrackingVisualization } from "./algorithms/backtracking";
export { HeapVisualization } from "./algorithms/heap";

View File

@@ -0,0 +1,87 @@
'use client';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { HeapNodeState } from '@/lib/visualizations/types';
interface HeapNodeProps {
node: HeapNodeState;
x: number;
y: number;
radius?: number;
className?: string;
}
const STATE_CLASSES = {
normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]',
comparing: 'fill-[var(--info)]/20 stroke-[var(--info)]',
swapping: 'fill-[var(--warning)]/20 stroke-[var(--warning)]',
settled: 'fill-[var(--success)]/20 stroke-[var(--success)]',
highlighted: 'fill-[var(--primary)]/30 stroke-[var(--primary)]',
removing: 'fill-[var(--destructive)]/20 stroke-[var(--destructive)] opacity-50',
} as const;
const TEXT_CLASSES = {
normal: 'fill-[var(--foreground)]',
comparing: 'fill-[var(--info)]',
swapping: 'fill-[var(--warning)]',
settled: 'fill-[var(--success)]',
highlighted: 'fill-[var(--primary)]',
removing: 'fill-[var(--destructive)] opacity-50',
} as const;
export function HeapNode({
node,
x,
y,
radius = 24,
className,
}: HeapNodeProps) {
const isActive = node.state === 'comparing' || node.state === 'swapping' || node.state === 'highlighted';
const isRemoving = node.state === 'removing';
return (
<motion.g
initial={false}
animate={{
scale: isActive ? 1.1 : isRemoving ? 0.8 : 1,
opacity: isRemoving ? 0.5 : 1,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
style={{ transformOrigin: `${x}px ${y}px` }}
className={cn(className)}
>
<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 }}
/>
<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]
)}
>
{node.value}
</text>
</motion.g>
);
}

View File

@@ -0,0 +1,738 @@
import type { AlgorithmDefinition, HeapNodeState, HeapState } from '@/lib/visualizations/types';
/**
* Kth Largest Element in an Array (LeetCode 215)
*
* Input: nums = [3, 2, 1, 5, 6, 4], k = 2
* Output: 5 (2nd largest element)
*
* Using a min-heap of size k:
* - Push each element to heap
* - If heap size > k, pop the smallest
* - Root of min-heap = kth largest
*/
// Helper to create input array state
function createInputArray(
values: number[],
currentIndex?: number,
processedIndices: number[] = []
) {
return {
id: 'input',
label: 'Input Array',
elements: values.map((value, index) => ({
value,
index,
state: (index === currentIndex
? 'highlighted'
: processedIndices.includes(index)
? 'dimmed'
: 'normal') as 'normal' | 'highlighted' | 'dimmed' | 'success' | 'comparing',
})),
};
}
// Helper to create heap state
function createHeapState(
elements: { value: number; state: HeapNodeState['state'] }[],
label?: string
): HeapState {
return {
id: 'heap',
label: label ?? 'Min-Heap (k=2)',
heapType: 'min',
maxSize: 2,
elements: elements.map((el, index) => ({
id: `heap-${index}`,
value: el.value,
index,
state: el.state,
})),
};
}
export const kthLargestAlgorithm: AlgorithmDefinition = {
id: 'kth-largest',
title: 'Kth Largest Element (Min-Heap)',
slug: 'kth-largest',
pattern: {
name: 'Heap',
description:
'Use a min-heap of size k to efficiently find the kth largest element in O(n log k) time.',
},
problemStatement:
'Given an integer array nums and an integer k, return the kth largest element in the array. Use a heap-based approach instead of sorting.',
intuition:
'A min-heap of size k always keeps the k largest elements seen so far. The root (minimum) of this heap is exactly the kth largest overall. As we process elements, we push to the heap and pop if it exceeds size k.',
code: {
language: 'python',
code: `import heapq
def find_kth_largest(nums: list[int], k: int) -> int:
heap = []
for num in nums:
heapq.heappush(heap, num) # Add element
if len(heap) > k:
heapq.heappop(heap) # Remove smallest
return heap[0] # Root = kth largest`,
},
initialExample: {
input: { nums: [3, 2, 1, 5, 6, 4], k: 2 },
expected: 5,
},
steps: [
// ==========================================
// Phase 1: Problem (2 steps)
// ==========================================
{
id: 'problem-1',
phase: 'problem',
explanation:
'Find the 2nd largest element in [3, 2, 1, 5, 6, 4]. Sorting gives [1, 2, 3, 4, 5, 6], so 2nd largest is 5. But can we do better than O(n log n)?',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([])],
},
},
{
id: 'problem-2',
phase: 'problem',
explanation:
'Brute force: Sort the array and return nums[n-k]. Time: O(n log n). With a heap, we can achieve O(n log k), which is faster when k << n.',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'n', name: 'n', value: 6 },
],
calculations: [],
heaps: [createHeapState([])],
},
},
// ==========================================
// Phase 2: Intuition (3 steps)
// ==========================================
{
id: 'intuition-1',
phase: 'intuition',
explanation:
'Key insight: If we maintain a collection of the k largest elements seen so far, the smallest among them is the kth largest overall.',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([])],
},
},
{
id: 'intuition-2',
phase: 'intuition',
explanation:
'A min-heap is perfect: it gives O(log k) access to the minimum element. We keep exactly k elements; the root is always the kth largest.',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'highlighted' },
{ value: 6, state: 'normal' },
], 'Goal: min-heap with k=2 largest')],
},
},
{
id: 'intuition-3',
phase: 'intuition',
explanation:
'Process: For each element, push to heap. If heap size exceeds k, pop the minimum. This evicts smaller elements, keeping only the k largest.',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'highlighted' },
{ value: 6, state: 'normal' },
])],
},
},
// ==========================================
// Phase 3: Pattern (2 steps)
// ==========================================
{
id: 'pattern-1',
phase: 'pattern',
explanation:
'Heap property: In a min-heap, parent ≤ children. Array index i has children at 2i+1 and 2i+2. Root at index 0 is always the minimum.',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [],
calculations: [],
heaps: [createHeapState([])],
},
},
{
id: 'pattern-2',
phase: 'pattern',
explanation:
'Top-K pattern: Use min-heap of size k for "k largest", max-heap of size k for "k smallest". The root gives the kth element after processing all items.',
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([])],
},
},
// ==========================================
// Phase 4: Code (3 steps)
// ==========================================
{
id: 'code-1',
phase: 'code',
explanation:
'Initialize an empty heap. We\'ll use Python\'s heapq which implements a min-heap.',
codeLine: 4,
codeHighlightLines: [4],
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([])],
},
},
{
id: 'code-2',
phase: 'code',
explanation:
'For each number: push to heap (heappush maintains heap property by bubbling up). If size > k: pop minimum (heappop bubbles down to restore property).',
codeLine: 7,
codeHighlightLines: [6, 7, 8, 9],
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([])],
},
},
{
id: 'code-3',
phase: 'code',
explanation:
'After processing all elements, the heap contains exactly k largest. Return heap[0] - the root, which is the minimum of the k largest = kth largest overall.',
codeLine: 11,
codeHighlightLines: [11],
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4])],
pointers: [],
variables: [{ id: 'k', name: 'k', value: 2 }],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'highlighted' },
{ value: 6, state: 'normal' },
])],
},
},
// ==========================================
// Phase 5: Execution (~25 steps)
// ==========================================
// Process 3
{
id: 'exec-1',
phase: 'execution',
explanation:
'Start with empty heap. Process first element: 3.',
codeLine: 6,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 0)],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'num', name: 'num', value: 3 },
],
calculations: [],
heaps: [createHeapState([])],
},
},
{
id: 'exec-2',
phase: 'execution',
explanation:
'Push 3 to heap. Heap: [3]. Size = 1 ≤ k = 2, so no pop needed.',
codeLine: 7,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 0)],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 1 },
],
calculations: [],
heaps: [createHeapState([
{ value: 3, state: 'settled' },
])],
},
},
// Process 2
{
id: 'exec-3',
phase: 'execution',
explanation:
'Process next element: 2.',
codeLine: 6,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 1, [0])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'num', name: 'num', value: 2, previousValue: 3 },
],
calculations: [],
heaps: [createHeapState([
{ value: 3, state: 'normal' },
])],
},
},
{
id: 'exec-4',
phase: 'execution',
explanation:
'Push 2 to heap. 2 < 3, so 2 bubbles up to become the new root. Heap: [2, 3].',
codeLine: 7,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 1, [0])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'comparing' },
{ value: 3, state: 'swapping' },
])],
},
},
{
id: 'exec-5',
phase: 'execution',
explanation:
'Heap size = 2 = k. No pop needed. Heap: [2, 3]. Root 2 would be kth largest so far.',
codeLine: 8,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 1, [0])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'settled' },
{ value: 3, state: 'settled' },
])],
},
},
// Process 1
{
id: 'exec-6',
phase: 'execution',
explanation:
'Process next element: 1.',
codeLine: 6,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 2, [0, 1])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'num', name: 'num', value: 1, previousValue: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'normal' },
{ value: 3, state: 'normal' },
])],
},
},
{
id: 'exec-7',
phase: 'execution',
explanation:
'Push 1 to heap. 1 < 2, so 1 bubbles up. Heap becomes: [1, 3, 2].',
codeLine: 7,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 2, [0, 1])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 3 },
],
calculations: [],
heaps: [createHeapState([
{ value: 1, state: 'comparing' },
{ value: 3, state: 'normal' },
{ value: 2, state: 'swapping' },
])],
},
},
{
id: 'exec-8',
phase: 'execution',
explanation:
'Heap size = 3 > k = 2. Pop the minimum (1). After pop and bubble-down: [2, 3].',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 2, [0, 1])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2, previousValue: 3 },
{ id: 'popped', name: 'popped', value: 1 },
],
calculations: [],
heaps: [createHeapState([
{ value: 1, state: 'removing' },
{ value: 3, state: 'normal' },
{ value: 2, state: 'normal' },
])],
},
},
{
id: 'exec-9',
phase: 'execution',
explanation:
'After removing 1, heap restored to [2, 3]. Element 1 was too small to be in top-2.',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 2, [0, 1])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'settled' },
{ value: 3, state: 'settled' },
])],
},
},
// Process 5
{
id: 'exec-10',
phase: 'execution',
explanation:
'Process next element: 5.',
codeLine: 6,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 3, [0, 1, 2])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'num', name: 'num', value: 5, previousValue: 1 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'normal' },
{ value: 3, state: 'normal' },
])],
},
},
{
id: 'exec-11',
phase: 'execution',
explanation:
'Push 5 to heap. 5 > 2 (parent), so no bubble-up needed. Heap: [2, 3, 5].',
codeLine: 7,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 3, [0, 1, 2])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 3 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'normal' },
{ value: 3, state: 'normal' },
{ value: 5, state: 'comparing' },
])],
},
},
{
id: 'exec-12',
phase: 'execution',
explanation:
'Heap size = 3 > k = 2. Pop minimum (2). Move 5 to root, bubble down: 5 > 3, so swap. Result: [3, 5].',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 3, [0, 1, 2])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'popped', name: 'popped', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 2, state: 'removing' },
{ value: 3, state: 'swapping' },
{ value: 5, state: 'swapping' },
])],
},
},
{
id: 'exec-13',
phase: 'execution',
explanation:
'After pop: heap = [3, 5]. Now tracking top-2: {3, 5}. Root 3 = current kth largest candidate.',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 3, [0, 1, 2])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 3, state: 'settled' },
{ value: 5, state: 'settled' },
])],
},
},
// Process 6
{
id: 'exec-14',
phase: 'execution',
explanation:
'Process next element: 6.',
codeLine: 6,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 4, [0, 1, 2, 3])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'num', name: 'num', value: 6, previousValue: 5 },
],
calculations: [],
heaps: [createHeapState([
{ value: 3, state: 'normal' },
{ value: 5, state: 'normal' },
])],
},
},
{
id: 'exec-15',
phase: 'execution',
explanation:
'Push 6 to heap. 6 > 3 (parent), no bubble-up. Heap: [3, 5, 6].',
codeLine: 7,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 4, [0, 1, 2, 3])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 3 },
],
calculations: [],
heaps: [createHeapState([
{ value: 3, state: 'normal' },
{ value: 5, state: 'normal' },
{ value: 6, state: 'comparing' },
])],
},
},
{
id: 'exec-16',
phase: 'execution',
explanation:
'Heap size = 3 > k. Pop minimum (3). After bubble-down: [5, 6]. Now tracking {5, 6}.',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 4, [0, 1, 2, 3])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'popped', name: 'popped', value: 3 },
],
calculations: [],
heaps: [createHeapState([
{ value: 3, state: 'removing' },
{ value: 5, state: 'swapping' },
{ value: 6, state: 'normal' },
])],
},
},
{
id: 'exec-17',
phase: 'execution',
explanation:
'Heap now [5, 6]. Root 5 = 2nd largest so far. The two largest seen are 6 and 5.',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 4, [0, 1, 2, 3])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'settled' },
{ value: 6, state: 'settled' },
])],
},
},
// Process 4
{
id: 'exec-18',
phase: 'execution',
explanation:
'Process last element: 4.',
codeLine: 6,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 5, [0, 1, 2, 3, 4])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'num', name: 'num', value: 4, previousValue: 6 },
],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'normal' },
{ value: 6, state: 'normal' },
])],
},
},
{
id: 'exec-19',
phase: 'execution',
explanation:
'Push 4 to heap. 4 < 5 (parent at index 0), so 4 bubbles up. Heap becomes [4, 6, 5].',
codeLine: 7,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 5, [0, 1, 2, 3, 4])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 3 },
],
calculations: [],
heaps: [createHeapState([
{ value: 4, state: 'comparing' },
{ value: 6, state: 'normal' },
{ value: 5, state: 'swapping' },
])],
},
},
{
id: 'exec-20',
phase: 'execution',
explanation:
'Heap size = 3 > k. Pop minimum (4). After pop: [5, 6]. 4 < 5, so it\'s not in top-2.',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], 5, [0, 1, 2, 3, 4])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'popped', name: 'popped', value: 4 },
],
calculations: [],
heaps: [createHeapState([
{ value: 4, state: 'removing' },
{ value: 6, state: 'swapping' },
{ value: 5, state: 'normal' },
])],
},
},
{
id: 'exec-21',
phase: 'execution',
explanation:
'Final heap: [5, 6]. These are the 2 largest elements from the array.',
codeLine: 9,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], undefined, [0, 1, 2, 3, 4, 5])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'size', name: 'size', value: 2 },
],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'settled' },
{ value: 6, state: 'settled' },
])],
},
},
// Return result
{
id: 'exec-22',
phase: 'execution',
explanation:
'Return heap[0] = 5. The root of the min-heap (smallest of the k largest) is the kth largest element.',
codeLine: 11,
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], undefined, [0, 1, 2, 3, 4, 5])],
pointers: [],
variables: [
{ id: 'k', name: 'k', value: 2 },
{ id: 'result', name: 'result', value: 5 },
],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'highlighted' },
{ value: 6, state: 'normal' },
])],
},
},
{
id: 'exec-23',
phase: 'execution',
explanation:
'Done! The 2nd largest element is 5. Time: O(n log k), Space: O(k). Much faster than sorting when k is small.',
codeLine: 11,
decision: {
question: 'Why min-heap for kth largest?',
answer: 'Min-heap lets us quickly remove the smallest among k elements',
action: 'Root is always the kth largest after processing all elements',
},
dataState: {
arrays: [createInputArray([3, 2, 1, 5, 6, 4], undefined, [0, 1, 2, 3, 4, 5])],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: 5 },
{ id: 'time', name: 'Time', value: 'O(n log k)' },
{ id: 'space', name: 'Space', value: 'O(k)' },
],
calculations: [],
heaps: [createHeapState([
{ value: 5, state: 'highlighted' },
{ value: 6, state: 'normal' },
])],
},
},
],
};

View File

@@ -89,6 +89,10 @@ export interface DataState {
// Grid support (for DP visualizations)
grids?: GridState[];
gridPointers?: GridPointerState[];
// Decision tree support (for Backtracking)
decisionTrees?: DecisionTreeState[];
// Heap support
heaps?: HeapState[];
}
/** Single step in the visualization */
@@ -250,6 +254,30 @@ export interface QueueState {
label?: string;
}
// ============================================
// Decision Tree Types (for Backtracking)
// ============================================
/** State of a decision tree node for backtracking */
export interface DecisionNodeState {
id: string;
value: string; // Current subset as string: "[]", "[1]", "[1,2]"
state: 'normal' | 'exploring' | 'complete' | 'backtracking' | 'current';
left: string | null; // Include branch
right: string | null; // Exclude branch
decision?: string; // "Include 2?" label
depth: number; // Recursion depth (0-indexed)
}
/** Complete decision tree state */
export interface DecisionTreeState {
id: string;
nodes: DecisionNodeState[];
rootId: string;
label?: string;
currentPath?: string[]; // IDs of nodes in current exploration path
}
// ============================================
// Grid Types (for DP visualizations)
// ============================================
@@ -280,3 +308,24 @@ export interface GridPointerState {
col: number;
color: 'current' | 'dependency' | 'result' | 'default';
}
// ============================================
// Heap Types
// ============================================
/** State of a heap node */
export interface HeapNodeState {
id: string;
value: number;
index: number; // Position in heap array (0-indexed)
state: 'normal' | 'comparing' | 'swapping' | 'settled' | 'highlighted' | 'removing';
}
/** Complete heap state */
export interface HeapState {
id: string;
elements: HeapNodeState[];
label?: string;
heapType: 'min' | 'max';
maxSize?: number; // For bounded heaps (like top-k)
}