hash functions
multi-set integers
data structures
algorithm design
computer science

What is a good hash function for a collection i.e., multi-set of integers?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Hashing a multi-set of integers (collection where order does not matter but multiplicity does) requires a commutative design. Standard sequence hashing fails because [1, 2, 2] and [2, 1, 2] should produce the same hash, while [1, 2] and [1, 2, 2] must differ. A good multi-set hash should be order-independent, sensitive to counts, and have low collision rates for your expected input distribution.

No hash can eliminate collisions entirely, but you can choose a construction with strong practical behavior. The right design depends on whether you need in-memory hash table keys, persistent fingerprints, or adversarial resistance.

Core Sections

1. Start from element-level strong mixing

First hash each integer with a robust 64-bit mixing function. This prevents low-entropy integer patterns from propagating into the collection hash.

python
1def mix64(x: int) -> int:
2    x &= 0xFFFFFFFFFFFFFFFF
3    x ^= (x >> 30)
4    x *= 0xBF58476D1CE4E5B9
5    x &= 0xFFFFFFFFFFFFFFFF
6    x ^= (x >> 27)
7    x *= 0x94D049BB133111EB
8    x &= 0xFFFFFFFFFFFFFFFF
9    x ^= (x >> 31)
10    return x

This SplitMix-style mixer is fast and has good avalanche properties for non-cryptographic use.

2. Use commutative accumulation with multiplicity awareness

A simple XOR is commutative but weak with duplicates (h ^ h = 0). Instead combine multiple commutative statistics:

python
1def multiset_hash(values):
2    MOD = 0xFFFFFFFFFFFFFFFF
3    s1 = 0  # sum of mixed hashes
4    s2 = 0  # sum of squared mixed hashes
5    c = 0   # count
6
7    for v in values:
8        h = mix64(v)
9        s1 = (s1 + h) & MOD
10        s2 = (s2 + (h * h & MOD)) & MOD
11        c += 1
12
13    # final non-commutative mix of commutative aggregates
14    out = mix64(s1 ^ ((s2 << 1) & MOD) ^ mix64(c))
15    return out

This remains order-independent and distinguishes many cases where simple XOR/sum would collide.

3. Sorting-based canonical hashing for stronger guarantees

If O(n log n) is acceptable, canonicalize by sorting and then sequence-hash the sorted values. This captures multiplicity exactly.

python
1def multiset_hash_sorted(values):
2    arr = sorted(values)
3    h = 1469598103934665603  # FNV offset basis
4    for v in arr:
5        x = mix64(v)
6        h ^= x
7        h = (h * 1099511628211) & 0xFFFFFFFFFFFFFFFF
8    return h

This is often the best practical choice when n is moderate and collision risk matters.

4. Consider adversarial vs non-adversarial contexts

For internal caches on trusted data, non-crypto 64-bit hashing is usually enough. For untrusted user input where collision attacks matter, use keyed hashing (for example SipHash variants) and, if necessary, 128-bit outputs.

5. Track collision impact in system design

Even good hashes collide eventually. Use equality checks after hash match for correctness. Do not use hash alone as proof of multiset equality unless your application tolerates probabilistic errors.

Common Pitfalls

  • Using plain XOR as the only combiner, which collapses duplicate elements badly.
  • Forgetting multiplicity, causing sets and multisets to hash the same way.
  • Assuming order-independent hash means collision-free identity.
  • Choosing weak integer mixers that preserve patterns from small numbers.
  • Skipping equality verification after hash match in correctness-critical code.

Summary

A solid multi-set hash needs two layers: strong per-element mixing and an order-independent combiner that preserves counts. For many systems, sorting then sequence hashing offers the clearest correctness model. For linear-time hashing, combine multiple commutative aggregates (sum, squared sum, count) and apply a strong finalizer. Match the design to your threat model, and always treat hashes as indexing aids, not absolute identity proofs.

A practical way to keep this issue from returning is to turn the fix into a lightweight runbook. Capture the exact environment assumptions (tool versions, runtime flags, cluster or platform settings, and required dependencies), then store a short verification command sequence that any teammate can run from a clean setup. This makes troubleshooting deterministic instead of person-dependent and reduces rework during on-call incidents.

It also helps to add one automated guardrail in CI or pre-deploy checks that validates the critical assumption described above. That guardrail might be a linter rule, a smoke test, a schema check, a policy validation step, or a minimal integration test. When the same class of failure is caught before release, teams spend less time on emergency debugging and more time on controlled improvements.


Course illustration
Course illustration

All Rights Reserved.