【问题标题】:How to improve hashing for short strings to avoid collisions?如何改进短字符串的散列以避免冲突?
【发布时间】:2011-12-22 00:15:59
【问题描述】:

我在 .NET4 中使用短字符串时遇到哈希冲突问题。
编辑:我在 .NET 中使用内置字符串哈希函数.

我正在使用像这样存储转换方向的对象来实现缓存

public class MyClass
{
    private string _from;
    private string _to;

   // More code here....

    public MyClass(string from, string to)
    {
        this._from = from;
        this._to = to;
    }

    public override int GetHashCode()
    {
        return string.Concat(this._from, this._to).GetHashCode();
    }

    public bool Equals(MyClass other)
    {
        return this.To == other.To && this.From == other.From;
    }

    public override bool Equals(object obj)
    {
        if (obj == null) return false;
        if (this.GetType() != obj.GetType()) return false;
        return Equals(obj as MyClass);
    }
}

这取决于方向,fromto 由“AAB”和“ABA”等短字符串表示。

我遇到了这些小字符串的稀疏哈希冲突,我尝试了一些简单的方法,比如添加盐(没有用)。

问题是我的太多像“AABABA”这样的小字符串与“ABAAAB”相反的哈希值冲突(请注意,这些不是真实的例子,我不知道 AAB 和ABA 实际上会导致冲突!)

我已经完成了像实现 MD5 一样的繁重任务(它有效,但速度要慢得多)

我也在这里执行了 Jon Skeet 的建议:
Should I use a concatenation of my string fields as a hash code? 这可行,但我不知道它对我的各种 3 个字符的字符串有多可靠。

如何改进和稳定小字符串的散列而不像 MD5 那样增加太多开销?

编辑:作为对发布的一些答案的回应...缓存是使用从MyClass 键入的并发字典实现的,如上所述。如果我将上面代码中的GetHashCode 替换为我发布的链接中的@JonSkeet 代码之类的简单代码:

int hash = 17;
hash = hash * 23 + this._from.GetHashCode();
hash = hash * 23 + this._to.GetHashCode();        
return hash;

一切都按预期运行。 还值得注意的是,在这个特定的用例中,缓存不在多线程环境中使用,因此不存在竞争条件。

编辑:我还应该注意,这种不当行为取决于平台。它在我完全更新的 Win7x64 机器上按预期工作,但在未更新的 Win7x64 机器上运行不正常。我不知道缺少哪些更新的范围,但我知道它没有 Win7 SP1 ......所以我假设可能还有一个框架 SP 或更新它也丢失了。

编辑: 如前所述,我的问题不是由散列函数问题引起的。我有一个难以捉摸的竞争条件,这就是为什么它可以在某些计算机上工作但不能在其他计算机上工作的原因,也是为什么“较慢”的散列方法可以使事情正常工作的原因。我选择的答案对于理解为什么我的问题不是字典中的哈希冲突最有用。

【问题讨论】:

  • 您遇到了与 3 个字符的字符串发生冲突?想发布其中的一些吗?我怀疑不是散列函数。
  • 你能展示一下缓存是如何实现的吗?
  • 在您展示的代码中,Equals 不是“public virtual bool Equals(object obj)”的覆盖...您在下面引用了哪些更新?
  • 我对生成的 3 + 3 对进行了一些简单的测试。第一种情况(使用 Concat)的碰撞率为 1.7,第二种情况下的碰撞率为 4。正如预期的那样,第二个会产生更多的碰撞(但在这种情况下会更好,因为它不会创建新对象)。而且我看不出碰撞如何导致您的问题。在 Dictionary 中预期并正确处理碰撞。它更多的是关于 Equals,但它看起来不错。 MyClass 看起来不错。我会说这个问题在这个类之外的某个地方 - 如何使用字典,如何创建 MyClass 实例等等......
  • @Downvoter,需要解释一下吗?我可能在这里寻求解决方案的方向错误,但我肯定会努力解决这个问题......

标签: c# .net string hash collision


【解决方案1】:

您确定碰撞是造成问题的原因吗?当你说

我终于发现了导致这个错误的原因

你的意思是你的代码有些慢还是别的什么?如果不是,我很好奇这是什么问题?因为任何散列函数(有限域上的“完美”散列函数除外)都会导致冲突。

我放了一段代码来检查 3 个字母单词的冲突。而且这段代码不会为他们报告一次碰撞。你明白我的意思吗?看起来内置哈希算法还不错。

Dictionary<int, bool> set = new Dictionary<int, bool>();
char[] buffer = new char[3];
int count = 0;
for (int c1 = (int)'A'; c1 <= (int)'z'; c1++)
{
    buffer[0] = (char)c1;
    for (int c2 = (int)'A'; c2 <= (int)'z'; c2++)
    {
        buffer[1] = (char)c2;
        for (int c3 = (int)'A'; c3 <= (int)'z'; c3++)
        {
            buffer[2] = (char)c3;
            string str = new string(buffer);
            count++;
            int hash = str.GetHashCode();
            if (set.ContainsKey(hash))
            {
                Console.WriteLine("Collision for {0}", str);
            }
            set[hash] = false;
        }
    }
}

Console.WriteLine("Generated {0} of {1} hashes", set.Count, count);

虽然您可以选择几乎任何著名的散列函数(正如 David 所提到的),甚至可以选择“完美”散列,因为看起来您的域是有限的(例如最小完美散列)......了解问题的根源是否真的是碰撞。

更新

我想说的是,.NET 内置的字符串哈希函数还不错。它不会产生太多冲突,以至于您需要在常规场景中编写自己的算法。这不取决于字符串的长度。如果您有很多 6 符号字符串,这并不意味着您看到冲突的机会高于 1000 符号字符串。这是哈希函数的基本属性之一。

再一次,另一个问题是你会因为碰撞而遇到什么样的问题?所有内置哈希表和字典都支持冲突解决。所以我想说你能看到的只是......可能有点慢。这是你的问题吗?

至于你的代码

return string.Concat(this._from, this._to).GetHashCode(); 

这可能会导致问题。因为在每次哈希码计算中,您都会创建一个新字符串。也许这就是导致您的问题的原因?

int hash = 17; 
hash = hash * 23 + this._from.GetHashCode(); 
hash = hash * 23 + this._to.GetHashCode();         
return hash; 

这将是更好的方法 - 只是因为您不会在堆上创建新对象。实际上,这是这种方法的要点之一——在不创建新对象的情况下获得具有复杂“键”的对象的良好哈希码。因此,如果您没有单个值键,那么这应该适合您。顺便说一句,这不是一个新的哈希函数,这只是一种在不损害哈希函数主要属性的情况下组合现有哈希值的方法。

【讨论】:

  • 是的,我也很好奇为什么一个冲突的值可能会导致一个错误。至少,散列似乎被误解了,因为预计会发生冲突并且无论如何都应该处理(除了您在此处描述的完美散列的情况)
  • +1 我很难相信微软的散列实现有那么糟糕。我想知道他是否可能会截断散列或以其他方式处理不当,这会导致冲突。而且,当然,如果您的代码在哈希冲突方面存在错误,那么您就做错了,因为应该会发生冲突。
  • 我的描述具有误导性。它们实际上是六个字符串,由两个三个字符串连接而成。
  • @Matthew PK 我已经更新了我的帖子。但目前还不清楚碰撞会给你带来什么样的问题……
  • @Matthew PK 这是一个可能的问题。字典不仅仅依赖于哈希码。如果哈希码不同 - 好吧,对象不同。但如果哈希码等于字典,则使用 E​​quals 方法实际比较对象(请参阅msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx)。所以首先,你需要重写 Equal 方法以及 GetHashCode。第二个确保所有这些都正确实施,因为您刚才描述的内容听起来不正确。实际代码会有所帮助。
【解决方案2】:

任何常见的散列函数都应该适用于此目的。如果您在这样的短字符串上遇到冲突,我会说您使用的是异常糟糕的哈希函数。您可以毫无问题地使用JenkinsKnuth's

这是一个非常简单的散列函数,应该足够了。 (用 C 实现,但应该很容易移植到任何类似的语言。)

unsigned int hash(const char *it)
{
 unsigned hval=0;
 while(*it!=0)
 {
  hval+=*it++;
  hval+=(hval<<10);
  hval^=(hval>>6);
  hval+=(hval<<3);
  hval^=(hval>>11);
  hval+=(hval<<15);
 }
 return hval;
}

请注意,如果要修剪此函数输出的位,则必须使用最低有效位。您还可以使用 mod 来减小输出范围。字符串的最后一个字符往往只影响低位。如果您需要更均匀的分布,请将return hval; 更改为return hval * 2654435761U;

更新

public override int GetHashCode()
{
    return string.Concat(this._from, this._to).GetHashCode();
}

这个坏了。它将 from="foot",to="ar" 视为与 from="foo",to="tar" 相同。由于您的 Equals 函数不认为那些相等,因此您的哈希函数不应该。可能的修复包括:

1) 形成字符串 from,"XXX",to 并对其进行哈希处理。 (这假设字符串“XXX”几乎从未出现在您的输入字符串中。

2) 将 'from' 的哈希值与 'to' 的哈希值结合起来。您必须使用巧妙的组合功能。例如,XOR 或 sum 将导致 from="foo",to="bar" 的哈希值与 from="bar",to="foo" 相同。不幸的是,如果不了解散列函数的内部结构,选择正确的组合函数并不容易。你可以试试:

int hc1=from.GetHashCode();
int hc2=to.GetHashCode();
return (hc1<<7)^(hc2>>25)^(hc1>>21)^(hc2<<11);

【讨论】:

  • 我在 .NET 中使用内置的 GetHashCode() 实现,用于像 string 这样的结构
  • 那么它似乎异常糟糕,假设您确实看到完整的 32 位哈希值与少量短字符串发生冲突。
  • 确实!因此,当我终于发现导致此错误的原因时,我感到惊讶:)
  • @MatthewPK “对于像 string 这样的结构”,字符串不是 struct 是什么意思
猜你喜欢
  • 2013-04-06
  • 2011-07-02
  • 1970-01-01
  • 2014-03-12
  • 2015-03-21
  • 2018-08-04
  • 2018-02-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多