Trie para autocompletado en JavaScript

Implementación de la estructura de datos Trie en JavaScript con insert, search, startsWith y autocomplete. Demo interactiva en el navegador: campo de texto con sugerencias en tiempo real sobre un vocabulario de palabras en español. Highlight de la parte escrita y máximo de 8 sugerencias.
				<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trie — Autocompletado en JavaScript</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #1e1e2e;
    color: #cdd6f4;
    font-family: 'Segoe UI', system-ui, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    padding: 40px 20px;
  }
  h1 { font-size: 1.8rem; margin-bottom: 6px; color: #cba6f7; }
  .subtitle { font-size: 0.85rem; color: #6c7086; margin-bottom: 30px; }
  .search-wrap { position: relative; width: 100%; max-width: 420px; }
  #input {
    width: 100%;
    padding: 14px 18px;
    background: #313244;
    border: 2px solid #45475a;
    border-radius: 10px;
    color: #cdd6f4;
    font-size: 1.1rem;
    outline: none;
    transition: border-color 0.2s;
  }
  #input:focus { border-color: #cba6f7; }
  #suggestions {
    position: absolute;
    top: calc(100% + 6px);
    left: 0; right: 0;
    background: #313244;
    border: 1px solid #45475a;
    border-radius: 10px;
    overflow: hidden;
    display: none;
    box-shadow: 0 8px 24px rgba(0,0,0,0.4);
    z-index: 10;
  }
  #suggestions.visible { display: block; }
  .suggestion {
    padding: 12px 18px;
    cursor: pointer;
    font-size: 1rem;
    border-bottom: 1px solid #45475a;
    transition: background 0.1s;
  }
  .suggestion:last-child { border-bottom: none; }
  .suggestion:hover, .suggestion.active { background: #45475a; }
  .suggestion mark {
    background: none;
    color: #cba6f7;
    font-weight: bold;
  }
  .stats {
    margin-top: 14px;
    font-size: 0.78rem;
    color: #6c7086;
  }
  /* Panel de visualización del Trie */
  #code-section {
    margin-top: 50px;
    width: 100%;
    max-width: 700px;
    background: #181825;
    border-radius: 12px;
    padding: 24px;
  }
  #code-section h2 { font-size: 1rem; color: #89b4fa; margin-bottom: 16px; }
  pre {
    font-family: 'Consolas', 'Fira Code', monospace;
    font-size: 0.82rem;
    color: #a6e3a1;
    white-space: pre-wrap;
    line-height: 1.6;
    overflow: auto;
  }
</style>
</head>
<body>
<h1>Trie · Autocompletado</h1>
<p class="subtitle">Implementación en JavaScript — empieza a escribir</p>

<div class="search-wrap">
  <input id="input" type="text" placeholder="Escribe una palabra..." autocomplete="off" spellcheck="false">
  <div id="suggestions"></div>
</div>
<div class="stats" id="stats"></div>

<section id="code-section">
  <h2>Implementación del Trie</h2>
  <pre id="code-display"></pre>
</section>

<script>
// ???????????????????????????????????????????????????????????
// Implementación del Trie
// ???????????????????????????????????????????????????????????

class TrieNode {
  constructor() {
    this.children = {};   // char ? TrieNode
    this.isEnd    = false; // true si es final de palabra
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
    this.wordCount = 0;
  }

  // Insertar una palabra
  insert(word) {
    word = word.toLowerCase();
    let node = this.root;
    for (const ch of word) {
      if (!node.children[ch]) {
        node.children[ch] = new TrieNode();
      }
      node = node.children[ch];
    }
    if (!node.isEnd) {
      node.isEnd = true;
      this.wordCount++;
    }
  }

  // Buscar si la palabra exacta existe
  search(word) {
    const node = this._findNode(word.toLowerCase());
    return node !== null && node.isEnd;
  }

  // ¿Existe alguna palabra que empiece por prefix?
  startsWith(prefix) {
    return this._findNode(prefix.toLowerCase()) !== null;
  }

  // Devuelve hasta `limit` palabras que empiecen por prefix
  autocomplete(prefix, limit = 8) {
    prefix = prefix.toLowerCase();
    const node = this._findNode(prefix);
    if (!node) return [];
    const results = [];
    this._dfs(node, prefix, results, limit);
    return results;
  }

  // Navegar hasta el nodo del último carácter del prefix
  _findNode(prefix) {
    let node = this.root;
    for (const ch of prefix) {
      if (!node.children[ch]) return null;
      node = node.children[ch];
    }
    return node;
  }

  // DFS desde un nodo acumulando palabras completas
  _dfs(node, current, results, limit) {
    if (results.length >= limit) return;
    if (node.isEnd) results.push(current);
    // Ordenar hijos para sugerencias alfabéticas
    for (const ch of Object.keys(node.children).sort()) {
      if (results.length >= limit) break;
      this._dfs(node.children[ch], current + ch, results, limit);
    }
  }
}

// ???????????????????????????????????????????????????????????
// Diccionario (~100 palabras en español)
// ???????????????????????????????????????????????????????????

const WORDS = [
  'abeja','abierto','abogado','abrazo','absoluto','abstracto','academia','acción',
  'acento','acero','algoritmo','almacén','ambiente','amigo','amor','análisis',
  'aplicación','archivo','arena','arte','base','biblioteca','bucle','caché',
  'clase','clave','código','compilar','complejidad','comunicación','condición',
  'consola','contenedor','contexto','control','cursor','datos','debug','decisión',
  'definición','depuración','desarrollo','diccionario','diseño','docker','dominio',
  'editor','elemento','enlace','entrada','entorno','error','estructura','evento',
  'excepción','expresión','fichero','flujo','framework','función','generador',
  'grafo','herencia','herramienta','imagen','implementar','índice','instancia',
  'iteración','javascript','kernel','lenguaje','librería','lista','llamada',
  'mapa','memoria','método','módulo','nodo','objeto','operador','parámetro',
  'patrón','pila','polimorfismo','proceso','programa','protocolo','proyecto',
  'python','queue','recursión','red','referencia','registro','rendimiento',
  'repositorio','servidor','sistema','stack','string','tabla','tipo','token',
  'variable','vector','versión','función','interfaz','módulo','clase','objeto'
];

const trie = new Trie();
WORDS.forEach(w => trie.insert(w));

// ???????????????????????????????????????????????????????????
// UI
// ???????????????????????????????????????????????????????????

const input       = document.getElementById('input');
const suggestBox  = document.getElementById('suggestions');
const statsEl     = document.getElementById('stats');
let activeIndex   = -1;
let currentSugs   = [];

function highlight(word, prefix) {
  if (!prefix) return word;
  const idx = word.toLowerCase().indexOf(prefix.toLowerCase());
  if (idx === -1) return word;
  return word.slice(0, idx)
    + '<mark>' + word.slice(idx, idx + prefix.length) + '</mark>'
    + word.slice(idx + prefix.length);
}

function showSuggestions(prefix) {
  const t0 = performance.now();
  currentSugs = trie.autocomplete(prefix, 8);
  const elapsed = (performance.now() - t0).toFixed(3);

  statsEl.textContent = prefix
    ? `${currentSugs.length} sugerencia(s) para "${prefix}" · búsqueda en ${elapsed}ms · ${trie.wordCount} palabras indexadas`
    : '';

  if (!prefix || !currentSugs.length) {
    suggestBox.classList.remove('visible');
    return;
  }

  suggestBox.innerHTML = '';
  activeIndex = -1;
  currentSugs.forEach((word, i) => {
    const div = document.createElement('div');
    div.className = 'suggestion';
    div.innerHTML = highlight(word, prefix);
    div.addEventListener('mousedown', e => {
      e.preventDefault();
      input.value = word;
      suggestBox.classList.remove('visible');
      statsEl.textContent = `Seleccionado: "${word}"`;
    });
    div.addEventListener('mouseover', () => {
      setActive(i);
    });
    suggestBox.appendChild(div);
  });
  suggestBox.classList.add('visible');
}

function setActive(idx) {
  const items = suggestBox.querySelectorAll('.suggestion');
  items.forEach(el => el.classList.remove('active'));
  if (idx >= 0 && idx < items.length) {
    items[idx].classList.add('active');
    activeIndex = idx;
  }
}

input.addEventListener('input', () => showSuggestions(input.value.trim()));
input.addEventListener('blur', () => setTimeout(() => suggestBox.classList.remove('visible'), 150));
input.addEventListener('focus', () => { if (input.value) showSuggestions(input.value.trim()); });

input.addEventListener('keydown', e => {
  if (!suggestBox.classList.contains('visible')) return;
  if (e.key === 'ArrowDown') {
    e.preventDefault();
    setActive(Math.min(activeIndex + 1, currentSugs.length - 1));
  } else if (e.key === 'ArrowUp') {
    e.preventDefault();
    setActive(Math.max(activeIndex - 1, 0));
  } else if (e.key === 'Enter' && activeIndex >= 0) {
    e.preventDefault();
    input.value = currentSugs[activeIndex];
    suggestBox.classList.remove('visible');
    statsEl.textContent = `Seleccionado: "${input.value}"`;
  } else if (e.key === 'Escape') {
    suggestBox.classList.remove('visible');
  }
});

// Mostrar el código de la implementación en el panel inferior
document.getElementById('code-display').textContent = `class TrieNode {
  constructor() {
    this.children = {};   // char ? TrieNode
    this.isEnd    = false;
  }
}

class Trie {
  insert(word) { /* O(m) */ }
  search(word) { /* O(m) */ }
  startsWith(prefix) { /* O(m) */ }
  autocomplete(prefix, limit = 8) {
    const node = this._findNode(prefix);
    if (!node) return [];
    const results = [];
    this._dfs(node, prefix, results, limit);
    return results;
  }
  _dfs(node, current, results, limit) {
    if (results.length >= limit) return;
    if (node.isEnd) results.push(current);
    for (const ch of Object.keys(node.children).sort()) {
      this._dfs(node.children[ch], current + ch, results, limit);
    }
  }
}
// m = longitud de la palabra/prefijo
// Complejidad espacial: O(N·L) — N palabras, L longitud media`;
</script>
</body>
</html>

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP