字符串的良好哈希函数


160

我试图为字符串想出一个很好的哈希函数。我当时想对字符串中的前五个字符的unicode值进行汇总(假设它有五个,否则就在结尾处停止)是一个好主意。这是一个好主意,还是一个坏主意?

我正在用Java进行此操作,但是我无法想象这会带来很大的不同。


4
好的哈希函数在很大程度上取决于哈希的输入以及算法的要求。例如,如果您所有的字符串都以相同的五个字符开头,那么这种哈希将不是很好。它还将趋于导致正态分布。
WhirlWind

1
可能的副本98153
Michael Mrozek

14
你为什么不能使用String自己的hashCode()
巴特·基尔斯

@WhirlWind,是的,我不确定字符串会有什么,除了它可能会是英文文本。
Leif Andersen

@Barl,主要是因为我的教授告诉我们实现自己的哈希函子...以及我不想使用Java的原因是因为它是通用的,我想可以使用更具体的哈希函子更好。
Leif Andersen

Answers:


160

通常哈希不会做算术,否则stoppots将具有相同的哈希值。

并且您不会将其限制为前n个字符,因为否则house和house将具有相同的哈希值。

通常,哈希采用值并将其乘以质数(使其更有可能生成唯一的哈希),因此您可以执行以下操作:

int hash = 7;
for (int i = 0; i < strlen; i++) {
    hash = hash*31 + charAt(i);
}

@jonathanasdf您怎么能说它总是为您提供唯一的哈希键。有数学证明吗?我认为我们必须使用具有更大质数的哈希散列,否则会发生溢出问题。
devsda

17
@devsda他没有说总是独特的,他说更有可能是独特的。至于为什么,在Google上的快速搜索显示了这篇文章:computinglife.wordpress.com/2008/11/20/…解释了为什么31被用于Java字符串哈希。没有给出数学证明,但是它确实解释了为什么素数会更好地起作用的一般概念。
Pharap 2013年

2
非常感谢您阐明了进行更好的哈希处理的想法。只是要仔细检查-在存储对象之前,Java将使用hashCode()返回值映射到某些表索引。因此,如果hashCode()返回m,它会执行(m mod k)类似的操作来获取大小为k的表的索引。那正确吗?
whitehat

1
“哈希=哈希* 31 + charAt(i);” 产生相同的哈希值,用于现货,顶部,止损,选择和底池。
Jack Straub

1
@maq我相信你是正确的。不知道我在想什么
杰克·斯特劳布'18

139

如果这是安全的事情,则可以使用Java crypto:

import java.security.MessageDigest;

MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(stringToEncrypt.getBytes());
String encryptedString = new String(messageDigest.digest());

93
真好 我有一个机器学习应用程序,对大型语料库进行统计NLP。在对文本中的原始单词进行了几次形态学归一化的初始传递之后,我丢弃了字符串值并改用哈希码。在我的整个语料库中,大约有600,000个唯一单词,使用默认的Java哈希码功能,我遇到了3.5%的冲突。但是,如果我SHA-256字符串值,然后从摘要的字符串生成哈希码,则冲突率小于0.0001%。谢谢!
benjismith

3
感谢您提供有关冲突和单词数量的信息。很有帮助。
philipp 2012年

19
@benjismith一百万个中的一个太大了……“小于0.0001%”是一种倾斜的说法,“精确地为0”吗?我真的怀疑您看到SHA-256碰撞,因为从未在任何地方,任何地方观察到过这种碰撞;甚至不是160位SHA-1。如果您有两个字符串产生相同的SHA-256,那么安全社区将很乐意看到它们。您将以举世无双的方式举世闻名。参见SHA函数比较
Tim Sylvester

7
@TimSylvester,您误会了。我没有找到SHA-256碰撞。我计算了SHA-256,然后将生成的字节序列输入到典型的Java“ hashCode”函数中,因为我需要32位哈希。那是我发现碰撞的地方。没什么了不起的:)
Benjismith 2014年

1
“散列”和“加密”之间没有区别吗?我了解MessageDigest是一种单向哈希函数,对吗?另外,当我使用该函数时,在LibreOffice中打开文件时,我得到的哈希字符串是很多垃圾UTF字符。是否可以将散列字符串作为一堆字母数字字符而不是垃圾UTF字符来获取?
2016年

38

您可能应该使用String.hashCode()

如果您真的想自己实现hashCode:

不要试图从哈希码计算中排除对象的重要部分以提高性能-高效Java的Joshua Bloch

只使用前五个字符是个坏主意。考虑一下URL之类的层次结构名称:它们都具有相同的哈希码(因为它们都以“ http://”开头,这意味着它们存储在哈希图中的同一存储桶下,表现出糟糕的性能。

这是一个有关“ 有效Java ”中的String hashCode的战争故事:

在1.2之前的所有发行版中实现的String哈希函数最多检查16个字符,从第一个字符开始,在整个字符串中均匀分布。对于大量的层次结构名称(例如URL),此哈希函数显示了可怕的行为。


1
如果使用的是双散列集合,那么让第一个散列真正快速又肮脏可能是值得的。如果一个具有一千个长字符串,其中一个字符串由一个伪函数映射到一个特定值,而另一个则映射到不同的值,则在单哈希表中的性能会很差,但在双哈希表中的性能会很差。哈希表,其中第二个哈希检查了整个字符串,可能几乎是单个哈希表的两倍(因为一半字符串不必完全哈希)。但是,没有一个标准的Java集合进行双哈希处理。
2014年

在有效的Java链路断开时@Frederik
幼儿园

17

如果您使用Java进行此操作,那么为什么要这样做呢?只需调用.hashCode()字符串


2
我将其作为类的一部分来做,而分配的一部分是编写几个不同的哈希函数。教授告诉我们要寻求“更好”的外部帮助。
Leif Andersen

20
如果需要跨JVM版本和实现保持一致,则不应依赖.hashCode()。而是使用一些已知的算法。
Stephen Ostermiller 2013年

7
的算法String::hashCode是在JDK中指定的,因此它与类的存在一样可移植java.lang.String
yshavit 2015年


8

Nick提供的此功能很好,但是如果使用新的String(byte [] bytes)进行到String的转换,它将失败。您可以使用此功能执行此操作。

private static final char[] hex = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

public static String byteArray2Hex(byte[] bytes) {
    StringBuffer sb = new StringBuffer(bytes.length * 2);
    for(final byte b : bytes) {
        sb.append(hex[(b & 0xF0) >> 4]);
        sb.append(hex[b & 0x0F]);
    }
    return sb.toString();
}

public static String getStringFromSHA256(String stringToEncrypt) throws NoSuchAlgorithmException {
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(stringToEncrypt.getBytes());
    return byteArray2Hex(messageDigest.digest());
}

也许这可以帮助某人


您可以将字节数组传递给messageDigest.update()。
szgal 2015年

byteArray2Hex()-正是我要找的东西!非常感谢:)
Krzysiek


5

有传言说FNV-1是一个很好的字符串散列函数。

对于长字符串(长于大约200个字符),可以通过MD4哈希函数获得良好的性能。作为一种加密功能,它大约在15年前就被打破了,但是出于非加密目的,它仍然非常好,而且速度惊人。在Java的上下文中,您必须将16位char值转换为32位字,例如,通过将这些值成对分组。可以在sphlib中找到Java中MD4的快速实现。在课堂分配的情况下,可能会造成过大的杀伤力,但值得一试。


该哈希函数比java附带的哈希函数好得多。
clankill3r

3

如果您想查看行业标准实现,请查看java.security.MessageDigest

“消息摘要是安全的单向哈希函数,可接收任意大小的数据并输出固定长度的哈希值。”


1

这是一个解释许多不同哈希函数的链接,目前,我更喜欢ELF哈希函数来解决您的特定问题。它以任意长度的字符串作为输入。


1

sdbm:此算法是为sdbm(ndbm的公共域重新实现)数据库库创建的

static unsigned long sdbm(unsigned char *str)
{   
    unsigned long hash = 0;
    int c;
    while (c = *str++)
            hash = c + (hash << 6) + (hash << 16) - hash;

    return hash;
}

0
         public String hashString(String s) throws NoSuchAlgorithmException {
    byte[] hash = null;
    try {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        hash = md.digest(s.getBytes());

    } catch (NoSuchAlgorithmException e) { e.printStackTrace(); }
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < hash.length; ++i) {
        String hex = Integer.toHexString(hash[i]);
        if (hex.length() == 1) {
            sb.append(0);
            sb.append(hex.charAt(hex.length() - 1));
        } else {
            sb.append(hex.substring(hex.length() - 2));
        }
    }
    return sb.toString();
}

-1

在尝试为字符串开发良好的hast函数时,最好使用奇数。这个函数接受一个字符串并返回一个索引值,到目前为止,它的工作还不错。并且碰撞更少 索引范围从0-300甚至可能更多,但是到目前为止,即使是像“机电工程”这样的长字,我也没有得到更高的体现

int keyHash(string key)
{
    unsigned int k = (int)key.length();
    unsigned int u = 0,n = 0;

    for (Uint i=0; i<k; i++)
    {
        n = (int)key[i];
        u += 7*n%31;
    }
    return u%139;
}

您可以做的另一件事是,将每个字符int解析结果乘以索引,例如“ bear”(0 * b)+(1 * e)+(2 * a)+(3 * r)一词,这将为您提供索引要使用的int值。上面的第一个哈希函数在“ here”和“ hear”处发生碰撞,但是在提供一些良好的唯一值方面仍然很棒。下面的一个不会与“ here”和“ hear”发生冲突,因为随着字符的增加,我会将每个字符与索引相乘。

int keyHash(string key)
{
    unsigned int k = (int)key.length();
    unsigned int u = 0,n = 0;

    for (Uint i=0; i<k; i++)
    {
        n = (int)key[i];
        u += i*n%31;
    }
    return u%139;
}

-1

这是我用于构建的哈希表的简单哈希函数。它基本上用于获取文本文件,并将每个单词存储在代表字母顺序的索引中。

int generatehashkey(const char *name)
{
        int x = tolower(name[0])- 97;
        if (x < 0 || x > 25)
           x = 26;
        return x;
}

这基本上是根据单词的第一个字母对单词进行哈希处理。因此,以'a'开头的单词的哈希键为0,'b'的值为1,依此类推,'z'的值为25。数字和符号的哈希键为26。这是一个优点; 您可以轻松,快速地计算出给定单词在哈希表中的索引位置,因为所有单词都按字母顺序排列,如下所示:代码可以在这里找到:https//github.com/abhijitcpatil/general

提供以下文本作为输入:阿提克斯有一天对杰姆说 “我希望您在后院的锡罐上开枪,但我知道您会追赶鸟类。射杀所有想要的蓝鸟,如果可以击中它们,但要记住杀死一只知更鸟是一种罪过。” 那是我唯一一次听到Atticus说做某事是一种罪过,我问Maudie小姐。她说:“你父亲的权利。” “模仿鸟除了做音乐让我们欣赏之外,没有做任何事情。他们不吃人的花园,不筑巢于玉米婴儿床,他们不做任何事情,而是为我们献出自己的心。这就是为什么杀死一只模仿鸟是一种罪过。

这将是输出:

0 --> a a about asked and a Atticus a a all after at Atticus
1 --> but but blue birds. but backyard
2 --> cribs corn can cans
3 --> do dont dont dont do dont do day
4 --> eat enjoy. except ever
5 --> for for fathers
6 --> gardens go
7 --> hearts heard hit
8 --> its in it. I it I its if I in
9 --> jays Jem
10 --> kill kill know
11 --> 
12 --> mockingbird. music make Maudie Miss mockingbird.”
13 --> nest
14 --> out one one only one
15 --> peoples
16 --> 17 --> right remember rather
18 --> sin sing said. she something sin say sin Shoot shot said
19 --> to Thats their thing they They to thing to time the That to the the tin to
20 --> us. up us
21 --> 
22 --> why was was want
23 --> 
24 --> you you youll you
25 --> 
26 --> Mockingbirds  Your em Id

2
良好的哈希函数可将值平均分配到存储桶中。
乔纳森·彼得森

-1

这样可以避免任何冲突,而且在我们在计算中使用移位之前将非常快。

 int k = key.length();
    int sum = 0;
    for(int i = 0 ; i < k-1 ; i++){
        sum += key.charAt(i)<<(5*i);
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.