【问题标题】:Efficient hash code for multiset in JavaJava中多重集的高效哈希码
【发布时间】:2011-11-18 07:49:13
【问题描述】:

我已经定义了java.util.Collection 的子接口,它实际上是一个多重集(又名包)。它可能不包含 null 元素,尽管这对我的问题并不重要。接口定义的equals合约如你所料:

  • obj instanceof MyInterface
  • obj 包含与 this 相同的元素(by equals
  • obj 每个元素包含相同数量的重复项
  • 元素的顺序被忽略

现在我想编写我的hashCode 方法。我最初的想法是:

int hashCode = 1;
for( Object o : this ) {
    hashCode += o.hashCode();
}

但是,我注意到com.google.common.collect.Multiset(来自 Guava)将哈希码定义如下:

int hashCode = 0;
for( Object o : elementSet() ) {
    hashCode += ((o == null) ? 0 : o.hashCode()) ^ count(o);
}

让我感到奇怪的是,一个空的 Multiset 的哈希码为 0,但更重要的是,我不理解 ^ count(o) 的好处,而不是简单地将每个副本的哈希码相加。也许是关于不多次计算相同的哈希码,但那为什么不* count(o)

我的问题:什么是有效的哈希码计算?在我的例子中,一个元素的数量不能保证很便宜。

【问题讨论】:

  • XOR '^' 分配值;加法不太好。
  • @Mitch:稍微充实一下并将其作为答案发布。
  • @Mitch Wheat:我非常不同意,XOR 和加法之间的唯一区别是进位的存在。由于计数通常很小,因此不能产生好的哈希码。像* (2*count(o)+1) 这样的东西应该会更好。 PS:我还没试过。
  • 所以我尝试了一个相当人为的例子,其中 Guava 的 hashCode 表现非常糟糕(我修改后的想法很出色)。看我的回答(不要太认真)。 @Rinke:改用entrySet 怎么样?
  • @maaartinus:你可以不同意任何你想要的,但散列值的分布是使用 XOR 的主要原因。

标签: java guava hashcode multiset


【解决方案1】:

更新

例如,假设我们有一个想要作为多重集处理的数组。

因此,您必须在所有条目出现时对其进行处理,不能使用count,也不能假设条目以已知顺序出现。

我会考虑的一般功能是

int hashCode() {
    int x = INITIAL_VALUE;
    for (Object o : this) {
        x = f(x, o==null ? NULL_HASH : g(o.hashCode()));
    }
    return h(x);
}

一些观察:

  • 正如其他答案中所述,INITIAL_VALUE 并不重要。
  • 我不会选择NULL_HASH=0,因为这会忽略空值。
  • 函数g 可用于您希望成员的哈希值在一个小范围内(例如,如果它们是单个字符,则可能发生这种情况)。
  • 函数h 可以用来改善结果,这不是很重要,因为这已经发生了,例如在HashMap.hash(int)
  • 函数f 是最重要的一个,不幸的是,它非常有限,因为它显然必须是关联的和可交换的。
  • 函数f 在两个参数中都应该是双射的,否则会产生不必要的冲突。

在任何情况下我都不会推荐f(x, y) = x^y,因为它会使一个元素出现两次以抵消。使用加法更好。类似的东西

f(x, y) = x + (2*A*x + 1) * y

其中A 是一个满足上述所有条件的常数。这可能是值得的。 对于A=0,它退化为加法,使用偶数A 不好,因为它会将x*y 的位移出。 使用A=1 很好,表达式2*x+1 可以使用x86 架构上的单个指令来计算。 如果成员的哈希分布不均,使用更大的奇数 A 可能会更好。

如果您选择重要的hashCode(),您应该测试它是否正常工作。你应该衡量你的程序的性能,也许你会发现简单的加法就足够了。否则,我会选择 NULL_HASH=1g=h=identityA=1

我的旧答案

这可能是出于效率原因。调用count 对于某些实现来说可能代价高昂,但可以使用entrySet 代替。不过可能更贵,我不知道。

我为 Guava 的 hashCode 和 Rinke 的以及我自己的提议做了一个简单的碰撞基准测试:

enum HashCodeMethod {
    GUAVA {
        @Override
        public int hashCode(Multiset<?> multiset) {
            return multiset.hashCode();
        }
    },
    RINKE {
        @Override
        public int hashCode(Multiset<?> multiset) {
            int result = 0;
            for (final Object o : multiset.elementSet()) {
                result += (o==null ? 0 : o.hashCode()) * multiset.count(o);
            }
            return result;
        }
    },
    MAAARTIN {
        @Override
        public int hashCode(Multiset<?> multiset) {
            int result = 0;
            for (final Multiset.Entry<?> e : multiset.entrySet()) {
                result += (e.getElement()==null ? 0 : e.getElement().hashCode()) * (2*e.getCount()+123);
            }
            return result;
        }
    }
    ;
    public abstract int hashCode(Multiset<?> multiset);
}

碰撞计数代码如下:

private void countCollisions() throws Exception {
    final String letters1 = "abcdefgh";
    final String letters2 = "ABCDEFGH";
    final int total = letters1.length() * letters2.length();
    for (final HashCodeMethod hcm : HashCodeMethod.values()) {
        final Multiset<Integer> histogram = HashMultiset.create();
        for (final String s1 : Splitter.fixedLength(1).split(letters1)) {
            for (final String s2 : Splitter.fixedLength(1).split(letters2)) {
                histogram.add(hcm.hashCode(ImmutableMultiset.of(s1, s2, s2)));
            }
        }
        System.out.println("Collisions " + hcm + ": " + (total-histogram.elementSet().size()));
    }
}

打印出来

Collisions GUAVA: 45
Collisions RINKE: 42
Collisions MAAARTIN: 0

所以在这个简单的例子中,Guava 的 hashCode 表现非常糟糕(可能发生 63 次冲突,其中 45 次发生冲突)。但是,我并不认为我的例子与现实生活有很大的相关性。

【讨论】:

  • 有趣的答案。你是怎么想出你的哈希函数的?它似乎工作得很好,但它背后的数学原理是什么?如果计数很昂贵,您会建议什么? (例如,假设我们有一个想要将其视为多重集的数组。)
  • 数学只是一个简单的观察:XOR 和 ADD 非常相似,乘法使其更好。但是,计算x*y 不适合混合xy。像(2*x+1)*(2*xy+1) 这样的东西通常效果很好,使用更大的常量(代替 1)可以几乎免费地防止一些冲突。
  • 在我上面的评论中,它应该是(2*x+1)*(2*y+1),但这并不是很好,因为结果总是很奇怪。但是,这里的关键是乘以奇数。使用与2*x*y+x+y+1 相同的x+(2*x+1)*y 很好,将一个术语乘以一个奇数可以更好地处理较小的xy
【解决方案2】:

如果计数很昂贵,请不要这样做。你知道它贵吗?您始终可以编写多个实现并使用您希望代表您的应用程序的数据来分析它们的性能。然后你会知道答案而不是猜测。

至于你为什么使用异或,see 'Calculating Aggregate hashCodes with XOR'

【讨论】:

    【解决方案3】:

    让我感到奇怪的是,一个空的 Multiset 的哈希码为 0

    为什么?所有空集合可能都有哈希码 0。即使没有,它也必须是一个固定值(因为所有空集合都是相等的),那么 0 有什么问题?

    什么是有效的哈希码计算?

    你的效率更高(这意味着计算速度更快),在有效性方面也不算太差(这意味着产生效果很好的结果)。如果我理解正确,它会将所有元素的哈希码相加(重复元素被添加两次)。这正是常规 Set 所做的,因此如果您没有重复项,您将获得与 Set 相同的 hashCode,这可能是一个优势(如果您将空集修复为 hashCode 0,而不是 1)。

    Google 的版本稍微复杂一些,我想是为了避免一些其他频繁的冲突。当然,它可能会导致其他一些被认为不太频繁发生的碰撞。

    特别是,使用 XOR 将 hashCodes 分布在整个可用范围内,即使单个输入 hashCodes 没有(例如,对于有限范围内的 Integers 不这样做,这是一个常见的用例)。

    考虑 Set [ 1, 2, 3] 的 hashCode。是 6。可能与相似的 Set 发生碰撞,例如 [6]、[4, 2]、[5, 1]。在那里投入一些 XOR 会有所帮助。如果这是必要的并且值得额外的费用是你必须做出的权衡。

    【讨论】:

    • 0 让我觉得很奇怪,因为我认为将它保留用于散列空值是一种很好的做法,仅此而已。当然没有错。
    • 你完全理解我的哈希函数。如果我理解正确,与 XOR 方法相比,它会(或可能)导致更多的冲突。恐怕我真的不明白为什么。 (HashMap 没有对此进行优化吗?)我也不明白为什么添加通常会更有效。在我的特殊情况下可能是这样,但如果获取计数很便宜,我会说 XOR 方法更有效。
    • 很抱歉投了反对票,但所有集合的 hashCode 不为零,所有空集合都不相等,并且 Collection 接口不暗示 Object equals 子句的任何附加子句。 list 和 set 接口可以,但是 list 和 set 永远不可能相等,因此空 list 和空 set 不相等。
    【解决方案4】:

    我观察到 java.util.Map 使用或多或少相同的逻辑:java.util.Map.hashCode() 指定返回 map.entrySet().hashCode(),而 Map.Entry 指定其 hashCode () 是 entry.getKey().hashCode() ^ entry.getValue().hashCode()。接受从 Multiset 到 Map 的类比,这正是您所期望的 hashCode 实现。

    【讨论】:

      猜你喜欢
      • 2017-02-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-16
      • 1970-01-01
      • 2010-12-17
      • 2015-02-04
      • 1970-01-01
      相关资源
      最近更新 更多