如何改善短弦的散列以避免碰撞?

时间:2011-12-22 00:15:59

标签: c# .net string hash collision

我遇到了在.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或更新它也缺失。

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

2 个答案:

答案 0 :(得分:7)

您确定碰撞是谁导致问题吗?当你说

  

我终于发现了造成这个错误的原因

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

我放了一段代码来检查3个字母单词的碰撞。此代码不会为它们报告单个冲突。你明白我的意思吗?看起来像buid-in哈希算法并不是那么糟糕。

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 :(得分: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();
}

这已经破了。它将=“foot”,to =“ar”视为与from =“foo”相同,而不是“tar”。由于您的Equals函数不认为它们相等,因此您的散列函数不应该。可能的修复包括:

1)将字符串从“XXX”形成为哈希值。 (这假设字符串“XXX”几乎不会出现在输入字符串中。

2)将'from'的散列与'to'的散列相结合。你必须使用一个聪明的组合功能。例如,XOR或sum将导致from =“foo”,to =“bar”以从=“bar”散列到=“foo”。不幸的是,如果不了解散列函数的内部结构,选择正确的组合函数并不容易。你可以尝试:

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