Construye y entiende una hash table en JavaScript desde cero con un enfoque práctico. Verás cómo inicializar la estructura, cómo funciona una hash function creada para la demo y cómo implementar el método set para agregar pares key–value sin perder datos cuando hay colisiones.
¿Qué estructura creamos y cómo se inicializa en JavaScript?
Una hash table es, en esencia, un objeto que usa una hash function para convertir una key en una dirección (address) dentro de un array de tamaño fijo llamado buckets. En el ejemplo, se instancia con 50 buckets para guardar información de forma eficiente.
Se trabaja en DevTools en la sección de console para probar el código.
La clase tiene un constructor que recibe un size y crea un array con ese tamaño.
La hash function usada es arbitraria, pensada solo para la demostración. Se comenta que genera un número aleatorio del 0 al 65535 y se asocia a UTF-16. Su objetivo: devolver un hash que actúe como address donde guardar el valor.
No necesitas implementar tu propia hash function en producción: hay opciones open source en GitHub.
¿Cómo luce la clase base con constructor y hash function?
classHashTable{constructor(size){this.data=newArray(size);// 50 buckets en el ejemplo.}hashMethod(key){// Método arbitrario para la demo: retorna una dirección válida.// Implementación real no es relevante aquí.returnMath.floor(Math.random()*this.data.length);}}
¿Cómo agregar elementos con set y evitar colisiones en buckets?
El primer método esencial es set. Recibe dos parámetros: key y value. Convierte la key en un address usando la hash function, y guarda el par key–value en el bucket correspondiente. Si ya existe información en ese address, ocurre una colisión y se agrega un nuevo par sin sobrescribir lo anterior.
Se calcula address con this.hashMethod(key).
Si no existe this.data[address], se crea un nuevo array para ese bucket.
Si ya existe, se agrega otro [key, value] al mismo bucket.
Se retorna this.data para inspeccionar lo guardado en memoria.
¿Cuál es la lógica exacta del método set?
classHashTable{constructor(size){this.data=newArray(size);}hashMethod(key){// Demo: devuelve una dirección válida.returnMath.floor(Math.random()*this.data.length);}set(key, value){const address =this.hashMethod(key);if(!this.data[address]){this.data[address]=[];}this.data[address].push([key, value]);returnthis.data;// Para ver cómo se guarda en memoria.}}
Colisiones: dos o más pares key–value en un mismo bucket.
Objetivo en colisiones: no sobrescribir datos previos, solo agregar el nuevo par.
Beneficio: se preserva la información y luego se podrá acceder al valor correcto con el método get.
¿Cómo probar en consola y qué observar al insertar datos?
Con 50 buckets, al insertar elementos se verá cómo se ocupan direcciones de forma aleatoria y, a veces, se producen colisiones en un mismo address.
¿Qué pasos seguir para ver el comportamiento en DevTools?
Al crear la instancia se obtiene un array con 50 espacios disponibles.
Al hacer set('Diego', 1990) se ocupa un bucket, por ejemplo, en un índice como 11.
Al hacer set('Mariana', 98) puede ocurrir colisión y verse dos pares en el mismo índice.
Al agregar set('Alejandra', 2000) se usa otro bucket libre, reduciendo el total de espacios disponibles.
En la siguiente implementación se trabajará el método get: al pasar una key, debe regresar su value incluso cuando hubo colisiones en el bucket correspondiente.
¿Tienes dudas sobre el manejo de colisiones o la inicialización de buckets? Comenta tu escenario y lo revisamos juntos.
Implementación de Hash Table en JavaScript: Método Set
Según la definición de Kaspersky:
.
"Una función criptográfica hash- usualmente conocida como “hash”- es un algoritmo matemático que transforma cualquier bloque arbitrario de datos en una nueva serie de caracteres con una longitud fija. Independientemente de la longitud de los datos de entrada, el valor hash de salida tendrá siempre la misma longitud."
.
En pocas palabras, un hash es un string aleatorio que se genera a partir de un string que le pasamos nosotros, este string que se genera tendrá una longitud fija, no importa si el string que nosotros le pasamos es muy largo.
.
El hash se suele usar mucho al momento de encriptar contraseñas, y la forma de calcularlos es mediante un algoritmo matemático 😉
Cuando no el MOD metido en criptografía ya que es la única operación que no tiene inversa como la adición(resta), multiplicación(división) o exponenciación(raiz)…
Compañeros, les comparto este vídeo que explica muy bien lo que es un hash y con ejemplos para no dejar dudas. Espero lo disfruten.
https://www.youtube.com/watch?v=GctssJjiqG4
estuve mirando un video que dice que el Hash Table es el que se utililiza para detectar canciones solo con escucharlas. Interpreta las ondas en codigo binario y de esa manera las encuentra las canciones. increible ¿no?
const address = this.hashMethod(key); creamos una direccion random con el hash method.
if(!this.data[address]) Esto es una de las cosas mas importantes, lo que compruebo con esto es asi: (!this.data[address]). BIEN, esto es asi:
voy a traer el array = this.data, y voy a comprobar si hay un addres(una posicion) libre,por ahora el array se ve asi posicion:[ ] Entonces, posicion = address. digo,che, ya hay un address con este valor? si no lo hay, agragame al array una clave valor, asi todo queda asi: address[key,value] ahora, si justo dio la casualidad de que esta direccion(address) este ocupada, hagamos que el array se vea asi: addres[ [key,value],[key,value] ] esto hace referncia a una colision. Es cecir, en el codigo, esta es la dolicion: this.data[address].push([key,value]); La colicion surge siempre y cuando, ya el array que la direccion(address) por la funcion hash, me dijo que ponga mi data ,YA ESTABA OCUPADO.
Explicacion mas clara de como funciona el if(!this.data[address]). Para entender esto hay que entender que this.data, es lo mismo que decir const array = []. y que el address que se me da no es un dato dentro de el array, el address que se me da, es una POSICION/DIRECCION para encontrar un array con o sin datos. por lo tanto, cuanto hacemos esto this.data[address], lo que hacemos en pasos sencillos es esto: const array = [ ['celular'],['pc'],['balon']]. this.data[address] es literalmente lo mismo que decir: array[0] esta ocupado?Si o no? si no lo esta,ocupalo, si lo esta, hacele un push y mete otro dato.
Esta estructura de datos es perfecta para resolver problemas que tengan que ver con búsquedas.
Las hash tienen distintos métodos, entre ellos están
insert: que inserta un elemento en la tabla.
search: buscar un elemento por key
delete: borra un elemento
El problema de las hash
La colicion: en ocaciones puede ocurrir que cuando pasemos por la hash function un key distinto a todos los key anteriores, nos puede retornar un hash repetido. Eso hace que tengamos dos elementos guardados con el mismo hash, y como te estas imagianando eso es un problema bastante feo y adivina, no hay manera de evitar esto.
Pero la colision se puede tratar, y la manera de hacerlo es curiosamente con otra estructura de datos, las linked list (listas enlazada)
Digamos que tenemos una objeto con un millon de {clave, valor} donde las claves son de tipo string, por ejemplo una dirección de correo.
Entiendo que hacer la búsqueda comparando cada string sería lento, al aplicar una hash table lo que hacemos es tener un algoritmo que convierte ese string en un número quedando {hashNumeroGenerado, clave, valor}, así cuando queramos buscar esa clave primero la convertimos en número con el algoritmo y luego buscamos ese número, que entiendo sería mucho más rápido comparar números que comparar strings y esa sería la ganancia de rendimiento de aplicar hashtables. luego si tenemos una colisión entonces comparamos los strings pero ya dentro de una cantidad mucho mas pequeña de opciones.
¡Exacto! Has comprendido bien cómo funciona una tabla hash para mejorar la búsqueda de elementos. Una tabla hash es una estructura de datos que utiliza una función hash para asignar una clave única a cada elemento que se va a almacenar en la tabla.
La función hash convierte la clave de tipo string en un número entero, que se utiliza como índice para acceder a una ubicación específica en la tabla hash. Debido a que la función hash debe ser única para cada clave, el proceso de búsqueda en una tabla hash es mucho más rápido que en otras estructuras de datos, ya que se evita la necesidad de comparar cadenas de caracteres largas.
Sin embargo, en algunas ocasiones, es posible que dos claves diferentes generen el mismo número hash (lo que se llama colisión). En este caso, se debe utilizar una técnica de resolución de colisiones, como la lista enlazada o el re-hash, para manejar esta situación.
En resumen, la utilización de una tabla hash puede mejorar significativamente el rendimiento de las operaciones de búsqueda en grandes conjuntos de datos con claves de tipo string.
En qué momento se asignó el value a una posición nueva? Entiendo que si ya existía algo en la posición address con push([key,value]) se crea un nuevo array y se le asignan esos dos valores, pero en la parte de crear un array nuevo en la posición adress, cuando no había nada, no veo como se asigna el Key y el Value de manera explícita, solo se utiliza this.data=[address]=[].
Hola chicos, me podrían explicar que es lo que hace exactamente cuando pone " this.data[address] " ???
Es que me confunde un poco :(, y si hay otras formas de hacerla, cuales? gracias
this.data es un arreglo, eso lo podemos ver en el constructor de la clase. Por otro lado this.data[address] nos permite saber si ese espacio del array esta ocupado o no.
.
Hay que tener en cuenta que address es un numero del 0 al 49. En otras palabras es el index. Es similar a hacer array[3], en donde buscamos si hay algo en la posición 3 de nuestro array. En esencia es eso
No deberiamos en el metodo set buscar si la key ya estaba en el array de colisiones y remplazar ese indice?
En caso contrario si, por ejemplo, llamo el método set 50 con la key "Diego" tendriamos en la posición del hash de "Diego" 50 elementos. Yo se que podríamos en este escenario sencillamente escoger la ultima ocurrencia de la llave, pero seria bastante menos eficiente, no?
¿los Hash table tienen alguna ventaja con respecto a los objetos? porque en los objetos también puedes almacenar información y acceder directamente a través de la clave, puedes crear un objeto que dentro almacene las claves-valor a los que quieres acceder, como un JSON por ejemplo.
Si tiene una ventaja significativa y es que imagina que quieres comparar dos archivos de texto. Imagina unos archivos extensión .log o cosas así llenos de mucha información. Cada que queremos hacer una comparación, no seria buena practica comparar "archivo1.log === archivo2.log", ya que el computo seria demasiado. Por esa razón mejor creamos un hash y lo comparamos.
.
En general los hashes y las Hash Tables se usan en backend y algunas otras aplicaciones mas complejas...
En este POST encontraras la respuesta, pero lo que comenta es que:
Cuando usarlas
Almacenamiento en cache supongamos que tenemos 45,000 estudiantes en una universidad y en algún proceso necesita conservar los registros de todos ellos. Normalmente un estudiante tiene un identificador (ID), entonces guardamos el ID y los valores deseados, para posteriormente encontrarlo de una manera eficiente.
También son útiles para almacenar relaciones entre datos
Cuando no usar una Hash Table
Cuando quieras iterar
Cuando quieras ordenar los datos
Cuando tengas que obtener la clava a través del valor, el post recomienda crear otra Hash Table para ello.
Existen múltiples algoritmos para generar hashes. Una de las características esperadas de los algoritmos de dichos algoritmos es que no exija mucha capacidad de cómputo y que la probabilidad de colisión sea lo más baja posible.
Importante de la función hash en esta implementación, es que tiene que generar números, aunque se repitan, en el rango de valores validos de los indices, en el ejemplo de 0-49, y se logro, si lo observan cuando hace la operacion %(modulo) entre la longitud del array, saludos :)
Dentro de la función set primero se crea un address:
const address =this.hashMethod(key)
Por la lógica programada este address siempre será un valor entre 0 y 49. Luego, verificamos si existe algo en la posición address. de no existir, creamos un array vacío en esa posición. Esto se hace con la finalidad de evitar conflictos, si en algún momento otra key genera el mismo hash, agregaremos los nuevos valores al array:
if (!this.data[address])
this.data[address] = []
Ya para terminar, agregamos el par [key, value] al array antes creado para evitar conflictos:
this.data[address].push([key, value])
Finalmente retornamos los datos:
returnthis.data
Esta seria mi propuesta de implementación del método set para que sobreescriba el valor anterior si se llama de nuevo con la misma key:
set(key, value){const address =this.hashMethod(key);const bucket =this.data[address];if(!bucket){this.data[address]=[[key, value]];returnthis.data;}for(let i =0; i < bucket.length; i++){if(bucket[i][0]=== key){ bucket[i][1]= value;returnthis.data;}} bucket.push([key, value]);returnthis.data;}
Ahora pueden llamar las veces que sea necesario myHashTable.set('Diego', "Something"); y no nos va a crear un nuevo elemento en el bucket de la key Diego.