Java中多集的高效哈希码

时间:2011-09-16 00:07:50

标签: java guava hashcode multiset

我已经定义了java.util.Collection的子接口,它实际上是一个多重集(也就是包)。它可能不包含null个元素,尽管这对我的问题并不重要。接口定义的等于合同正如您所期望的那样:

  • obj instanceof MyInterface
  • obj包含与this相同的元素(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)

我的问题:什么是有效的哈希码计算?在我的情况下,元素的计数不能保证很便宜。

4 个答案:

答案 0 :(得分:2)

如果数量很贵,请不要这样做。你知道吗贵吗?您始终可以编写多个实现代码,并使用您希望代表应用程序的数据来描述其性能。那么你知道答案而不是猜测。

至于为何使用XOR,see 'Calculating Aggregate hashCodes with XOR'

答案 1 :(得分:2)

  

令我感到奇怪的是,一个空的Multiset会有哈希码0

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

  

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

你的效率更高(这意味着更快地计算),在效果方面也不是那么糟糕(这意味着让效果更好)。如果我理解正确,它会添加所有元素的哈希码(重复元素被添加两次)。这正是常规Set的作用,因此如果没有重复项,则获得与Set相同的hashCode,这可能是一个优点(如果您修复空集以使hashCode为0,而不是1)。

谷歌的版本有点复杂,我想是为了避免一些频繁的碰撞。当然,它可能会导致其他一些被认为不那么频繁发生的碰撞。

特别是,使用XOR在整个可用范围内传播hashCodes,即使单个输入hashCodes没有(例如,它们不是针对有限范围内的整数,这是一种常见的用例)。 p>

考虑Set [1,2,3]的hashCode。它可能与类似的集碰撞,例如[6],[4,2],[5,1]。在那里投掷一些XOR有帮助。如果有必要并且值得花费额外费用,那就必须做出权衡。

答案 2 :(得分:2)

更新

  

比方说,例如,在我们有一个我们想要作为多重集处理的数组的情况下。

因此,您必须处理所有条目,不能使用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次碰撞)。但是,我并不认为我的榜样与现实生活息息相关。

答案 3 :(得分:1)

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