【问题标题】:How to form String keys to get the most uniform hash code distribution如何形成 String 键以获得最均匀的哈希码分布
【发布时间】:2017-01-06 22:45:55
【问题描述】:

我想在 HashMap 中存储大量对象。识别每个对象的关键是一个字符串,它总是由 3 个部分/子字符串组成,为简单起见,我将它们命名为 A、B 和 C。A 具有高可变性,B 平均可变性和 C 低可变性 .有多种组合方式:

key = A + "_" + B + "_" + C;
key = A + "_" + C + "_" + B;
key = B + "_" + A + "_" + C;
...

首先,我想知道应该如何从具有不同可变性/随机性的子字符串中构建密钥,以获得最均匀的哈希码分布。最随机的位应该先出现,还是最后出现,还是......?

其次,我想知道key的长度如何影响从HashMap中获取对象的时间。例如,如果我将密钥长度加倍,对象检索是否需要两倍的时间?还是说计算哈希码只需要一小部分时间,因为从 HashMap 的桶中获取对象的过程需要更长的时间?

【问题讨论】:

  • 也许有一些有用的阅读stackoverflow.com/questions/9364134/…
  • 这里的成本/收益非常不平衡,您应该只使用最易读的密钥
  • 无法预测任意字符串的哈希码分布。任何订单都可以。
  • @Andreas 如果您对子字符串的分布不做任何假设,则为 true。然而,鉴于 xpages-noob 知道子字符串分布的可变性这一事实,他可以使用该知识来制作更好的散列函数。以我的回答为例。
  • @AustinD 你不能覆盖String 的哈希函数。键是字符串,而不是其他对象。

标签: java hash hashmap hashcode


【解决方案1】:

底线:您应该使用String 类提供的标准hashCode 方法...但不要,因为顺序无关紧要。

(实际上,如果你说 C 的可变性最高,A 的可变性最低,那么java.lang.String.hashCode 的性能将是可怕的!)

带走:给定关于Object 成员的附加信息,散列顺序对密钥的分布有很大影响。

通常,在没有任何特定领域知识的情况下,最好选择可读性和成熟库的可靠性来解决此类问题。但是,由于您对子字符串的分布有特定的了解,因此您可以对 hashFunction 做出更明智的决定。

为了演示,假设 A 部分可以取任何字符值,B 部分只取前 15 个字符,C 部分只取前 5 个字符。并假设您通过以下方式覆盖 hashCode 方法:

@Override
public int hashCode(){
    final int constant = 37;
    final String partA = getPartA(myString);
    final String partB = getPartB(myString);
    final String partC = getPartC(myString);
    int total = 17;
    total= total * constant + partA;
    total= total * constant + partB;
    total= total * constant + partC;

    return total;

}

我们希望这种方法的字符串几乎均匀随机分布。但是,如果我们要反转以下行:

    total= total * constant + partC; //formerly part A
    total= total * constant + partB;
    total= total * constant + partA; //formerly part C

我们只会在值范围的前半部分生成代码。以下是在 15,000 个随机字符串上测试的一些实验结果,这些结果符合我上述假设。

HashCode 分布当计算为 A 然后 B 然后 C:

计算为 C 然后 B 然后 A 时的 HashCode 分布:

【讨论】:

  • 无论你是做ABC还是CBA,拼接字符串的长度都是一样的,计算hashCode的时间也是一样的。没有性能差异,哈希码分布很可能相似。
  • @Andreas 但是我们并没有在这里连接整个字符串......我们正在独立处理每个子字符串。即使我们将其视为单个字符串...鉴于所述分布,使用 String.hashCode() 仍然会比使用 new StringBuffer(string).reverse().toString().hashCode() 获得更好的分布
  • 在这么短的时间内给出这么详细的答案。感谢和尊重。
  • @AustinD 但这不是问题所在。问题是字符串部分的顺序是否对连接字符串的哈希码分布质量很重要。你假设你可以不用串联,但你不能,因为OP needs a concatenated string。你假设的前提是错误的。不过,否则会是一个很好的答案。
  • @Andreas 是的,但是这种行为在串联形式中仍然存在……只是在非串联形式中更容易演示。
【解决方案2】:

您是否只是为了在HashMap 中使用它而制作密钥?如果是这样,那么您甚至不必成功。您可以将对象直接放在HashMap 中,但您必须重写方法hashCode()equals()

好消息是——您的 IDE(例如 Eclipse)可以为您生成 hashCode()equals() 的建议代码。 (在 Eclipse 中,Source>Generate hashCode() and equals() ...)。你可以从那里接受它的建议。

请参阅下面的示例代码。

我倾向于认为计算速度非常快。但是如果您担心速度,并且如果三个字段/部分/子字符串是不可变的,那么您可以在构造函数中计算 hashCode,就像我在示例代码中所做的那样。

从 hashmap 访问元素的速度取决于负载因子(即 hashmap 的填充程度)。如果 hashmap 负载很轻(大多数存储桶中有零个或一个元素),则访问的时间几乎是恒定的 O(1)。如果 hashmap 负载很重(大多数桶有很多元素),那么性能会显着降低。

示例代码

package StringKeyForHashMap;

import java.util.HashMap;
import java.util.Map;

public class Thing {
    private final String    a;
    private final String    b;
    private final String    c;
    private final int       hashCode;


    public Thing(String a, String b, String c) {
        super();
        this.a = a;
        this.b = b;
        this.c = c;
        this.hashCode = computeHashCode();
    }


    @Override
    public int hashCode() {
        return this.hashCode;
    }

    private int computeHashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((a == null) ? 0 : a.hashCode());
        result = prime * result + ((b == null) ? 0 : b.hashCode());
        result = prime * result + ((c == null) ? 0 : c.hashCode());
        return result;
    }


    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Thing other = (Thing) obj;
        if (a == null) {
            if (other.a != null)
                return false;
        } else if (!a.equals(other.a))
            return false;
        if (b == null) {
            if (other.b != null)
                return false;
        } else if (!b.equals(other.b))
            return false;
        if (c == null) {
            if (other.c != null)
                return false;
        } else if (!c.equals(other.c))
            return false;
        return true;
    }


    public static void main(String[] args) {
        /*
         * Below I assume that the value of interest is 
         * an integer
         */
        Map<Thing, Integer> map = new HashMap<>();  
        map.put(new Thing("AAA", "BBB", "CCC"), 0);
    }

}

【讨论】:

  • 感谢您的回答。由于我必须与其他非 Java 系统交换密钥,因此我更喜欢使用 String 密钥来标识对象。
【解决方案3】:

String 在字符串的开头与字符串的结尾是否具有高可变性无关紧要。

为了测试这一点,下面的代码模拟了 Java 8 的 HashMap 类的哈希表逻辑。 tableSizeForhash 方法是从 JDK 源代码中复制而来的。

代码将创建 60 个字符串,它们的区别仅在于前 7 个字符或后 7 个字符。然后它将构建一个具有适当容量的哈希表并计算哈希桶冲突的次数。

从输出中可以看出,无论被散列的字符串的前导或尾随可变性如何,冲突计数都是相同的(在统计范围内)。

输出

Count: 1000      Collisions: 384      By collision size: {1=240, 2=72}
Count: 1000      Collisions: 278      By collision size: {1=191, 2=30, 3=3, 4=3, 6=1}
Count: 100000    Collisions: 13876    By collision size: {1=12706, 2=579, 3=4}
Count: 100000    Collisions: 15742    By collision size: {1=12644, 2=1378, 3=110, 4=3}
Count: 10000000  Collisions: 2705759  By collision size: {1=1703714, 2=381705, 3=65050, 4=9417, 5=1038, 6=101, 7=3}
Count: 10000000  Collisions: 2626728  By collision size: {1=1698957, 2=365663, 3=56156, 4=6278, 5=535, 6=27, 7=4}

测试代码

public class Test {
    public static void main(String[] args) throws Exception {
        //
        test(1000, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_%07d");
        test(1000, "%07d_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
        test(100000, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_%07d");
        test(100000, "%07d_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
        test(10000000, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_%07d");
        test(10000000, "%07d_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
    }
    private static void test(int count, String format) {
        // Allocate hash-table
        final int initialCapacity = count * 4 / 3 + 1;
        final int tableSize = tableSizeFor(initialCapacity);
        int[] tab = new int[tableSize];

        // Build strings, calculate hash bucket, and increment bucket counter
        for (int i = 0; i < count; i++) {
            String key = String.format(format, i);
            int hash = hash(key);
            int bucket = (tableSize - 1) & hash;
            tab[bucket]++;
        }

        // Collect collision counts, i.e. counts > 1
        // E.g. a bucket count of 3 means 1 original value plus 2 collisions
        int total = 0;
        Map<Integer, AtomicInteger> collisions = new TreeMap<>();
        for (int i = 0; i < tableSize; i++)
            if (tab[i] > 1) {
                total += tab[i] - 1;
                collisions.computeIfAbsent(tab[i] - 1, c -> new AtomicInteger()).incrementAndGet();
            }

        // Print result
        System.out.printf("Count: %-8d  Collisions: %-7d  By collision size: %s%n", count, total, collisions);
    }
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
}

【讨论】:

  • 感谢您的回答。不幸的是,这里的每个答案都说明了一些不同的东西。我将评估不同的建议和代码示例,并在我了解更多信息时进行报告。
【解决方案4】:

顺序不影响散列键的分布。所有角色都有相同的“权重”。

key越长,计算hash的时间越长,但是String一旦创建就复用hashCode,所以如果复用同一个String,hashCode只会生成一次。

话虽如此,我建议你改变你的实现:

  1. 创建在构造函数中接受 A、B、C 的不可变类,并在构造函数中计算哈希值。
  2. 让 hashCode 从构造函数返回哈希值。
  3. 如果可能,请重用类的实例,这样您就不需要在每次访问地图时重新计算哈希码。
  4. 不要忘记覆盖等号。

即使您不重用对象,它也是一种更好的方法,因为它封装了哈希逻辑。但是,如果对象被重用,真正的好处就来了。

【讨论】:

  • 感谢您的回答。如果我理解正确,您建议使用新的不可变类的实例作为 HashMap 的键。如果是这样,这对于我需要它的项目来说真的不起作用,因为键必须是字符串,因为它们与其他非 Java 系统交换。
  • 为什么不呢?您从外部系统接收字符串,然后创建对象:new MyMapKey(A,B,C)
  • 我知道,但为了简单起见,我更愿意在两个系统上用同一个键来识别对象。
猜你喜欢
  • 2015-11-01
  • 1970-01-01
  • 2016-02-07
  • 2020-03-23
  • 2014-03-02
  • 1970-01-01
  • 1970-01-01
  • 2021-05-26
  • 2020-12-24
相关资源
最近更新 更多