如何处理 Swift 中字典的哈希冲突

How to handle hash collisions for Dictionaries in Swift

TLDR

我的自定义结构实现了 Hashable Protocol。但是,当在 Dictionary 中插入键时发生散列冲突时,不会自动处理它们。我该如何克服这个问题?

背景

我之前曾问过这个问题 . Later I added ,这似乎有效。

但是,最近我发现使用 Dictionary.

时存在 hashValue 碰撞的细微问题

最基本的例子

我已将代码尽可能简化为以下示例。

自定义结构

struct MyStructure: Hashable {

    var id: Int

    init(id: Int) {
        self.id = id
    }

    var hashValue: Int {
        get {
            // contrived to produce a hashValue collision for id=1 and id=2
            if id == 1 {
                return 2 
            }
            return id
        }
    }
}

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

注意重载相等运算符 (==) 的全局函数,以符合 Hashable 协议要求的 Equatable Protocol

精妙词典关键问题

如果我用 MyStructure 作为键创建一个 Dictionary

var dictionary = [MyStructure : String]()

let ok = MyStructure(id: 0)            // hashValue = 0
let collision1 = MyStructure(id: 1)    // hashValue = 2
let collision2 = MyStructure(id: 2)    // hashValue = 2

dictionary[ok] = "some text"
dictionary[collision1] = "other text"
dictionary[collision2] = "more text"

print(dictionary) // [MyStructure(id: 2): more text, MyStructure(id: 0): some text]
print(dictionary.count) // 2

相等的哈希值导致 collision1 键被 collision2 键覆盖。没有警告。如果这样的碰撞在有 100 个键的字典中只发生一次,那么它很容易被遗漏。 (我花了很长时间才注意到这个问题。)

字典文字存在明显问题

但是,如果我用字典文字重复此操作,问题就会变得更加明显,因为会引发致命错误。

let ok = MyStructure(id: 0)            // hashValue = 0
let collision1 = MyStructure(id: 1)    // hashValue = 2
let collision2 = MyStructure(id: 2)    // hashValue = 2

let dictionaryLiteral = [
    ok : "some text",
    collision1 : "other text",
    collision2 : "more text"
]
// fatal error: Dictionary literal contains duplicate keys

问题

我的印象是 hashValue 没有必要总是 return 一个唯一值。例如,Mattt Thompson says

One of the most common misconceptions about implementing a custom hash function comes from ... thinking that hash values must be distinct.

尊敬的 SO 用户 @Gaffa says 处理哈希冲突的一种方法是

Consider hash codes to be non-unique, and use an equality comparer for the actual data to determine uniqueness.

在我看来,在撰写本文时,问题 尚未得到充分回答。

阅读 Swift Dictionary 问题 后,我假设 Swift 自动处理了与 Dictionary 的散列冲突。但显然,如果我使用自定义 class 或结构,则情况并非如此。

让我觉得答案在于 Equatable 协议是如何实现的,但我不确定我应该如何改变它。

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

这个函数是在每次查找字典键时调用,还是仅在出现散列冲突时调用? (更新:见

当(且仅当)哈希冲突发生时,我应该如何确定唯一性?

我认为您已经拥有了所需的所有拼图——只需将它们拼凑起来即可。你有很多很棒的资源。

哈希冲突没问题。如果发生哈希冲突,将检查对象是否相等(仅针对具有匹配哈希的对象)。出于这个原因,对象的 Equatable 一致性需要基于 hashValue 以外的东西,除非您确定哈希不会冲突。

这就是符合 Hashable 的对象也必须符合 Equatable 的确切原因。 Swift 需要一种更特定于域的比较方法,以便在散列法无法解决问题时使用。

在同一个 NSHipster article 中,您可以看到 Mattt 如何在他的示例 Person class 中实现 isEqual:hash。具体来说,他有一个 isEqualToPerson: 方法来检查一个人的其他属性(生日、全名)以确定是否相等。

- (BOOL)isEqualToPerson:(Person *)person {
  if (!person) {
    return NO;
  }

  BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
  BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

  return haveEqualNames && haveEqualBirthdays;
}

他在检查相等性时不使用散列值 - 他使用他个人特有的属性 class。

同样,Swift 不允许您简单地使用 Hashable 对象作为字典键——隐含地,通过协议继承——键也必须符合 Equatable。对于标准库 Swift 类型,这已经得到处理,但对于您的自定义类型和 class,您必须创建自己的 == 实现。这就是为什么 Swift 不会自动处理与自定义类型的字典冲突 - 您必须自己实现 Equatable

作为一个离别的想法,马特还指出,您通常可以只进行身份检查以确保您的两个对象位于不同的内存地址,从而确保不同的对象。在 Swift 中,会像这样:

if person1 === person2 {
    // ...
}

这里不能保证person1person2有不同的属性,只是它们在内存中占据不同的space。相反,在早期的 isEqualToPerson: 方法中,不能保证具有相同姓名和出生日期的两个人实际上是同一个人。因此,您必须考虑什么对您的特定对象类型有意义。同样,Swift 未在自定义类型上为您实现 Equatable 的另一个原因。

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

Note the global function to overload the equality operator (==) in order to conform to the Equatable Protocol, which is required by the Hashable Protocol.

您的问题是不正确的平等实现。

哈希 table(例如 Swift 字典或集合)需要单独的 equalityhash实施。

hash 让你接近你正在寻找的对象; equality 为您提供您正在寻找的确切对象。

您的代码对 hashequality 使用相同的实现,这将保证发生冲突。

要解决此问题,请实现 equality 以匹配精确的对象值(但是您的模型定义了相等性)。例如:

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.id == rhs.id
}

the equal hash values cause the collision1 key to be overwritten by the collision2 key. There is no warning. If such a collision only happened once in a dictionary with 100 keys, then it could easily be missed.

哈希冲突与此无关。 (哈希冲突永远不会影响结果,只会影响性能。)它完全按照记录工作。

Dictionary 操作对键的 equality (==) 起作用。字典不包含重复的键(意思是相同的键)。当您使用键设置值时,它会覆盖任何包含相同键的条目。当你得到一个带有下标的条目时,它会找到一个键值等于,不一定相同,你给的东西。等等。

根据您定义 == 运算符的方式,

collision1collision2 相等 (==)。因此,设置键 collision2 的条目必须覆盖键 collision1.

的任何条目

P.S。同样的事情也适用于其他语言的词典。例如,在 Cocoa 中,NSDictionary 不允许重复键,即 isEqual: 的键。在 Java 中,Maps 不允许重复键,即 .equals().

的键

你可以看到我对本页回答的评论和回答。我认为所有答案仍然以非常混乱的方式编写。

tl;dr 0) 您不需要编写实现 isEqual 即 == 在哈希值之间。 1) 仅 provide/return hashValue2) 只需像往常一样实施 Equatable

0) 为了符合 hashable 你必须有一个名为 hashValue 的计算值并给它一个合适的值。 equatable 协议不同,hashValues 的比较 已经 存在。你不要需要写:

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
    // Snippet A
}

1) 然后它使用 hashValue 检查该哈希值的索引(通过其对数组计数的模计算)是否存在正在查找的键或不。它在该索引的 key/value 对数组中查找。

2) 但是作为故障保险,即万一匹配哈希你回到常规 == 函数。 (逻辑上你需要它是因为故障安全。但你也需要它是因为 Hashable 协议符合 Equatable,因此你必须为 == 编写一个实现。否则你会得到一个编译器错误)

func == (lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.id == rhs.id
    //Snippet B
}

结论:

必须包含代码段 B,排除代码段 A,同时还有 hashValue 属性