diff --git a/src/content/algorithms/huffman-coding.ts b/src/content/algorithms/huffman-coding.ts new file mode 100644 index 0000000..efede9b --- /dev/null +++ b/src/content/algorithms/huffman-coding.ts @@ -0,0 +1,60 @@ +import type { Locale } from '@i18n/translations' + +const descriptions: Record = { + en: `Huffman Coding + +Huffman Coding is a greedy algorithm for lossless data compression. It assigns shorter binary codes to frequent characters and longer codes to rare ones, reducing the total number of bits needed to represent the data. + +How it works: +1. Count how often each character appears +2. Create a leaf node per character and put them in a min-priority queue +3. Repeatedly remove the two lowest-frequency nodes and merge them under a new parent whose frequency is their sum +4. When one node remains, use it as the tree root +5. Assign codes by walking the tree: left = 0, right = 1 + +Why it works: + No code is a prefix of another, so the encoded bitstream decodes unambiguously. The greedy merge guarantees an optimal prefix code for the given frequencies. + +Time Complexity: + Best: O(n log n) + Average: O(n log n) + Worst: O(n log n) + +Space Complexity: O(n) + +Properties: + - Lossless: the original data is recovered exactly + - Optimal among prefix codes for a known frequency distribution + - Used in DEFLATE (ZIP, gzip, PNG), JPEG, and MP3 + +Invented by David A. Huffman in 1952 while he was a student at MIT, it remains a cornerstone of modern compression.`, + es: `Codificación de Huffman + +La Codificación de Huffman es un algoritmo voraz para compresión de datos sin pérdida. Asigna códigos binarios más cortos a los caracteres frecuentes y más largos a los raros, reduciendo la cantidad total de bits necesarios para representar los datos. + +Cómo funciona: +1. Contar cuántas veces aparece cada carácter +2. Crear un nodo hoja por carácter y ponerlos en una cola de prioridad mínima +3. Quitar repetidamente los dos nodos de menor frecuencia y fusionarlos bajo un nuevo padre cuya frecuencia sea la suma +4. Cuando quede un solo nodo, usarlo como raíz del árbol +5. Asignar códigos recorriendo el árbol: izquierda = 0, derecha = 1 + +Por qué funciona: + Ningún código es prefijo de otro, así que el flujo de bits codificado se decodifica sin ambigüedad. La fusión voraz garantiza un código de prefijo óptimo para las frecuencias dadas. + +Complejidad Temporal: + Mejor: O(n log n) + Promedio: O(n log n) + Peor: O(n log n) + +Complejidad Espacial: O(n) + +Propiedades: + - Sin pérdida: los datos originales se recuperan exactamente + - Óptimo entre los códigos de prefijo para una distribución de frecuencias conocida + - Usado en DEFLATE (ZIP, gzip, PNG), JPEG y MP3 + +Inventado por David A. Huffman en 1952 cuando era estudiante en el MIT, sigue siendo un pilar de la compresión moderna.`, +} + +export default descriptions diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 028b918..5e3d771 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -163,6 +163,7 @@ export const translations: Record = { Backtracking: 'Backtracking', 'Divide and Conquer': 'Divide and Conquer', Math: 'Math', + Compression: 'Compression', }, }, @@ -243,6 +244,7 @@ export const translations: Record = { Backtracking: 'Backtracking', 'Divide and Conquer': 'Divide y Vencerás', Math: 'Matemáticas', + Compression: 'Compresión', }, }, } diff --git a/src/lib/algorithms/catalog.ts b/src/lib/algorithms/catalog.ts index 99e6f65..d13fc3f 100644 --- a/src/lib/algorithms/catalog.ts +++ b/src/lib/algorithms/catalog.ts @@ -295,6 +295,14 @@ export const algorithmCatalog: AlgorithmSummary[] = [ difficulty: 'intermediate', visualization: 'matrix', }, + // Compression + { + id: 'huffman-coding', + name: 'Huffman Coding', + category: 'Compression', + difficulty: 'advanced', + visualization: 'concept', + }, ] const CATEGORY_ORDER = [ @@ -307,6 +315,7 @@ const CATEGORY_ORDER = [ 'Backtracking', 'Divide and Conquer', 'Math', + 'Compression', ] as const export const catalogCategories: CategorySummary[] = CATEGORY_ORDER.map((name) => ({ diff --git a/src/lib/algorithms/compression.ts b/src/lib/algorithms/compression.ts new file mode 100644 index 0000000..9adf225 --- /dev/null +++ b/src/lib/algorithms/compression.ts @@ -0,0 +1,351 @@ +import type { Algorithm, Step, HuffmanState } from '@lib/types' +import { d } from '@lib/algorithms/shared' + +type HNode = HuffmanState['nodes'][number] +type HNodeState = NonNullable[number] + +const huffmanCoding: Algorithm = { + id: 'huffman-coding', + name: 'Huffman Coding', + category: 'Compression', + difficulty: 'advanced', + visualization: 'concept', + code: `function huffmanCoding(text) { + // 1. Count character frequencies + const freq = {}; + for (const ch of text) { + freq[ch] = (freq[ch] || 0) + 1; + } + + // 2. Create a leaf node per character and push + // them all into a min-priority queue + let pq = Object.entries(freq).map( + ([char, f]) => ({ char, freq: f, left: null, right: null }) + ); + + // 3. Build the tree: repeatedly merge the two + // lowest-frequency nodes into a new parent + while (pq.length > 1) { + pq.sort((a, b) => a.freq - b.freq); + const left = pq.shift(); + const right = pq.shift(); + pq.push({ char: null, freq: left.freq + right.freq, left, right }); + } + const root = pq[0]; + + // 4. Walk the tree to assign a binary code to + // each character (left = 0, right = 1) + const codes = {}; + function assign(node, code) { + if (!node.left && !node.right) { + codes[node.char] = code || '0'; + return; + } + assign(node.left, code + '0'); + assign(node.right, code + '1'); + } + assign(root, ''); + + // 5. Encode the text using the generated codes + const encoded = [...text].map((ch) => codes[ch]).join(''); + return { codes, encoded }; +} + +huffmanCoding('ABRACADABRA');`, + + generateSteps(locale = 'en') { + const TEXT = 'ABRACADABRA' + const steps: Step[] = [] + + // ── Model state, mutated as the algorithm runs ── + const nodes: Record = {} + let nextId = 0 + const newNode = ( + char: string | null, + freq: number, + left: number | null, + right: number | null, + ): number => { + const id = nextId++ + nodes[id] = { id, char, freq, left, right } + return id + } + + // Snapshot helper — clones nodes so each step is immutable + const snap = (extra: { + phase: 'frequency' | 'build' | 'encode' | 'done' + queue: number[] + nodeStates?: Record + highlightChar?: string | null + freqTable?: { char: string; freq: number; active?: boolean }[] + codes?: { char: string; code: string; freq: number; active?: boolean }[] + activeCode?: string | null + summary?: { + uniqueChars: number + originalBits: number + compressedBits: number + avgBits: number + savingPct: number + encoded: string + } + operation?: string + }) => ({ + type: 'huffman' as const, + text: TEXT, + nodes: Object.fromEntries(Object.entries(nodes).map(([k, v]) => [k, { ...v }])), + ...extra, + }) + + // ════════════════════════════════════════════ + // Phase 1 — Frequency counting + // ════════════════════════════════════════════ + + // Count frequencies in order of first appearance + const freqMap = new Map() + for (const ch of TEXT) freqMap.set(ch, (freqMap.get(ch) ?? 0) + 1) + const orderedChars = [...freqMap.keys()] + + steps.push({ + concept: snap({ + phase: 'frequency', + queue: [], + freqTable: [], + operation: d(locale, 'Input', 'Entrada'), + }), + description: d( + locale, + `Compress the text "${TEXT}" (${TEXT.length} characters). First, count how often each character appears.`, + `Comprimir el texto "${TEXT}" (${TEXT.length} caracteres). Primero, contar cuántas veces aparece cada carácter.`, + ), + codeLine: 3, + variables: { text: TEXT, length: TEXT.length }, + }) + + // Build a leaf node + freq table row per character, one step each + const queue: number[] = [] + const freqTable: { char: string; freq: number; active?: boolean }[] = [] + + for (const ch of orderedChars) { + const f = freqMap.get(ch)! + const id = newNode(ch, f, null, null) + queue.push(id) + freqTable.push({ char: ch, freq: f }) + + steps.push({ + concept: snap({ + phase: 'frequency', + queue: [...queue], + highlightChar: ch, + freqTable: freqTable.map((r) => ({ ...r, active: r.char === ch })), + nodeStates: { [id]: 'new' }, + operation: d(locale, 'Counting frequencies', 'Contando frecuencias'), + }), + description: d( + locale, + `'${ch}' appears ${f} time${f === 1 ? '' : 's'}. Create a leaf node for it and add it to the priority queue.`, + `'${ch}' aparece ${f} ${f === 1 ? 'vez' : 'veces'}. Crear un nodo hoja y agregarlo a la cola de prioridad.`, + ), + codeLine: 5, + variables: { char: ch, freq: f }, + }) + } + + // Sort the queue by (freq, id) — this is the min-priority ordering used throughout + const sortQueue = (q: number[]) => [...q].sort((a, b) => nodes[a].freq - nodes[b].freq || a - b) + + let pq = sortQueue(queue) + + steps.push({ + concept: snap({ + phase: 'build', + queue: [...pq], + freqTable: freqTable.map((r) => ({ ...r })), + operation: d(locale, 'Priority queue ready', 'Cola de prioridad lista'), + }), + description: d( + locale, + `${pq.length} leaf nodes are now in the priority queue, sorted by frequency (lowest first). Time to build the tree.`, + `Ahora hay ${pq.length} nodos hoja en la cola de prioridad, ordenados por frecuencia (menor primero). Hora de construir el árbol.`, + ), + codeLine: 11, + variables: { queueSize: pq.length }, + }) + + // ════════════════════════════════════════════ + // Phase 2 — Build the tree + // ════════════════════════════════════════════ + + while (pq.length > 1) { + const leftId = pq[0] + const rightId = pq[1] + + // Step A — highlight the two lowest-frequency nodes + steps.push({ + concept: snap({ + phase: 'build', + queue: [...pq], + freqTable: freqTable.map((r) => ({ ...r })), + nodeStates: { [leftId]: 'merging', [rightId]: 'merging' }, + operation: d(locale, 'Pick the two smallest', 'Tomar los dos menores'), + }), + description: d( + locale, + `Remove the two lowest-frequency nodes: ${nodeLabel(nodes, leftId)} and ${nodeLabel(nodes, rightId)} (${nodes[leftId].freq} + ${nodes[rightId].freq} = ${nodes[leftId].freq + nodes[rightId].freq}).`, + `Quitar los dos nodos de menor frecuencia: ${nodeLabel(nodes, leftId)} y ${nodeLabel(nodes, rightId)} (${nodes[leftId].freq} + ${nodes[rightId].freq} = ${nodes[leftId].freq + nodes[rightId].freq}).`, + ), + codeLine: 18, + variables: { + left: nodes[leftId].freq, + right: nodes[rightId].freq, + sum: nodes[leftId].freq + nodes[rightId].freq, + }, + }) + + // Merge into a new parent + const parentFreq = nodes[leftId].freq + nodes[rightId].freq + const parentId = newNode(null, parentFreq, leftId, rightId) + pq = sortQueue([...pq.slice(2), parentId]) + + // Step B — show the new parent in the queue + steps.push({ + concept: snap({ + phase: 'build', + queue: [...pq], + freqTable: freqTable.map((r) => ({ ...r })), + nodeStates: { [parentId]: 'new', [leftId]: 'path', [rightId]: 'path' }, + operation: d(locale, 'Merge into a parent', 'Fusionar en un padre'), + }), + description: d( + locale, + `Create a parent node of frequency ${parentFreq} linking both, then push it back. ${pq.length} node${pq.length === 1 ? '' : 's'} left in the queue.`, + `Crear un nodo padre de frecuencia ${parentFreq} que enlaza a ambos y devolverlo a la cola. Quedan ${pq.length} nodo${pq.length === 1 ? '' : 's'} en la cola.`, + ), + codeLine: 20, + variables: { parentFreq, queueSize: pq.length }, + }) + } + + const rootId = pq[0] + + // ════════════════════════════════════════════ + // Phase 3 — Assign codes (DFS, left = 0, right = 1) + // ════════════════════════════════════════════ + + const codeByChar = new Map() + const orderForCodes: { char: string; code: string; path: number[] }[] = [] + + const assign = (id: number, code: string, path: number[]) => { + const node = nodes[id] + const here = [...path, id] + if (node.left === null && node.right === null) { + const finalCode = code || '0' + codeByChar.set(node.char!, finalCode) + orderForCodes.push({ char: node.char!, code: finalCode, path: here }) + return + } + if (node.left !== null) assign(node.left, code + '0', here) + if (node.right !== null) assign(node.right, code + '1', here) + } + assign(rootId, '', []) + + steps.push({ + concept: snap({ + phase: 'encode', + queue: [rootId], + freqTable: freqTable.map((r) => ({ ...r })), + codes: [], + operation: d(locale, 'Assign codes', 'Asignar códigos'), + }), + description: d( + locale, + 'The tree is complete. Walk it from the root: every left branch adds a 0, every right branch adds a 1. The code for a character is the path to its leaf.', + 'El árbol está completo. Recorrerlo desde la raíz: cada rama izquierda agrega un 0, cada rama derecha un 1. El código de un carácter es el camino hasta su hoja.', + ), + codeLine: 26, + variables: { root: nodes[rootId].freq }, + }) + + const codesAcc: { char: string; code: string; freq: number; active?: boolean }[] = [] + for (const entry of orderForCodes) { + const nodeStates: Record = {} + for (const pid of entry.path) nodeStates[pid] = 'path' + nodeStates[entry.path[entry.path.length - 1]] = 'leafFound' + + codesAcc.push({ char: entry.char, code: entry.code, freq: freqMap.get(entry.char)! }) + + steps.push({ + concept: snap({ + phase: 'encode', + queue: [rootId], + freqTable: freqTable.map((r) => ({ ...r, active: r.char === entry.char })), + codes: codesAcc.map((c) => ({ ...c, active: c.char === entry.char })), + nodeStates, + activeCode: entry.code, + operation: d(locale, 'Tracing path', 'Trazando camino'), + }), + description: d( + locale, + `'${entry.char}' is reached by the path "${entry.code}", so its Huffman code is ${entry.code} (${entry.code.length} bit${entry.code.length === 1 ? '' : 's'}).`, + `Se llega a '${entry.char}' por el camino "${entry.code}", así que su código de Huffman es ${entry.code} (${entry.code.length} bit${entry.code.length === 1 ? '' : 's'}).`, + ), + codeLine: 28, + variables: { char: entry.char, code: entry.code, bits: entry.code.length }, + }) + } + + // ════════════════════════════════════════════ + // Phase 4 — Encode & summary + // ════════════════════════════════════════════ + + const encoded = [...TEXT].map((ch) => codeByChar.get(ch)!).join('') + const uniqueChars = orderedChars.length + const fixedWidth = Math.max(1, Math.ceil(Math.log2(uniqueChars))) + const originalBits = TEXT.length * 8 // ASCII baseline + const compressedBits = encoded.length + const avgBits = compressedBits / TEXT.length + const savingPct = Math.round((1 - compressedBits / originalBits) * 100) + + steps.push({ + concept: snap({ + phase: 'done', + queue: [rootId], + freqTable: freqTable.map((r) => ({ ...r })), + codes: codesAcc.map((c) => ({ ...c })), + summary: { + uniqueChars, + originalBits, + compressedBits, + avgBits, + savingPct, + encoded, + }, + operation: d(locale, 'Done', 'Listo'), + }), + description: d( + locale, + `Encoding "${TEXT}" takes ${compressedBits} bits with Huffman vs ${originalBits} bits as 8-bit ASCII — about ${savingPct}% smaller (~${avgBits.toFixed(2)} bits/char instead of a fixed ${fixedWidth}).`, + `Codificar "${TEXT}" usa ${compressedBits} bits con Huffman frente a ${originalBits} bits en ASCII de 8 bits — alrededor de ${savingPct}% más chico (~${avgBits.toFixed(2)} bits/carácter en vez de ${fixedWidth} fijos).`, + ), + codeLine: 38, + variables: { + originalBits, + compressedBits, + saving: `${savingPct}%`, + }, + consoleOutput: [ + `encoded: ${encoded}`, + `${originalBits} bits → ${compressedBits} bits (${savingPct}% smaller)`, + ], + }) + + return steps + }, +} + +/** Short label for a node: its char (leaf) or its frequency sum (internal) */ +function nodeLabel(nodes: Record, id: number): string { + const n = nodes[id] + return n.char !== null ? `'${n.char}'` : `(${n.freq})` +} + +export { huffmanCoding } diff --git a/src/lib/algorithms/cpp/compression.ts b/src/lib/algorithms/cpp/compression.ts new file mode 100644 index 0000000..6c47f99 --- /dev/null +++ b/src/lib/algorithms/cpp/compression.ts @@ -0,0 +1,58 @@ +import type { CodeImplementation } from '@lib/types' +import { annotated } from '@lib/code-languages' + +export const compressionCpp: Record = { + 'huffman-coding': annotated(`struct Node { + char ch; + int freq; + Node* left; + Node* right; +}; + +struct CompareFreq { + bool operator()(const Node* a, const Node* b) const { + return a->freq > b->freq; + } +}; + +struct HuffmanResult { + unordered_map codes; + string encoded; +}; + +HuffmanResult huffmanCoding(const string& text) { + unordered_map freq; //@3 + for (char ch : text) { + ++freq[ch]; //@5 + } + + priority_queue, CompareFreq> queue; //@11 + for (auto [ch, count] : freq) { + queue.push(new Node{ch, count, nullptr, nullptr}); + } + + while (queue.size() > 1) { + Node* left = queue.top(); queue.pop(); //@18 + Node* right = queue.top(); queue.pop(); //@19 + queue.push(new Node{'\\0', left->freq + right->freq, left, right}); //@20 + } + Node* root = queue.top(); + + unordered_map codes; //@26 + assignCodes(root, "", codes); + string encoded; //@38 + for (char ch : text) encoded += codes[ch]; + return {codes, encoded}; +} + +void assignCodes(Node* node, string code, unordered_map& codes) { + if (node->left == nullptr && node->right == nullptr) { //@28 + codes[node->ch] = code.empty() ? "0" : code; + return; + } + assignCodes(node->left, code + "0", codes); + assignCodes(node->right, code + "1", codes); +} + +huffmanCoding("ABRACADABRA");`), +} diff --git a/src/lib/algorithms/cpp/index.ts b/src/lib/algorithms/cpp/index.ts index ced9795..aa32986 100644 --- a/src/lib/algorithms/cpp/index.ts +++ b/src/lib/algorithms/cpp/index.ts @@ -9,6 +9,7 @@ import { dynamicProgrammingCpp } from '@lib/algorithms/cpp/dynamic-programming' import { backtrackingCpp } from '@lib/algorithms/cpp/backtracking' import { divideAndConquerCpp } from '@lib/algorithms/cpp/divide-and-conquer' import { mathCpp } from '@lib/algorithms/cpp/math' +import { compressionCpp } from '@lib/algorithms/cpp/compression' /** C++ translations keyed by algorithm id. */ export const cppImplementations: Record = { @@ -21,4 +22,5 @@ export const cppImplementations: Record = { ...backtrackingCpp, ...divideAndConquerCpp, ...mathCpp, + ...compressionCpp, } diff --git a/src/lib/algorithms/index.ts b/src/lib/algorithms/index.ts index c1adcd4..035cc0f 100644 --- a/src/lib/algorithms/index.ts +++ b/src/lib/algorithms/index.ts @@ -44,6 +44,8 @@ import { towerOfHanoi } from '@lib/algorithms/divide-and-conquer' import { sieveOfEratosthenes } from '@lib/algorithms/math' +import { huffmanCoding } from '@lib/algorithms/compression' + /** * Full algorithm list for SSR / static generation (getStaticPaths, SEO). * Client islands should import `catalog` + `loadAlgorithm` instead so the @@ -99,6 +101,8 @@ export const algorithms: Algorithm[] = [ towerOfHanoi, // Math sieveOfEratosthenes, + // Compression + huffmanCoding, ] export const categories: Category[] = [ @@ -120,6 +124,7 @@ export const categories: Category[] = [ algorithms: algorithms.filter((a) => a.category === 'Divide and Conquer'), }, { name: 'Math', algorithms: algorithms.filter((a) => a.category === 'Math') }, + { name: 'Compression', algorithms: algorithms.filter((a) => a.category === 'Compression') }, ] export { algorithmCatalog, catalogCategories, getCatalogEntry, isKnownAlgorithmId } from './catalog' diff --git a/src/lib/algorithms/java/compression.ts b/src/lib/algorithms/java/compression.ts new file mode 100644 index 0000000..eb27017 --- /dev/null +++ b/src/lib/algorithms/java/compression.ts @@ -0,0 +1,61 @@ +import type { CodeImplementation } from '@lib/types' +import { annotated } from '@lib/code-languages' + +export const compressionJava: Record = { + 'huffman-coding': annotated(`static final class Node { + final Character ch; + final int freq; + final Node left; + final Node right; + + Node(Character ch, int freq) { + this(ch, freq, null, null); + } + + Node(Character ch, int freq, Node left, Node right) { + this.ch = ch; + this.freq = freq; + this.left = left; + this.right = right; + } +} + +record HuffmanResult(Map codes, String encoded) {} + +HuffmanResult huffmanCoding(String text) { + Map freq = new LinkedHashMap<>(); //@3 + for (char ch : text.toCharArray()) { + freq.merge(ch, 1, Integer::sum); //@5 + } + + PriorityQueue queue = new PriorityQueue<>( //@11 + Comparator.comparingInt(node -> node.freq) + ); + freq.forEach((ch, count) -> queue.add(new Node(ch, count))); + + while (queue.size() > 1) { + Node left = queue.remove(); //@18 + Node right = queue.remove(); //@19 + queue.add(new Node(null, left.freq + right.freq, left, right)); //@20 + } + Node root = queue.remove(); + + Map codes = new HashMap<>(); //@26 + assignCodes(root, "", codes); + String encoded = text.chars() //@38 + .mapToObj(ch -> codes.get((char) ch)) + .collect(Collectors.joining()); + return new HuffmanResult(codes, encoded); +} + +void assignCodes(Node node, String code, Map codes) { + if (node.left == null && node.right == null) { //@28 + codes.put(node.ch, code.isEmpty() ? "0" : code); + return; + } + assignCodes(node.left, code + "0", codes); + assignCodes(node.right, code + "1", codes); +} + +huffmanCoding("ABRACADABRA");`), +} diff --git a/src/lib/algorithms/java/index.ts b/src/lib/algorithms/java/index.ts index 1b61e40..717115f 100644 --- a/src/lib/algorithms/java/index.ts +++ b/src/lib/algorithms/java/index.ts @@ -9,6 +9,7 @@ import { dynamicProgrammingJava } from '@lib/algorithms/java/dynamic-programming import { backtrackingJava } from '@lib/algorithms/java/backtracking' import { divideAndConquerJava } from '@lib/algorithms/java/divide-and-conquer' import { mathJava } from '@lib/algorithms/java/math' +import { compressionJava } from '@lib/algorithms/java/compression' /** Java translations keyed by algorithm id. */ export const javaImplementations: Record = { @@ -21,4 +22,5 @@ export const javaImplementations: Record = { ...backtrackingJava, ...divideAndConquerJava, ...mathJava, + ...compressionJava, } diff --git a/src/lib/algorithms/loaders.ts b/src/lib/algorithms/loaders.ts index c49f0a0..26b50e6 100644 --- a/src/lib/algorithms/loaders.ts +++ b/src/lib/algorithms/loaders.ts @@ -13,6 +13,7 @@ type ImplementationGroup = | 'backtracking' | 'divide-and-conquer' | 'math' + | 'compression' /** Cache so revisiting an algorithm does not re-download its chunk. */ const algorithmCache = new Map>() @@ -96,6 +97,10 @@ const ALGORITHM_LOADERS: Record Promise> = { // Math 'sieve-of-eratosthenes': () => import('./math?algorithm=sieveOfEratosthenes').then(readDefaultAlgorithm), + + // Compression + 'huffman-coding': () => + import('./compression?algorithm=huffmanCoding').then(readDefaultAlgorithm), } const IMPLEMENTATION_GROUP_BY_CATEGORY: Record = { @@ -108,6 +113,7 @@ const IMPLEMENTATION_GROUP_BY_CATEGORY: Record = { Backtracking: 'backtracking', 'Divide and Conquer': 'divide-and-conquer', Math: 'math', + Compression: 'compression', } const IMPLEMENTATION_LOADERS: Record< @@ -130,6 +136,8 @@ const IMPLEMENTATION_LOADERS: Record< 'divide-and-conquer': () => import('./python/divide-and-conquer?implementation-loader').then(readImplementationLoader), math: () => import('./python/math?implementation-loader').then(readImplementationLoader), + compression: () => + import('./python/compression?implementation-loader').then(readImplementationLoader), }, java: { concepts: () => import('./java/concepts?implementation-loader').then(readImplementationLoader), @@ -146,6 +154,8 @@ const IMPLEMENTATION_LOADERS: Record< 'divide-and-conquer': () => import('./java/divide-and-conquer?implementation-loader').then(readImplementationLoader), math: () => import('./java/math?implementation-loader').then(readImplementationLoader), + compression: () => + import('./java/compression?implementation-loader').then(readImplementationLoader), }, cpp: { concepts: () => import('./cpp/concepts?implementation-loader').then(readImplementationLoader), @@ -161,6 +171,8 @@ const IMPLEMENTATION_LOADERS: Record< 'divide-and-conquer': () => import('./cpp/divide-and-conquer?implementation-loader').then(readImplementationLoader), math: () => import('./cpp/math?implementation-loader').then(readImplementationLoader), + compression: () => + import('./cpp/compression?implementation-loader').then(readImplementationLoader), }, rust: { concepts: () => import('./rust/concepts?implementation-loader').then(readImplementationLoader), @@ -177,6 +189,8 @@ const IMPLEMENTATION_LOADERS: Record< 'divide-and-conquer': () => import('./rust/divide-and-conquer?implementation-loader').then(readImplementationLoader), math: () => import('./rust/math?implementation-loader').then(readImplementationLoader), + compression: () => + import('./rust/compression?implementation-loader').then(readImplementationLoader), }, } diff --git a/src/lib/algorithms/python/compression.ts b/src/lib/algorithms/python/compression.ts new file mode 100644 index 0000000..0b71338 --- /dev/null +++ b/src/lib/algorithms/python/compression.ts @@ -0,0 +1,44 @@ +import type { CodeImplementation } from '@lib/types' +import { annotated } from '@lib/code-languages' + +export const compressionPython: Record = { + 'huffman-coding': annotated(`from dataclasses import dataclass + + +@dataclass +class Node: + char: str | None + freq: int + left: "Node | None" = None + right: "Node | None" = None + + +def huffman_coding(text): + freq = {} #@3 + for char in text: + freq[char] = freq.get(char, 0) + 1 #@5 + + queue = [Node(char, count) for char, count in freq.items()] #@11 + while len(queue) > 1: + queue.sort(key=lambda node: node.freq) + left = queue.pop(0) #@18 + right = queue.pop(0) #@19 + queue.append(Node(None, left.freq + right.freq, left, right)) #@20 + root = queue[0] + + codes = {} #@26 + + def assign(node, code): + if node.left is None and node.right is None: #@28 + codes[node.char] = code or "0" + return + assign(node.left, code + "0") + assign(node.right, code + "1") + + assign(root, "") + encoded = "".join(codes[char] for char in text) #@38 + return codes, encoded + + +huffman_coding("ABRACADABRA")`), +} diff --git a/src/lib/algorithms/python/index.ts b/src/lib/algorithms/python/index.ts index db71623..0c590f6 100644 --- a/src/lib/algorithms/python/index.ts +++ b/src/lib/algorithms/python/index.ts @@ -9,6 +9,7 @@ import { dynamicProgrammingPython } from '@lib/algorithms/python/dynamic-program import { backtrackingPython } from '@lib/algorithms/python/backtracking' import { divideAndConquerPython } from '@lib/algorithms/python/divide-and-conquer' import { mathPython } from '@lib/algorithms/python/math' +import { compressionPython } from '@lib/algorithms/python/compression' /** Python translations keyed by algorithm id. */ export const pythonImplementations: Record = { @@ -21,4 +22,5 @@ export const pythonImplementations: Record = { ...backtrackingPython, ...divideAndConquerPython, ...mathPython, + ...compressionPython, } diff --git a/src/lib/algorithms/rust/compression.ts b/src/lib/algorithms/rust/compression.ts new file mode 100644 index 0000000..2558282 --- /dev/null +++ b/src/lib/algorithms/rust/compression.ts @@ -0,0 +1,68 @@ +import type { CodeImplementation } from '@lib/types' +import { annotated } from '@lib/code-languages' + +export const compressionRust: Record = { + 'huffman-coding': annotated(`use std::cmp::Reverse; +use std::collections::{BinaryHeap, HashMap}; + +#[derive(Eq, Ord, PartialEq, PartialOrd)] +struct Node { + freq: usize, + ch: Option, + left: Option>, + right: Option>, +} + +impl Node { + fn leaf(ch: char, freq: usize) -> Self { + Self { freq, ch: Some(ch), left: None, right: None } + } + + fn parent(left: Node, right: Node) -> Self { + Self { + freq: left.freq + right.freq, + ch: None, + left: Some(Box::new(left)), + right: Some(Box::new(right)), + } + } +} + +fn huffman_coding(text: &str) -> (HashMap, String) { + let mut freq = HashMap::new(); //@3 + for ch in text.chars() { + *freq.entry(ch).or_insert(0) += 1; //@5 + } + + let mut queue: BinaryHeap> = freq //@11 + .iter() + .map(|(&ch, &count)| Reverse(Node::leaf(ch, count))) + .collect(); + + while queue.len() > 1 { + let left = queue.pop().unwrap().0; //@18 + let right = queue.pop().unwrap().0; //@19 + queue.push(Reverse(Node::parent(left, right))); //@20 + } + let root = queue.pop().unwrap().0; + + let mut codes = HashMap::new(); //@26 + assign_codes(&root, String::new(), &mut codes); + let mut encoded = String::new(); //@38 + for ch in text.chars() { + encoded.push_str(&codes[&ch]); + } + (codes, encoded) +} + +fn assign_codes(node: &Node, code: String, codes: &mut HashMap) { + if node.left.is_none() && node.right.is_none() { //@28 + codes.insert(node.ch.unwrap(), if code.is_empty() { "0".into() } else { code }); + return; + } + assign_codes(node.left.as_ref().unwrap(), format!("{code}0"), codes); + assign_codes(node.right.as_ref().unwrap(), format!("{code}1"), codes); +} + +huffman_coding("ABRACADABRA");`), +} diff --git a/src/lib/algorithms/rust/index.ts b/src/lib/algorithms/rust/index.ts index afed8c7..e2e5088 100644 --- a/src/lib/algorithms/rust/index.ts +++ b/src/lib/algorithms/rust/index.ts @@ -9,6 +9,7 @@ import { dynamicProgrammingRust } from '@lib/algorithms/rust/dynamic-programming import { backtrackingRust } from '@lib/algorithms/rust/backtracking' import { divideAndConquerRust } from '@lib/algorithms/rust/divide-and-conquer' import { mathRust } from '@lib/algorithms/rust/math' +import { compressionRust } from '@lib/algorithms/rust/compression' /** Rust translations keyed by algorithm id. */ export const rustImplementations: Record = { @@ -21,4 +22,5 @@ export const rustImplementations: Record = { ...backtrackingRust, ...divideAndConquerRust, ...mathRust, + ...compressionRust, } diff --git a/src/lib/categories.ts b/src/lib/categories.ts index 95d4247..2d0f6ec 100644 --- a/src/lib/categories.ts +++ b/src/lib/categories.ts @@ -14,6 +14,7 @@ export const categorySlugs: Record = { Backtracking: 'backtracking', 'Divide and Conquer': 'divide-and-conquer', Math: 'math', + Compression: 'compression', } const slugToCategory = Object.fromEntries( @@ -64,6 +65,8 @@ export function getCategoryIntro(locale: Locale, categoryName: string): string { 'Divide and Conquer': 'Divide el problema, resuelve subproblemas y combina resultados. Torre de Hanoi y más.', Math: 'Algoritmos matemáticos clásicos con visualización clara del proceso.', + Compression: + 'Algoritmos de compresión sin pérdida que reducen datos mediante representaciones eficientes.', } return intros[categoryName] ?? `Explora visualizaciones interactivas de ${label.toLowerCase()}.` } @@ -85,6 +88,8 @@ export function getCategoryIntro(locale: Locale, categoryName: string): string { 'Divide and Conquer': 'Split the problem, solve subproblems, combine results. Tower of Hanoi and more.', Math: 'Classic mathematical algorithms with a clear step-by-step visualization.', + Compression: + 'Lossless compression algorithms that reduce data through efficient representations.', } return intros[categoryName] ?? `Explore interactive ${label.toLowerCase()} visualizations.` } diff --git a/src/lib/types.ts b/src/lib/types.ts index f3f0bf1..0a2dec7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -194,6 +194,33 @@ export interface BucketsState { operation?: string } +// ── Compression visualization types ── + +export interface HuffmanState { + type: 'huffman' + phase: 'frequency' | 'build' | 'encode' | 'done' + text: string + highlightChar?: string | null + nodes: Record< + number, + { id: number; char: string | null; freq: number; left: number | null; right: number | null } + > + queue: number[] + nodeStates?: Record + freqTable?: { char: string; freq: number; active?: boolean }[] + codes?: { char: string; code: string; freq: number; active?: boolean }[] + activeCode?: string | null + summary?: { + uniqueChars: number + originalBits: number + compressedBits: number + avgBits: number + savingPct: number + encoded: string + } + operation?: string +} + export type ConceptState = | BigOState | CallStackState @@ -206,6 +233,7 @@ export type ConceptState = | MemoTableState | CoinChangeState | BucketsState + | HuffmanState export interface Step { array?: number[] diff --git a/src/lib/visualizers/concept/huffman.ts b/src/lib/visualizers/concept/huffman.ts new file mode 100644 index 0000000..78ff09d --- /dev/null +++ b/src/lib/visualizers/concept/huffman.ts @@ -0,0 +1,488 @@ +/** + * Concept visualizer: Huffman coding. + */ +import type { HuffmanState } from '@lib/types' +import { applyStyles, svgEl } from '@lib/visualizers/concept/dom' + +type HuffmanNode = HuffmanState['nodes'][number] +type NodeState = NonNullable[number] + +const HUFFMAN_COLORS: Record = { + normal: { fill: 'rgba(96,165,250,0.12)', stroke: 'rgba(96,165,250,0.35)', text: '#60a5fa' }, + merging: { fill: 'rgba(251,146,60,0.18)', stroke: 'rgba(251,146,60,0.5)', text: '#fb923c' }, + new: { fill: 'rgba(74,222,128,0.18)', stroke: 'rgba(74,222,128,0.5)', text: '#4ade80' }, + path: { fill: 'rgba(250,204,21,0.15)', stroke: 'rgba(250,204,21,0.45)', text: '#facc15' }, + leafFound: { + fill: 'rgba(250,204,21,0.24)', + stroke: 'rgba(250,204,21,0.6)', + text: '#fde047', + }, +} + +function textElement( + tag: K, + className: string, + text: string, +): HTMLElementTagNameMap[K] { + const element = document.createElement(tag) + element.className = className + element.textContent = text + return element +} + +function appendCell( + row: HTMLTableRowElement, + className: string, + content: string | Node, +): HTMLTableCellElement { + const cell = document.createElement('td') + cell.className = className + cell.append(content) + row.append(cell) + return cell +} + +function renderInputText(text: string, highlightChar: string | null | undefined): HTMLElement { + const strip = document.createElement('div') + strip.className = 'flex flex-wrap justify-center gap-1 max-w-2xl' + + for (const char of text) { + const active = highlightChar != null && char === highlightChar + const cell = textElement( + 'div', + 'w-7 h-9 rounded-md border flex items-center justify-center font-mono text-sm font-bold', + char, + ) + applyStyles(cell, { + backgroundColor: active ? 'rgba(251,146,60,0.18)' : 'var(--subtle)', + borderColor: active ? 'rgba(251,146,60,0.5)' : 'var(--viz-border)', + color: active ? '#fb923c' : 'var(--viz-muted)', + boxShadow: active ? '0 0 10px rgba(251,146,60,0.25)' : 'none', + }) + strip.append(cell) + } + + return strip +} + +function renderTree( + nodes: HuffmanState['nodes'], + roots: number[], + nodeStates: Record, +): SVGSVGElement { + const positions: Record = {} + let column = 0 + let maxDepth = 0 + + const place = (id: number, depth: number): number => { + const node = nodes[id] + if (!node) return column + maxDepth = Math.max(maxDepth, depth) + + const childColumns: number[] = [] + if (node.left != null) childColumns.push(place(node.left, depth + 1)) + if (node.right != null) childColumns.push(place(node.right, depth + 1)) + + const currentColumn = + childColumns.length > 0 + ? childColumns.reduce((sum, childColumn) => sum + childColumn, 0) / childColumns.length + : column++ + positions[id] = { column: currentColumn, depth } + return currentColumn + } + + for (const root of roots) { + place(root, 0) + column += 0.7 + } + + const width = 620 + const horizontalPadding = 36 + const radius = 17 + const top = 26 + const levelHeight = 62 + const maxColumn = Math.max(0, ...Object.values(positions).map((position) => position.column)) + const height = top + maxDepth * levelHeight + radius + 22 + const xOf = (value: number) => + maxColumn === 0 + ? width / 2 + : horizontalPadding + (value / maxColumn) * (width - 2 * horizontalPadding) + const yOf = (depth: number) => top + depth * levelHeight + const stateOf = (id: number): NodeState => nodeStates[id] ?? 'normal' + const isOnPath = (id: number) => { + const state = stateOf(id) + return state === 'path' || state === 'leafFound' + } + + const svg = svgEl('svg', { + viewBox: `0 0 ${width} ${height}`, + role: 'img', + ariaLabel: 'Huffman tree', + }) + svg.classList.add('w-full', 'max-w-2xl') + applyStyles(svg, { maxHeight: `${Math.max(height, 90)}px` }) + + for (const [key, node] of Object.entries(nodes)) { + const id = Number(key) + const parentPosition = positions[id] + if (!parentPosition) continue + + const children: [number | null, string][] = [ + [node.left, '0'], + [node.right, '1'], + ] + for (const [childId, bit] of children) { + if (childId == null) continue + const childPosition = positions[childId] + if (!childPosition) continue + + const x1 = xOf(parentPosition.column) + const y1 = yOf(parentPosition.depth) + const x2 = xOf(childPosition.column) + const y2 = yOf(childPosition.depth) + const highlighted = isOnPath(id) && isOnPath(childId) + const group = svgEl('g') + const line = svgEl('line', { + x1, + y1, + x2, + y2, + stroke: highlighted ? 'rgba(250,204,21,0.6)' : 'var(--viz-border)', + strokeWidth: highlighted ? 2 : 1.5, + }) + + const label = svgEl('text', { + x: x1 + (x2 - x1) * 0.42, + y: y1 + (y2 - y1) * 0.42 - 3, + textAnchor: 'middle', + fontSize: 11, + fontFamily: 'monospace', + fontWeight: 'bold', + fill: highlighted ? '#facc15' : 'var(--viz-muted)', + }) + label.textContent = bit + group.append(line, label) + svg.append(group) + } + } + + for (const [key, node] of Object.entries(nodes)) { + const id = Number(key) + const position = positions[id] + if (!position) continue + + const x = xOf(position.column) + const y = yOf(position.depth) + const state = stateOf(id) + const colors = HUFFMAN_COLORS[state] + const isLeaf = node.left == null && node.right == null + const group = svgEl('g') + + if (state !== 'normal') { + group.append( + svgEl('circle', { + cx: x, + cy: y, + r: radius + 4, + fill: colors.stroke, + opacity: 0.18, + }), + ) + } + + group.append( + svgEl('circle', { + cx: x, + cy: y, + r: radius, + fill: colors.fill, + stroke: colors.stroke, + strokeWidth: 1.5, + }), + ) + + const frequency = svgEl('text', { + x, + y: y + 1, + textAnchor: 'middle', + dominantBaseline: 'central', + fill: colors.text, + fontSize: 13, + fontFamily: 'monospace', + fontWeight: 'bold', + }) + frequency.textContent = String(node.freq) + group.append(frequency) + + if (isLeaf && node.char != null) { + const character = svgEl('text', { + x, + y: y + radius + 13, + textAnchor: 'middle', + fill: 'var(--viz-label)', + fontSize: 12, + fontFamily: 'monospace', + fontWeight: 'bold', + }) + character.textContent = `'${node.char}'` + group.append(character) + } + + svg.append(group) + } + + return svg +} + +function renderNodeTable( + nodes: HuffmanState['nodes'], + roots: number[], + nodeStates: Record, + codes: NonNullable, +): HTMLElement { + const tableIds = Object.keys(nodes) + .map(Number) + .sort((a, b) => a - b) + const codeByChar = Object.fromEntries(codes.map(({ char, code }) => [char, code])) + const isFinalRoot = (id: number) => + roots.length === 1 && roots[0] === id && (nodes[id].left != null || nodes[id].right != null) + + const statusInfo = (id: number) => { + const state = nodeStates[id] ?? 'normal' + const node = nodes[id] + const isLeaf = node.left == null && node.right == null + if (state === 'merging') { + return { + label: 'merging', + color: HUFFMAN_COLORS.merging.text, + background: 'rgba(251,146,60,0.09)', + } + } + if (state === 'new') { + return { label: 'new', color: HUFFMAN_COLORS.new.text, background: 'rgba(74,222,128,0.09)' } + } + if (state === 'leafFound') { + return { + label: 'coded', + color: HUFFMAN_COLORS.leafFound.text, + background: 'rgba(250,204,21,0.12)', + } + } + if (state === 'path') { + return { + label: 'on path', + color: HUFFMAN_COLORS.path.text, + background: 'rgba(250,204,21,0.06)', + } + } + if (isFinalRoot(id)) return { label: 'root', color: '#a5b4fc', background: 'transparent' } + if (isLeaf) return { label: 'leaf', color: '#60a5fa', background: 'transparent' } + return { label: 'node', color: 'var(--viz-muted)', background: 'transparent' } + } + + const wrapper = document.createElement('div') + wrapper.className = 'flex flex-col items-center gap-1.5 w-full max-w-xl overflow-x-auto' + wrapper.append( + textElement( + 'div', + 'text-[10px] font-mono text-neutral-500 uppercase tracking-wider', + 'Node table · left = 0, right = 1', + ), + ) + + const table = document.createElement('table') + table.className = 'font-mono text-xs border-collapse' + const head = document.createElement('thead') + const headRow = document.createElement('tr') + headRow.className = 'text-neutral-500 text-[10px] uppercase tracking-wider' + const headers: [string, string][] = [ + ['#', 'text-right'], + ['Status', 'text-left'], + ['Char', 'text-center'], + ['Freq', 'text-right'], + ['Code', 'text-center'], + ['Pointer', 'text-center'], + ] + for (const [label, alignment] of headers) { + const header = document.createElement('th') + header.className = `px-2.5 py-1 ${alignment} font-medium` + header.textContent = label + headRow.append(header) + } + head.append(headRow) + table.append(head) + + const body = document.createElement('tbody') + for (const id of tableIds) { + const node: HuffmanNode = nodes[id] + const status = statusInfo(id) + const isLeaf = node.left == null && node.right == null + const row = document.createElement('tr') + row.className = 'border-t border-white/6' + applyStyles(row, { backgroundColor: status.background }) + + appendCell(row, 'px-2.5 py-1 text-right text-neutral-500', String(id)) + + const statusContent = document.createElement('span') + statusContent.className = 'inline-flex items-center gap-1.5' + applyStyles(statusContent, { color: status.color }) + const dot = document.createElement('span') + dot.className = 'w-1.5 h-1.5 rounded-full shrink-0' + applyStyles(dot, { backgroundColor: status.color }) + statusContent.append(dot, status.label) + appendCell(row, 'px-2.5 py-1', statusContent) + + appendCell( + row, + 'px-2.5 py-1 text-center font-bold text-neutral-200', + node.char != null ? `'${node.char}'` : '—', + ) + appendCell(row, 'px-2.5 py-1 text-right text-neutral-300', String(node.freq)) + appendCell( + row, + 'px-2.5 py-1 text-center tracking-wider text-amber-300', + node.char != null ? (codeByChar[node.char] ?? '—') : '—', + ) + + const pointer = document.createElement('span') + if (isLeaf) { + pointer.className = 'text-neutral-600' + pointer.textContent = '—' + } else { + pointer.className = 'inline-flex gap-2' + const left = textElement('span', 'text-neutral-500', '0→') + left.append(textElement('span', 'text-neutral-300', String(node.left))) + const right = textElement('span', 'text-neutral-500', '1→') + right.append(textElement('span', 'text-neutral-300', String(node.right))) + pointer.append(left, right) + } + appendCell(row, 'px-2.5 py-1 text-center text-neutral-400 whitespace-nowrap', pointer) + body.append(row) + } + table.append(body) + wrapper.append(table) + return wrapper +} + +function renderSummary(summary: NonNullable): HTMLElement { + const wrapper = document.createElement('div') + wrapper.className = 'flex flex-col items-center gap-3 mt-1 w-full max-w-md' + + const comparison = document.createElement('div') + comparison.className = 'flex items-stretch gap-3 w-full' + const ascii = document.createElement('div') + ascii.className = + 'flex-1 flex flex-col items-center px-3 py-2 rounded-lg border border-white/10 bg-white/3' + ascii.append( + textElement( + 'span', + 'text-[9px] font-mono text-neutral-500 uppercase tracking-wider', + 'ASCII · 8-bit', + ), + textElement( + 'span', + 'font-mono text-lg font-bold text-neutral-400', + String(summary.originalBits), + ), + textElement('span', 'text-[9px] font-mono text-neutral-600', 'bits'), + ) + + const arrow = textElement('div', 'flex items-center text-neutral-600 font-mono text-sm', '→') + const huffman = document.createElement('div') + huffman.className = + 'flex-1 flex flex-col items-center px-3 py-2 rounded-lg border border-green-400/30 bg-green-400/8' + huffman.append( + textElement( + 'span', + 'text-[9px] font-mono text-green-500/80 uppercase tracking-wider', + 'Huffman', + ), + textElement( + 'span', + 'font-mono text-lg font-bold text-green-400', + String(summary.compressedBits), + ), + textElement('span', 'text-[9px] font-mono text-green-600/70', 'bits'), + ) + comparison.append(ascii, arrow, huffman) + + const ratio = document.createElement('div') + ratio.className = 'w-full' + const track = document.createElement('div') + track.className = 'h-2.5 w-full rounded-full bg-white/5 overflow-hidden border border-white/8' + const bar = document.createElement('div') + bar.className = 'h-full rounded-full bg-green-400/60' + applyStyles(bar, { + width: `${Math.max(4, (summary.compressedBits / summary.originalBits) * 100)}%`, + }) + track.append(bar) + const ratioLabels = document.createElement('div') + ratioLabels.className = 'flex justify-between mt-1 text-[10px] font-mono text-neutral-500' + ratioLabels.append( + textElement('span', '', `~${summary.avgBits.toFixed(2)} bits/char`), + textElement('span', 'text-green-400 font-bold', `${summary.savingPct}% smaller`), + ) + ratio.append(track, ratioLabels) + + const encoded = document.createElement('div') + encoded.className = 'w-full text-center' + encoded.append( + textElement( + 'span', + 'text-[10px] font-mono text-neutral-500 uppercase tracking-wider', + 'encoded', + ), + textElement( + 'div', + 'font-mono text-[11px] text-amber-300/90 break-all leading-relaxed mt-0.5', + summary.encoded, + ), + ) + + wrapper.append(comparison, ratio, encoded) + return wrapper +} + +export function renderHuffman(state: HuffmanState): HTMLElement { + const { + nodes, + queue, + nodeStates = {}, + codes = [], + text, + highlightChar, + summary, + operation, + phase, + } = state + const roots = queue.filter((id) => nodes[id] != null) + + const wrapper = document.createElement('div') + wrapper.className = + 'flex-1 flex flex-col items-center justify-center gap-4 w-full py-2 scale-90 md:scale-100' + wrapper.append( + textElement( + 'div', + 'text-neutral-500 font-mono text-[11px] uppercase tracking-widest', + 'Huffman Coding', + ), + ) + + if (operation) { + wrapper.append( + textElement( + 'div', + 'font-mono text-xs px-3 py-1 rounded-full bg-white/5 border border-white/10 text-neutral-300', + operation, + ), + ) + } + if (phase === 'frequency') wrapper.append(renderInputText(text, highlightChar)) + if (roots.length > 0) wrapper.append(renderTree(nodes, roots, nodeStates)) + if (Object.keys(nodes).length > 0) { + wrapper.append(renderNodeTable(nodes, roots, nodeStates, codes)) + } + if (summary) wrapper.append(renderSummary(summary)) + + return wrapper +} diff --git a/src/lib/visualizers/concept/index.ts b/src/lib/visualizers/concept/index.ts index 9b9471b..7cab4f1 100644 --- a/src/lib/visualizers/concept/index.ts +++ b/src/lib/visualizers/concept/index.ts @@ -15,6 +15,7 @@ export type ConceptType = | 'memoTable' | 'coinChange' | 'buckets' + | 'huffman' type ConceptRenderer = (state: never) => HTMLElement @@ -52,6 +53,8 @@ const loaders: Record Promise> = { ), buckets: () => import('@lib/visualizers/concept/buckets').then((m) => m.renderBuckets as ConceptRenderer), + huffman: () => + import('@lib/visualizers/concept/huffman').then((m) => m.renderHuffman as ConceptRenderer), } const cache = new Map()