如何计算字符串列表的良好哈希码?

时间:2010-04-28 15:26:56

标签: .net database-design hashcode

背景:

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

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

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

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

  • Xor所有字符串的哈希码
  • Xor将每个字符串后面的结果乘以(比如31)
  • 将所有字符串组合在一起,然后获取哈希码
  • 其他方式

那么人们的想法是什么?


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

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


Bug!,Bug!

Eric Lippert的

Quoting from Guidelines and rules for GetHashCode

  

的文档   System.String.GetHashCode说明   具体说那两个相同   字符串可以有不同的哈希码   在不同版本的CLR中,和   事实上他们做到了。不要存储字符串   哈希在数据库中并期望它们   永远都是一样的,因为他们   不会。

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

11 个答案:

答案 0 :(得分:45)

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

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

答案 1 :(得分:3)

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

将所有字符串组合在一起然后获取哈希码”对我来说似乎更自然和安全。

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

如果字符串很大,您可能更喜欢散列每个字符串,捕获哈希码并重新生成结果。 CPU更多,内存更少。

答案 2 :(得分:3)

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

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

答案 3 :(得分:2)

另一种突然出现在我头脑中的方式,链式xors带有基于索引的旋转哈希:

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的代码会更有效率。

答案 4 :(得分:1)

基于SQL的解决方案可以基于校验和和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

我相信这是与顺序无关的,因此字符串“快速棕色”将产生与“棕色快速”相同的校验和。

答案 5 :(得分:1)

我希望这是不必要的,但是因为你没有提到任何听起来像你只是使用哈希码进行第一次检查然后验证字符串实际上是相等的东西,我觉得需要警告你:

Hashcode相等!=值相等

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

答案 6 :(得分:1)

所以我理解,你实际上有一些字符串需要通过哈希码识别,你需要识别的那组字符串永远不会改变?

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

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

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

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

因此,基于Numerical Recipes中建议的值的实现将是:

  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被设计为一个安全的哈希,它只是因为它被发现不安全。一个“安全”哈希是一个给定特定哈希,故意构造输入是不可行的会导致那个哈希。在某些情况下 - 但不是我理解你的 - 你需要这个属性。另一方面,你可能需要它,如果你正在处理用户输入数据的字符串 - - 即恶意用户可能故意试图混淆您的系统。您可能也会在以下我写过的内容中进行交流:

答案 7 :(得分:1)

使用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;
    }
}

答案 8 :(得分:0)

答案 9 :(得分:-1)

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

答案 10 :(得分:-3)

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

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