如何形成String键以获得最统一的哈希码分发

时间:2017-01-06 22:45:55

标签: java hash hashmap hashcode

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

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

首先,我想知道如何从具有不同可变性/随机性的子串构建密钥,以获得最统一的哈希码分布。最随机的位应该是第一个,还是最后一个,或者......?

其次,我想知道长度如何影响从HashMap获取对象的时间。例如,如果我将密钥长度加倍,那么对象检索需要两倍的时间吗?或者哈希码的计算只占用了那个时间的一小部分,因为从HashMap的桶中获取对象的过程需要更长的时间?

4 个答案:

答案 0 :(得分:1)

底线:您应该使用hashCode类提供的标准String方法...但因为订单不是&# 39;无所谓。

(事实上,如果你说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个符合我上述假设的随机字符串上进行了测试。

计算为A时的HashCode分布然后是B然后C: HashCode distribution when computed as A then B then C

计算为C时的HashCode分布,然后是B,然后是A: HashCode distribution when computed as C then B then A

答案 1 :(得分:1)

您是否仅为了在HashMap中使用密钥而制作密钥?如果是这样,那么你甚至不必去做。您可以将对象直接放在HashMap中,但必须覆盖方法hashCode()equals()

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

请参阅下面的示例代码。

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

从散列映射访问元素的速度取决于负载因子(即散列映射的有效程度)。如果散列图被轻载(大多数桶中有零个或一个元素),则几乎可以获得持续时间O(1)进行访问。如果散列图负载很重(大多数存储桶有很多元素),那么性能会显着降低。

示例代码

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);
    }

}

答案 2 :(得分:1)

String在字符串开头与字符串末尾的变异性是否相关无关紧要。

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

代码将创建60个字符串,这些字符串仅由第一个或最后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);
    }
}

答案 3 :(得分:0)

订单不会影响散列键的分发。所有字符都具有相同的&#34;权重&#34;。

密钥越长,计算散列所需的时间就越多,BUT一旦创建了hashCode就重用了hashCode,因此如果重用相同的String,hashCode只会生成一次。

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

  1. 创建在构造函数中接受A,B,C的不可变类,并在构造函数中计算哈希值。
  2. 使hashCode从构造函数返回哈希值。
  3. 如果可能,请重复使用该类的实例,这样每次访问地图时都不需要重新计算哈希码。
  4. 别忘了重写等于。
  5. 即使您没有重复使用该对象,它也是一种更好的方法,因为它封装了哈希逻辑。但如果对象被重用,真正的好处就来了。