【问题标题】:Dictionary hash function for fuzzy lookups用于模糊查找的字典哈希函数
【发布时间】:2018-12-13 12:04:05
【问题描述】:

当需要在字符串之间进行近似比较时,基本的Levenshtein Distance 可以提供帮助。它测量与另一个字符串相等所需的字符串修改量:

"aaaa" vs "aaab" => 1
"abba" vs "aabb" => 2
"aaaa" vs "a"    => 3

当使用Dictionary<T, U> 时,可以提供自定义IEqualityComparer<T>。可以将 Levenshtein 距离实现为 IEqualityComparer<string>

public class LevenshteinStringComparer : IEqualityComparer<string>
{
    private readonly int _maximumDistance;

    public LevenshteinStringComparer(int maximumDistance)
        => _maximumDistance = maximumDistance;

    public bool Equals(string x, string y)
        => ComputeLevenshteinDistance(x, y) <= _maximumDistance;

    public int GetHashCode(string obj)
        => 0;

    private static int ComputeLevenshteinDistance(string s, string t)
    {
        // Omitted for simplicity
        // Example can be found here: https://www.dotnetperls.com/levenshtein
    }
}

所以我们可以使用模糊字典:

var dict = new Dictionary<string, int>(new LevenshteinStringComparer(2));
dict["aaa"] = 1;
dict["aab"] = 2; // Modify existing value under "aaa" key

// Only one key was created:
dict.Keys => { "aaa" }

完成所有这些设置后,您可能已经注意到我们没有在LevenshteinStringComparer 中实现正确的GetHashCode,字典将不胜感激。作为关于哈希码的一些经验法则,我会使用:

  • 不相等的对象不应具有相同的哈希码
  • 相等的对象必须具有相同的哈希码

我能想象的唯一可能遵循这些规则的哈希函数是一个常数,就像在给定代码中实现的那样。虽然这不是最佳的,但是当我们开始例如获取字符串的默认哈希时,aaaaab 最终会得到不同的哈希,即使它们被处理为相等。进一步思考这意味着所有可能的字符串都必须具有相同的哈希值。

我说的对吗? 为什么当我使用带有哈希冲突的默认字符串哈希函数作为比较器时,字典的性能会变得更好? 这不应该使字典中的哈希桶无效吗?

public int GetHashCode(string obj)
    => obj.GetHashCode();

【问题讨论】:

  • 我在这里看到了一个问题。如果您允许距离 1 并且已经有一个带有 'aab' 的项目,添加 'aaa' 或 'abb' 的结果相同,但如果第一个项目是 'abb',则最终结果不同。
  • 这让我想起了Ring Species - 作为生活在邻近地区的动物物种仍然可以交配的想法,当你绕过一个大障碍物,如山脉或海洋时,你可以最终使环两端的最后两只动物无法交配。什么时候两种动物不再是同一个物种而变成不同的物种?你的钥匙也一样。我可以从您认为不同的两个键开始,但我可以找到一系列中间键,这些中间键将被视为将前两个键连接在一起的匹配项。
  • 这不是重复的,但我认为这个讨论涉及...stackoverflow.com/questions/15377043/…
  • @PepitoSh 是的,这本词典在某些方面很奇怪,但你提到的是否违反了我们应该遵循的一些具体实施规则?
  • 一点也不。我的世界更具确定性,我看不出这种字典的用途。也许如果你带来了一些潜在的用例。

标签: c# dictionary hash hashtable fuzzy-search


【解决方案1】:

我不认为有一个哈希函数可以在你的情况下工作。

问题是您必须仅根据信号值分配存储桶,而您无法知道之前添加了什么。但是被散列的项目的 Levenshtein 距离可以是从 0 到“无穷大”的任何值,唯一重要的是它与什么进行比较。因此,您不能满足散列函数的第二个条件(相等的对象具有相同的散列码)。

另一个论点 “伪证明” 是当您希望 最大距离为 2 并且您已经 字典中有两个项目 时的情况>,它们之间的距离为 3。如果然后添加一个与第一个项目的距离为 2 且与第二个项目的距离为 1 的字符串,您将如何决定它应该与哪个项目匹配?它满足您对这两个项目的最大值,但它可能应该与第二个而不是第一个匹配。但是对字典的内容一无所知,您不知道如何正确散列它。

对于第二个问题 - 使用默认的 string.GetHashCode() 方法确实可以提高性能,但它会破坏相等比较器的功能。如果您在示例代码上测试此解决方案,您可以看到 dict 现在将包含两个键。这是因为GetHashCode返回了两个不同的哈希码,所以没有冲突,dict现在有两个桶,你的Equals方法甚至没有被执行。

【讨论】:

  • 哦,是的,还没有检查使用字符串散列函数时创建的字典键。顺便说一句,答案很好
  • 很高兴它有帮助:-)。编码愉快!
【解决方案2】:

我可以理解模糊查找。但不是模糊存储。为什么在为“aab”赋值时要覆盖“aaa”?如果你想要的只是模糊查找,那么拥有一个普通的字典不是更好,它有一个扩展来进行模糊查找,比如......

public static class DictionaryExtensions
{
    public static IEnumerable<T> FuzzyMatch<T>(this IDictionary<string, T> dictionary, string key, int distance = 2)
    {
        IEqualityComparer<string> comparer = new LevenshteinStringComparer(distance);

        return dictionary
            .Keys
            .Where(k => comparer.Equals(k, key))
            .Select(k => dictionary[k]);
    }
}

这更像是一个评论而不是一个答案。要回答您的问题,如果您考虑以下示例...

"abba" vs "cbbc" => 2
"cddc" vs "cbbc" => 2
"abba" vs "cddc" => 4

你明白这里的要点了吗?即显然以下不可能是真的

abba == cbbc && 
cddc == cbbc &&
abba != cddc

【讨论】:

    猜你喜欢
    • 2014-10-06
    • 1970-01-01
    • 2015-02-12
    • 2016-10-31
    • 2021-04-06
    • 1970-01-01
    • 1970-01-01
    • 2011-06-10
    相关资源
    最近更新 更多