2009-11-02 21 views
52

Busco una función hash que:¿Qué es una buena función hash de 64 bits en Java para cadenas textuales?

  1. hashes cadenas de texto así (por ejemplo, pocas colisiones)
  2. está escrito en Java, y ampliamente utilizado
  3. Bono: trabaja en varios campos (en lugar de concatenarlos y aplicar el hash en la cadena concatenada)
  4. Bonificación: tiene una variante de 128 bits.
  5. Bonificación: No requiere CPU.
+25

El siguiente enlace tiene varias implementaciones de funciones generales propósito de hash que sean eficientes y exhiben colisiones mínimos: http://www.partow.net/programming/hashfunctions/index.html –

Respuesta

55

¿Por qué no utiliza una variante long del predeterminado String.hashCode() (donde algunos chicos realmente inteligentes sin duda se esfuerzan para hacerlo eficiente, sin mencionar los miles de ojos de desarrollador que ya vieron este código)?

// adapted from String.hashCode() 
public static long hash(String string) { 
    long h = 1125899906842597L; // prime 
    int len = string.length(); 

    for (int i = 0; i < len; i++) { 
    h = 31*h + string.charAt(i); 
    } 
    return h; 
} 

Si usted está buscando aún más bits, que probablemente podría utilizar una edición BigInteger :

Como ya he mencionado en un comentario a la respuesta de @brianegge, no hay muchos casos de uso de hashes con más de 32 bits y lo más probable es que no uno solo de los hashes con más de 64 bits:

que podía imaginar una enorme tabla hash distribuida a través de docenas de servidores, quizás almacenar decenas de miles de asignaciones. Para tal escenario, @brianegge todavía tiene un punto válido aquí: 32 bit permiten 2^32 (ca. 4.3 billones) diferentes claves hash. Suponiendo un algoritmo fuerte, aún debería tener muy pocas colisiones. Con 64 bit (18,446,744,073 billones de teclas diferentes) sin duda guardará, independientemente de cualquier escenario loco que lo necesite.Sin embargo, es casi imposible pensar en los casos de uso para llaves de 128 bits (340,282,366,920,938,463,463,374,607,431 mil millones de teclas posibles).

combinar el hash para varios campos, simplemente hacer un XOR multiplican uno con un primer y añadirlos:

long hash = MyHash.hash(string1) * 31 + MyHash.hash(string2); 

El pequeño primo es allí para evitar código hash igual para los valores conmutadas , es decir, {'foo', 'bar'} y {'bar', 'foo'} no son iguales y deben tener un código hash diferente. XOR es malo, ya que devuelve 0 si ambos valores son iguales. Por lo tanto, {'foo', 'foo'} y {'bar', 'bar'} tendrían el mismo código hash.

+0

Tenga en cuenta que para Cadenas <= 5 caracteres, los 32 bits superiores serán 0. Así que esto solo hará una diferencia para las cadenas más largas. –

+0

Buena captura. Un valor inicial más alto y otro no nulo debería ayudar. – sfussenegger

+0

Elegí una prima bastante alta de 64 bits como valor de inicio. Como resultado, los valores hash para Cadenas <= 5 caracteres no deberían ser 0 en los primeros 32 bits. Sin embargo, pensándolo bien, dudo que tener 32 0s al principio hiera las propiedades de la función hash. Mantuve 31 como segundo primo ya que esto es lo que se usa en String.hashCode() también (que todavía está muy cerca de lo que sugiero aquí) – sfussenegger

-2

DESCARGO DE RESPONSABILIDAD: Esta solución es aplicable si desea hojear palabras de lenguaje natural de manera eficiente. Es ineficaz para hash texto más largo, o texto que contiene caracteres no alfabéticos.

No estoy al tanto de una función, pero esto es una idea que podría ayudar:

  • Dedicar 52 de los 64 bits que representan a la que las letras están presentes en la cadena. Por ejemplo, si 'a' estuviera presente, establecería el bit [0], para 'b' establecería el bit 1, para 'A' establecería el bit [26]. De esta forma, solo el texto que contiene exactamente el mismo conjunto de letras tendrá la misma "firma".

Puede utilizar los 12 bits restantes para codificar la longitud de la cadena (o un valor de módulo) para reducir aún más las colisiones, o generar un código hash de 12 bits utilizando una función hash tradicional.

Suponiendo que su entrada es solo de texto, me imagino que esto daría lugar a muy pocas colisiones y sería de bajo costo para calcular (O (n)). A diferencia de otras soluciones, hasta ahora este enfoque tiene en cuenta el dominio del problema para reducir las colisiones - Se basa en el Detector Anagram descrito en Perlas de programación (consulte here).

+0

-1 Cuanto más largas sean las cadenas, más colisiones obtendrá. Además, el hash es débil ya que (asumiendo el lenguaje natural) la mayoría de las cadenas contendrán vocales y consonantes frecuentes (incluso es posible adivinar de manera bastante confiable el lenguaje de una cuerda mediante consonantes frecuentes y vocales por cierto). – sfussenegger

+1

@sfussenegger: OP menciona la necesidad de tener cadenas de texto bien, lo que implica un límite superior en la longitud de la cadena (por ejemplo, la palabra más larga en el idioma inglés tiene solo 45 caracteres). Además, el OP no menciona que esto debe ser un hash seguro. – Adamski

+1

¿Por qué este requisito implica un límite superior en la longitud de la cuerda? Esta función hash es extremadamente débil, independientemente de si es segura o no. – sfussenegger

4

Create an SHA-1 hash y luego enmascare las 64bits más bajas.

+0

Incluso podría hacer un XOR de los primeros 64 bits. Pero, ¿no es un hash SHA-1 un poco exagerado? Si no es necesario un hash criptográficamente seguro, definitivamente ha perdido algunos puntos en el requisito 5;) – sfussenegger

+4

@sfussenegger: No intente agregar aleatoriedad aleatoria. XOR no siempre ayuda. Incluso cortar el hash puede tener resultados impredecibles. Pruébelo con unos pocos millones de casos de prueba o comprenda las matemáticas que lo respaldan. De lo contrario, solo empeorarías las cosas con una "mejora ciega". –

+4

No se trata de agregar aleatoriedad aleatoria.La idea era simplemente mantener todos los bits del hash SHA-1 ** diseñado para una distribución uniforme **. Por lo tanto, no debería haber ningún efecto secundario inesperado, pero al final es una carga inútil al final. El recorte no tiene resultados impredecibles, porque eso es exactamente lo que p. HashMap.indexFor (int, int) lo hace para asignar un hash a un índice de la tabla hash. Por lo tanto, realmente no importa si se recorta un hash de 128 bits a 64 bits, ya que se recortará aún más para que se ajuste a la tabla hash de todos modos. – sfussenegger

0

¿Miras Apache commons lang?

Pero para 64 bit (y 128) necesita algunos trucos: las reglas establecidas en el libro Effective Java de Joshua Bloch lo ayudan a crear hash fácil de 64 bits (solo use long en lugar de int). Por 128 bits que necesita cortes adicionales ...

+0

Commons-lang no ayudará en absoluto a hashes más grandes que los de 32 bits estándar. Lo hace muy bien, pero más allá de eso no tanto. – jasonmp85

4
long hash = string.hashCode(); 

Sí, los mejores 32 bits será 0, pero es probable que se quede sin recursos de hardware antes de ejecutar a tener problemas con colisiones hash. El hashCode in String es bastante eficiente y bien probado.

actualización creo que los satisface por encima de la cosa más simple que posiblemente podrían trabajar, sin embargo, estoy de acuerdo con @sfussenegger idea de extender el hashCode cadena existente.

Además de tener un buen hashCode para su String, le recomendamos que vuelva a aplicar el código hash en su implementación. Si su almacenamiento es utilizado por otros desarrolladores, o usado con otros tipos, esto puede ayudar a distribuir sus claves. Por ejemplo, el HashMap de Java se basa en tablas hash de potencia de dos, por lo que agrega esta función para garantizar que los bits más bajos estén suficientemente distribuidos.

h ^= (h >>> 20)^(h >>> 12); 
    return h^(h >>> 7)^(h >>> 4); 
+1

-1 Los recursos de hardware no son el problema aquí. No especifique cómo se usará el valor de hash, pero le prometo que no está "almacenando n valores en un hashmap". Conseguiré colisiones si proceso suficientes elementos antes de tener problemas de hardware. – ripper234

+0

Pude imaginar una * enorme * hashtable distribuida en docenas de servidores, tal vez almacenando decenas de miles de millones de mapeos. Para tal escenario, @brianegge todavía tiene un punto válido aquí: 32 bit permiten 2^32 (ca. 4.3 billones) diferentes claves hash. Suponiendo un algoritmo fuerte, aún debería tener muy pocas colisiones. Con 64 bit (18,446,744,073 billones de teclas diferentes) seguramente se guardará, independientemente de cualquier escenario loco para el que lo necesites. Sin embargo, es bastante imposible pensar en cajas de uso para llaves de 128 bits (340,282,366,920,938,463,463,374,607,431 mil millones). – sfussenegger

+0

El punto principal aquí: un par de personas en Sun y en todo el mundo han mejorado este algoritmo en los últimos diez años. Es poco probable que pueda encontrar algo sin invertir al menos una semana haciendo una investigación exhaustiva sobre las propiedades de distribución de sus cadenas. –

2

¿Por qué no utilizar un polinomio CRC64? Estos son razonablemente eficientes y optimizados para garantizar que todos los bits se cuenten y distribuyan en el espacio de resultados.

Hay un montón de implementaciones disponibles en la red si google "CRC64 Java"

1

hacer algo como esto:

import java.io.ByteArrayOutputStream; 
import java.io.DataOutputStream; 
import java.io.IOException; 
import java.math.BigInteger; 
import java.security.MessageDigest; 
import java.security.NoSuchAlgorithmException; 

public class Test { 

    public static void main(String[] args) throws NoSuchAlgorithmException, 
      IOException { 
     ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     DataOutputStream dos = new DataOutputStream(baos); 

     try { 
      MessageDigest md = MessageDigest.getInstance("MD5"); 
      SomeObject testObject = new SomeObject(); 

      dos.writeInt(testObject.count); 
      dos.writeLong(testObject.product); 
      dos.writeDouble(testObject.stdDev); 
      dos.writeUTF(testObject.name); 
      dos.writeChar(testObject.delimiter); 
      dos.flush(); 

      byte[] hashBytes = md.digest(baos.toByteArray()); 
      BigInteger testObjectHash = new BigInteger(hashBytes); 

      System.out.println("Hash " + testObjectHash); 
     } finally { 
      dos.close(); 
     } 
    } 

    private static class SomeObject { 
     private int count = 200; 
     private long product = 1235134123l; 
     private double stdDev = 12343521.456d; 
     private String name = "Test Name"; 
     private char delimiter = '\n'; 
    } 
} 

DataOutputStream le permite escribir primitivas y Cuerdas y ellos tienen salida como bytes. Envolviendo una ByteArrayOutputStream en ella dejará que se escribe a una matriz de bytes, que se integra muy bien con MessageDigest. Puede elegir de cualquier algoritmo enumerado here.

Finalmente BigInteger le permiten apagar los bytes de salida en un número más fácil de usar. Los algoritmos MD5 y SHA1 producen hashes de 128 bits, por lo que si necesita 64 puede truncar.

SHA1 debería tener casi cualquier cosa, y con colisiones infrecuentes (es de 128 bits). Esto funciona desde Java, pero no estoy seguro de cómo se implementa. En realidad, puede ser bastante rápido. Funciona en varios campos en mi implementación: simplemente empújelos todos al DataOutputStream y listo. Incluso podría hacerlo con reflejos y anotaciones (quizás @HashComponent(order=1) para mostrar qué campos entran en un hash y en qué orden).Tiene una variante de 128 bits y creo que encontrarás que no usa tanta CPU como crees que será.

He utilizado un código como este para obtener hashes para enormes conjuntos de datos (a estas alturas probablemente miles de millones de objetos) para poder fragmentarlos en muchas tiendas back-end. Debería funcionar para lo que sea que lo necesite. Tenga en cuenta que creo que tal vez quiera llamar solo al MessageDigest.getInstance() una vez y luego al clone() a partir de ese momento: IIRC la clonación es mucho más rápida.

1

reversa de la cadena para obtener otro código hash de 32 bits y luego combinar los dos:

String s = "astring"; 
long upper = ((long) s.hashCode()) << 32; 
long lower = ((long) s.reverse().hashCode()) - ((long) Integer.MIN_VALUE); 
long hash64 = upper + lower; 

Ésta es pseudocódigo; el método String.reverse() no existe y deberá implementarse de otra manera.

Cuestiones relacionadas