Hashing a set of integers in an order independent way

你说的曾经没有我的故事 提交于 2019-12-03 12:27:15

A simple approach is to xor or add the hashes of the individual integers together. xor and add are commutative, so this satisfies order independence.

Thus:

int hc = 0;
for(int i = 0; i < n; i++) {
   hc += a[i];
}
return hc;

or

int hc = 0;
for(int i = 0; i < n; i++) {
   hc ^= a[i];
}
return hc;

because the hash code of an int is its value anyway.

In fact, this is exactly whatHashSet<Integer>.hashCode (uses add) will do. If your integers are already boxed, or you can handle boxing them, that's a built-in solution.

Assuming you need speed without the overhead of the *Set classes, then you could write H as follows:

/**
 * Hashes a set of integers.
 * 
 * @param list to hash
 * @return hash code
 */
public static int H(int list[]) {
    // XOR all the integers together.
    int hashcode = 0;
    for (int val : list) {
        hashcode ^= val;
    }
    return hashcode;
}

It is the same regardless of the order, and it is relatively efficient.

For example:

public static void main(String[] args) {
    System.out.println(Integer.toHexString(H(new int[]{0xabcd,0x1234,0x1111})));
    System.out.println(Integer.toHexString(H(new int[]{0x1234,0x1111,0xabcd})));
}

Displays:

a8e8
a8e8

This could be generalized to more than just ints by doing the following:

/**
 * Hashes a set of objects.
 * 
 * @param list to hash
 * @return hash code
 */
public static int H(Object list[]) {
    // XOR all the hashes together.
    int hashcode = 0;
    for (Object val : list) {
        hashcode ^= val.hashCode();
    }
    return hashcode;
}

The main program would then have to use arrays of Integer instead of the primitive int.

Adding the numbers should be almost as quick, and might give you a better distribution over the 32 bit range. If the elements of the set are already uniformly distributed over the range, then xor might be better.

However, with both methods, you can manufacture collisions easily with integers. For example, with the adding method;

{1000, 1001, 1002}
{0, 1, 3002}

Both of these arrays have the same H().

With the XOR method;

{0x1010, 0x0101}
{0x1111, 0x0000}

Both of these have the same H().

Similarly, the 0 element is problematic since lists will have the same hash with or without it. You can mitigate this by adding a constant value at each iteration. For example:

            ...
            hashcode += val.hashCode() + CONSTANT;
            ...

Or by including the number of elements as the initial hash code:

            ...
            // XOR all the hashes together.
            int hashcode = list.length;
            ...

You can put all the Integers in Java HashSet and use its hashCode.

On the other hand, java.util.Set does specify the following in documents:

Returns the hash code value for this set. The hash code of a set is defined to be the sum of the hash codes of the elements in the set, where the hash code of a null element is defined to be zero. This ensures that s1.equals(s2) implies that s1.hashCode()==s2.hashCode() for any two sets s1 and s2, as required by the general contract of Object.hashCode().

And Integer.hashCode() is then

a hash code value for this object, equal to the primitive int value represented by this Integer object.

Thus the hashCode for set of integers i1, i2, ... i_n in Java standard library is i1 + i2 + ... + i_n.

In case that the numbers are rather small, you can also multiply each element by a some suitably sized prime number. Knuth used 2654435761 which is too big for an java int, but you can take its 2-complement, -1640531527. Thus take C = -1640531527, and then your code is C*i1 + C*i2 + ... C*i_n.

private static final int C = -1640531527;

public static int calculateHash(int[] set) {
    int code = 0;
    for (int e: set) {
        code += C * e;
    }

    return code;
}

However there is 1 obvious flaw in the thinking. To make any use of the hashCode, you need to be able to prove that 2 sets are indeed equal, thus in any case the easiest way to prove is to sort the elements. Of course if there are substantially less than millions of sets then there are not that many collisions either.

I'd prefer summation rather then xoring, because 1) sum is used in Set's hashCode() implementation, 2) sum as approach to array hashing is recommended in Effective Java 3) it's less collision-prone. I suggest you to look at openjdk's AbstractSet implementation: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7-b147/java/util/AbstractSet.java?av=f

120    public int hashCode() {
121        int h = 0;
122        Iterator<E> i = iterator();
123        while (i.hasNext()) {
124            E obj = i.next();
125            if (obj != null)
126                h += obj.hashCode();
127        }
128        return h;
129    }

I'd also recommend making h long, and return (int) ((h & 0xffffffffL) & h >>> 32))

This is by no means trivial programming, but you could take inspiration from DES algorithm's S-boxes: by this you can achieve a good dispersion function which maps similar integers to very dissimilar ones. Then XOR-ing these dissimilar integers should no longer present a threat due to collisions.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!