具有可互换属性的可散列结构?

Posted

技术标签:

【中文标题】具有可互换属性的可散列结构?【英文标题】:Hashable struct with interchangeable properties? 【发布时间】:2019-07-09 04:26:22 【问题描述】:

我需要使自定义结构符合Hashable,以便我可以将其用作字典键类型。不过,挑战在于结构的两个属性可以互换,以便识别唯一的实例。

这里有一个简化的例子来说明问题:

struct MultiplicationQuestion 
    let leftOperand: Int
    let rightOperand: Int
    var answer: Int  return leftOperand * rightOperand 

识别唯一MultiplicationQuestion 的两个重要属性是leftOperandrightOperand,但它们的顺序无关紧要,因为“1 x 2”与“2 x”本质上是同一个问题1'。 (由于我不会在这里讨论的原因,它们需要作为单独的属性保存。)

我尝试如下定义Hashable 一致性,因为我知道我为== 定义的相等性与内置哈希器将要做什么之间存在冲突:

extension MultiplicationQuestion: Hashable 
    static func == (lhs: MultiplicationQuestion, rhs: MultiplicationQuestion) -> Bool 
        return (lhs.leftOperand == rhs.leftOperand && lhs.rightOperand == rhs.rightOperand) || (lhs.leftOperand == rhs.rightOperand && lhs.rightOperand == rhs.leftOperand)
    
    func hash(into hasher: inout Hasher) 
        hasher.combine(leftOperand)
        hasher.combine(rightOperand)
    

我通过创建两组问题并对它们执行各种操作来对此进行测试:

var oneTimesTables = Set<MultiplicationQuestion>()
var twoTimesTables = Set<MultiplicationQuestion>()
for i in 1...5 
    oneTimesTables.insert( MultiplicationQuestion(leftOperand: 1, rightOperand: i) )
    twoTimesTables.insert( MultiplicationQuestion(leftOperand: 2, rightOperand: i) )


let commonQuestions = oneTimesTables.intersection(twoTimesTables)
let allQuestions = oneTimesTables.union(twoTimesTables)

希望的结果(一厢情愿)是 commonQuestions 包含一个问题 (1 x 2),而 allQuestions 包含九个问题,已删除重复项。

然而,实际结果是不可预测的。如果我多次运行操场,我会得到不同的结果。大多数时候,commonQuestions.count 是 0,但有时是 1。大多数时候,allQuestions.count 是 10,但有时是 9。(我不确定我在期待什么,但这种不一致是当然是惊喜!)

如何使hash(into:) 方法为属性相同但相反的两个实例生成相同的哈希?

【问题讨论】:

【参考方案1】:

这就是 Hasher 的工作原理

https://developer.apple.com/documentation/swift/hasher

但是,底层哈希算法旨在展示 雪崩效应:种子或输入字节的细微变化 序列通常会在生成的哈希中产生剧烈的变化 价值。

所以问题在 hash(into:) func 中

由于序列很重要combine 操作不可交换。您应该找到一些其他函数作为此结构的哈希。在你的情况下,最好的选择是

    func hash(into hasher: inout Hasher) 
        hasher.combine(leftOperand & rightOperand)
    

正如@Martin R 指出的那样,冲突更少,最好使用^

    func hash(into hasher: inout Hasher) 
        hasher.combine(leftOperand ^ rightOperand)
    

【讨论】:

谢谢@Tiran。不幸的是,answer 不会始终如一地确定一个独特的问题……2 x 5 和 1 x 10 都会给出相同的答案,但它们是不同的问题。 leftOperand &amp; rightOperand 可能更有希望!二进制操作让我很头疼,但我会看看。 (在等待答案的同时,我也想知道为什么我不尝试将操作数组合到一个 Set 或一个排序的数组中。我也准备测试这个想法。) hasher.combine(leftOperand ^ rightOperand) 稍微减少碰撞。 @Kal:散列值不需要(并且通常不能)唯一。 假设你有 f 这是一些哈希函数。这意味着f(a) != f(b) =&gt; a != b。但是f(a) == f(b) 并不意味着a == bf(a) == f(b)a != b 的那一刻称为碰撞。一个函数的碰撞次数不应该影响对象之间的比较结果,它只会影响找到它们之间的差异的速度。所以碰撞并不意味着不好的比较。在您的情况下,任何建议的功能都可以正常工作,但 ^ 会因为更少的冲突而稍微快一些。 我相信它是这样发生的)在您的示例中,您可以将 hash 替换为 hasher.combine(2) 并且您会看到结果没有改变,不同之处在于性能【参考方案2】:

Tiran Ut's answer(和 cmets)对我帮助很大,我已将其标记为正确。尽管如此,我认为值得添加另一个答案来分享我学到的一些东西并提出另一种解决问题的方法。

Apple 的 hash(into:) documentation 说:

用于哈希的组件必须与组件相同 在您的类型的 == 运算符实现中进行比较。

如果它是简单的一对一属性比较(就像所有代码示例所示!),那就太好了,但是如果您的 == 方法具有像我这样的条件逻辑呢?您如何将其转换为一个(或多个)值以供散列器使用?

我一直在纠结这个细节,直到 Tiran 建议给哈希器提供一个恒定值(如 2)仍然可以工作,因为无论如何== 解决了哈希冲突。当然,您不会在生产环境中这样做,因为您会失去哈希查找的所有性能优势,但对我来说,如果您不能准确制作哈希参数与您的 == 操作数相同,使哈希相等逻辑更多具有包容性(而不是更少)。

Tiran Ut 的答案中的解决方案有效,因为按位运算不关心操作数的顺序,就像我的 == 逻辑一样。有时,两个完全不同的对可能会生成相同的值(导致有保证的哈希冲突),但在这些情况下唯一真正的后果是对性能的小幅影响。

最后,我意识到我可以在这两种情况下使用完全相同的逻辑,从而避免哈希冲突——好吧,除了由不完美的哈希算法引起的任何冲突。我给MultiplicationQuestion添加了一个新的私有常量,并初始化如下:

uniqueOperands = Set([leftOperand, rightOperand])

排序数组也可以,但 Set 似乎是更优雅的选择。由于 Set 没有顺序,我对 == 的详细条件逻辑(使用 &amp;&amp;||)已经巧妙地封装在 Set 类型中。

现在,我可以使用相同的值来测试相等性并提供哈希:

static func ==(lhs: MultiplicationQuestion, rhs: MultiplicationQuestion) -> Bool 
    return lhs.uniqueOperands == rhs.uniqueOperands


func hash(into hasher: inout Hasher) 
    hasher.combine(uniqueOperands)

我已经测试了性能,它与按位运算相当。不仅如此,我的代码在这个过程中也变得更加简洁易读。似乎是双赢。

【讨论】:

以上是关于具有可互换属性的可散列结构?的主要内容,如果未能解决你的问题,请参考以下文章

一个协议的可散列协议

Python数据结构与算法---统计可散列的对象Counter

Python数据结构与算法---统计可散列的对象Counter

Swift 5.+ - 使类可散列?

在 SQL 版本之间可互换的 Linq dbml

具有可互换持久层的应用程序