【问题标题】:How do I calculate a good hash code for a list of strings?如何为字符串列表计算一个好的哈希码?
【发布时间】:2011-02-13 10:10:19
【问题描述】:

背景:

  • 我有一个简短的字符串列表。
  • 字符串的数量并不总是相同,但几乎总是“少数”的数量级
  • 在我们的数据库中,这些字符串将存储在第二个规范化表中
  • 这些字符串在写入数据库后永远不会更改。

我们希望能够在查询中快速匹配这些字符串,而不会因执行大量连接而影响性能。

所以我正在考虑将所有这些字符串的哈希码存储在主表中并将其包含在我们的索引中,因此只有当哈希码匹配时才会由数据库处理连接。

那么我如何获得一个好的哈希码呢?我可以:

  • 对所有字符串的哈希码进行异或运算
  • Xor 与每个字符串后的结果相乘(例如乘以 31)
  • 将所有字符串放在一起,然后得到哈希码
  • 其他方式

那么人们是怎么想的呢?


最后我只是连接字符串并计算连接的哈希码,因为它很简单并且工作得很好。

(如果您关心我们使用的是 .NET 和 SqlServer)


错误!错误!

Quoting from Guidelines and rules for GetHashCode Eric Lippert

文档 System.String.GetHashCode 注释 特别是两个相同的 字符串可以有不同的哈希码 在不同版本的 CLR 中,以及 事实上他们确实如此。不要存储字符串 数据库中的哈希并期望它们 永远一样,因为他们 不会的。

所以 String.GetHashcode() 不应该用于此。

【问题讨论】:

标签: .net database-design hashcode


【解决方案1】:

标准的java实践,就是简单的写

final int prime = 31;
int result = 1;
for( String s : strings )
{
    result = result * prime + s.hashCode();
}
// result is the hashcode.

【讨论】:

  • 这是否会比获取连接字符串的哈希码提供更好的哈希码分布,如果是,为什么?
  • 我不知道,但是如果您将项目放在一个列表中(例如 ArrayList),并要求提供 hoshCode,这就是您将得到的(带有 null 项目的附加约束)一个 0 哈希码)。 java.sun.com/javase/6/docs/api/java/util/List.html#hashCode()
  • 他们这样做的原因是 ArrayList 不知道它包含什么样的对象,所以它唯一可以做的就是结合通过基础对象的元素。
【解决方案2】:

我认为没有理由不连接字符串并计算连接的哈希码。

打个比方,假设我想计算内存块的 MD5 校验和,我不会将块拆分成更小的块并为它们计算单独的 MD5 校验和,然后将它们与一些特别的方法结合起来。

【讨论】:

  • 这听起来很棒!但是,here 是不想这样做的原因,因为如果您的字符串没有区别,您可能会丢失信息。所有["", "aa"]["a", "a"]["aa", ""] 都将具有相同的哈希码!这就是你使用素数加法的原因。
  • 是的。对于正确的散列,IT 安全堆栈交换人员会始终告诉您不要连接可变长度字符串以进行散列。如果您对 2 个单独的 MD5 进行异或运算,那应该没问题。或者,您可以在 2 个字符串之间添加一个分隔符,但这只有在这两个字符串都不包含分隔符的情况下才是安全的。 (通常是您无法保证的)"A"+"|"+"|A""A|"+"|"+"A" 相同。
  • @CarlWalsh 散列不总是意味着丢失信息吗?只要哈希码小于原始数据,一些输入之间总会有冲突。
  • @AndreasBrinck 是的。如果您知道您的字符串具有不同的格式,因此连接不会导致冲突,那么没问题。但总的来说,与使用素数加法算法相比,数据更容易发生冲突,因此串联并不是一个很好的通用解决方案。 GetHashCode() 的设计者会确保空字符串的哈希码不为零,但连接意味着你失去了这种区别。
  • 连接的另一个考虑因素是导致更多的工作被复制,然后对你产生的字符串进行 GC。如果您的应用程序期望 GetHashCode 非常快,那么您可能需要验证性能是否仍然可以接受。
【解决方案3】:

您的第一个选项的唯一不便之处在于 (String1, String2) 会生成与 (String2, String1) 相同的哈希码。如果这不是问题(例如,因为您有修复订单),那很好。

将所有字符串放在一起,然后得到哈希码”对我来说似乎更自然和安全。

更新:正如评论指出的那样,这有一个缺点,即列表 ("x", "yz") 和 ("xy","z") 会给出相同的哈希值。为避免这种情况,您可以使用不能出现在字符串中的字符串分隔符连接字符串。

如果字符串很大,您可能更愿意对每个字符串进行散列,对散列码进行分类并重新散列结果。更多 CPU,更少内存。

【讨论】:

  • 如果你把所有的字符串放在一起,你会得到 HASH("firststring"+"secondstring") == HASH("first"+"stringsecondstring") 这是不好的
【解决方案4】:

另一种在我脑海中浮现的方式,使用基于索引的旋转散列来链接异或:

int shift = 0;
int result = 1;
for(String s : strings)
{
    result ^= (s.hashCode() << shift) | (s.hashCode() >> (32-shift)) & (1 << shift - 1);
    shift = (shift+1)%32;
}

编辑:阅读有效java中给出的解释,我认为geoff的代码会更有效率。

【讨论】:

  • 链接异或也是我的第一个想法。既然一个好的 hashCode 不应该有一个模式,为什么还要费心去改变呢?为什么不把所有的哈希一起异或?
  • @Bill K 因为如果是这样,["hello", "world"] 将具有与 ["world", "hello"] 相同的哈希值 :-)
【解决方案5】:

基于 SQL 的解决方案可以基于 checksum 和 checksum_agg 函数。如果我没听错的话,你会有类似的东西:

MyTable
  MyTableId
  HashCode

MyChildTable
  MyTableId  (foreign key into MyTable)
  String

带有存储在 MyChildTable 中的给定项目 (MyTableId) 的各种字符串。要计算和存储反映这些(永远不会更改的)字符串的校验和,应该可以这样:

UPDATE MyTable
 set HashCode = checksum_agg(checksum(string))
 from MyTable mt
  inner join MyChildTable ct
   on ct.MyTableId = mt.MyTableId
 where mt.MyTableId = @OnlyForThisOne

我相信这是与顺序无关的,因此字符串“The quick brown”会产生与“brown The quick”相同的校验和。

【讨论】:

    【解决方案6】:

    我希望这是不必要的,但由于您没有提及任何听起来您只是在使用哈希码进行第一次检查然后验证字符串实际上相等的事情,我觉得有必要警告您:

    哈希码相等!= 值相等

    会有很多字符串集产生相同的哈希码,但并不总是相等的。

    【讨论】:

    • 你只是使用哈希码进行第一次检查,然后验证字符串实际上是否相等我猜是哈希码的正确用法。
    • @fortran - 是的。但操作并不是说:问题的含义似乎是他们期望如果哈希码相等,那么字符串集是相等的。
    • 我根本没读过。 “所以只有当哈希码匹配时才由数据库处理连接”听起来就像哈希码将是第一次检查一样。
    • @Geoff,你肯定是对的。如果你是,我浪费了 45 秒打字。如果你错了,这可能会挽救 OP 的不愉快日子。
    【解决方案7】:

    所以我明白了,你实际上有一些字符串需要通过哈希码来识别,而你需要在其中识别的那组字符串永远不会改变?

    如果是这种情况,这并不重要,只要您使用的方案为您提供不同字符串/字符串组合的唯一编号。我将首先连接字符串并计算 String.hashCode() 并查看您是否最终得到唯一的数字。如果你不这样做,那么你可以尝试:

    • 不要连接字符串,而是连接组件字符串的哈希码,并尝试不同的乘法器(例如,如果您想识别两个字符串序列的组合,请尝试 HC1 + 17 * HC2,如果这不能给出唯一的数字,尝试 HC1 + 31 * HC2,然后尝试 19,然后尝试 37 等等——基本上任何小的奇数都可以)。
    • 如果您没有以这种方式获得唯一编号——或者如果您需要应对扩展的可能性集——那么请考虑使用更强的哈希码。 64 位哈希码是在易于比较和哈希唯一的可能性之间的良好折衷。

    64 位哈希码的可能方案如下:

    • 使用相当强大的方案生成一个由 256 个 64 位随机数组成的数组(您可以使用 SecureRandom,尽管 XORShift 方案可以正常工作)
    • 选择“m”,另一个“随机”64 位奇数,设置了或多或少一半的位
    • 要生成哈希码,遍历每个字节值 b,组成字符串,然后从随机数数组中取出第 b 个数;然后 XOR 或将其与当前哈希值相加,乘以“m”

    因此,基于数值配方中建议的值的实现将是:

      private static final long[] byteTable;
      private static final long HSTART = 0xBB40E64DA205B064L;
      private static final long HMULT = 7664345821815920749L;
    
      static {
        byteTable = new long[256];
        long h = 0x544B2FBACAAF1684L;
        for (int i = 0; i < 256; i++) {
          for (int j = 0; j < 31; j++) {
            h = (h >>> 7) ^ h;
            h = (h << 11) ^ h;
            h = (h >>> 10) ^ h;
          }
          byteTable[i] = h;
        }
      }
    

    上面是初始化我们的随机数数组。我们使用 XORShift 生成器,但我们真的可以使用任何质量相当好的随机数生成器(使用特定种子创建 SecureRandom() 然后调用 nextLong() 就可以了)。然后,生成哈希码:

      public static long hashCode(String cs) {
        if (cs == null) return 1L;
        long h = HSTART;
        final long hmult = HMULT;
        final long[] ht = byteTable;
        for (int i = cs.length()-1; i >= 0; i--) {
          char ch = cs.charAt(i);
          h = (h * hmult) ^ ht[ch & 0xff];
          h = (h * hmult) ^ ht[(ch >>> 8) & 0xff];
        }
        return h;
      }
    

    一个需要考虑的指南是,给定一个 n 位的哈希码,平均而言,在发生冲突之前,您预计必须生成大约 2^(n/2) 个字符串的哈希值。或者换一种说法,使用 64 位散列,您预计在大约 40 亿个字符串之后会发生冲突(因此,如果您要处理多达几百万个字符串,那么发生冲突的可能性可以忽略不计)。

    另一种选择是 MD5,它是一个非常强的散列(实际上是安全的),但它是一个 128 位散列,因此您必须处理 128 位值的轻微缺点。对于这些目的,我想说 MD5 是多余的——正如我所说,使用 64 位散列,您可以相当安全地处理数百万个字符串。

    (抱歉,我应该澄清一下——MD5 被设计为安全哈希,只是后来发现它不安全。“安全”哈希是指在给定特定哈希时,故意构造输入是不可行的会导致那个散列。在某些情况下——但不是我对你的理解——你需要这个属性。另一方面,如果你正在处理用户输入数据的字符串,你可能需要它- - 即恶意用户可能会故意混淆您的系统。您可能还对我过去写的以下内容感兴趣:

    【讨论】:

      【解决方案8】:

      使用GetHashCode() 不适合组合多个值。问题是对于字符串,哈希码只是一个校验和。这对于相似的值几乎没有熵。例如为 ("abc", "bbc") 添加哈希码将与 ("abd", "abc") 相同,导致冲突。

      如果您需要绝对确定,您会使用真正的哈希算法,如 SHA1、MD5 等。唯一的问题是它们是块函数,很难快速比较哈希是否相等。相反,请尝试使用 CRC 或 FNV1 哈希。 FNV1 32位超级简单:

      public static class Fnv1 {
          public const uint OffsetBasis32 = 2166136261;
          public const uint FnvPrime32 = 16777619;
      
          public static int ComputeHash32(byte[] buffer) {
              uint hash = OffsetBasis32;
      
              foreach (byte b in buffer) {
                  hash *= FnvPrime32;
                  hash ^= b;
              }
      
              return (int)hash;
          }
      }
      

      【讨论】:

        【解决方案9】:

        【讨论】:

          【解决方案10】:

          如果您碰巧使用 Java,则可以创建一个字符串数组(或将集合转换为数组),然后使用 Arrays.hashCode() 文档中的 here

          【讨论】:

            【解决方案11】:

            让我们解决您的根本问题。

            不要使用哈希码。只需为每个字符串添加一个整数主键

            【讨论】:

              猜你喜欢
              • 2013-01-16
              • 1970-01-01
              • 2013-02-16
              • 1970-01-01
              • 2012-07-12
              • 1970-01-01
              • 2017-04-04
              • 2011-11-02
              • 2011-07-20
              相关资源
              最近更新 更多