Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 3 – Hashing: maps, sets and hash functions

Hash functions, universal hashing and flooding

What a hash function for a table must do, a polynomial hash for strings, how hash flooding makes O(1) lookups O(n), and why random seeds defend against it.

  • Advanced
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25 and Clang 21.0.0
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: What a hash function for a table must do · A polynomial hash for strings · Hash flooding: collisions chosen on purpose · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Write a polynomial rolling hash for strings
  • Explain universal hashing and why random seeds matter
  • Demonstrate a hash-flooding slowdown and its defences
  • Check which built-in hashes of a language are salted

Before you start

A hash function turns a key into a number of fixed size, and a hash table uses that number to choose a bucket, so the function decides whether keys spread out or pile up. Hash functions for tables must be fast, give equal keys equal hashes and spread different keys evenly. Spreading is easy to achieve for ordinary data and impossible to promise for every input: anyone who knows a fixed hash function can choose keys that all collide, and a server fed such keys turns each O(1) operation into O(n). Feeding a table keys that collide on purpose is called hash flooding. Universal hashing picks the table’s hash function at random from a family, so no fixed set of keys is bad for every run. This lesson builds a polynomial hash for strings, shows how collisions arise, how flooding works and which hashes of real languages resist it.

What a hash function for a table must do

  1. Consistent within a run: C++’s std::hash only has to agree with itself during one execution, which leaves room for salted hashes.
  2. Equal keys, equal hashes, as the previous lessons showed.
  3. Spread: different keys share a bucket about as rarely as random choices would.
  4. Fast: every insert, find and delete computes it.

Cryptographic hash functions such as SHA-256 solve a different problem: nobody should be able to find two inputs with the same output, which costs far more work per key than a table can spend. Table hashes may collide, as long as collisions stay rare for the keys the table receives.

A polynomial hash for strings

A common hash for a string s of length n treats its character codes as the digits of a number in base B:

h(s)=(s0Bn−1+s1Bn−2+⋯+sn−1)modMh(s) = \left(s_0 B^{n-1} + s_1 B^{n-2} + \dots + s_{n-1}\right) \bmod M

Horner’s rule computes it in one pass: multiply the running value by B and add the next code. M is usually a large prime and B larger than any character code, and a character’s position matters, so “ab” and “ba” hash differently.

Horner's rule on 'kochi', then two strings with the same hash Python · strings/poly_hash.py
"""A polynomial hash for strings, computed with Horner's rule, and a search for two strings with the same hash.

h(s) = s[0]·B^(n−1) + s[1]·B^(n−2) + … + s[n−1] (mod M), where s[i] is the code of the i-th character.
"""
import random


def poly_hash(text, base, mod):
    h = 0
    for ch in text:
        h = (h * base + ord(ch)) % mod
    return h


B, M = 131, 1_000_000_007
h = 0
for ch in "kochi":
    h = (h * B + ord(ch)) % M
    print(f"after {ch!r}: h = {h:,}")
print(f"poly_hash('kochi') = {poly_hash('kochi', B, M):,}")

# With a small modulus, two different strings soon share a hash: hash random six-letter strings until two collide.
small = 1_009
rng = random.Random(4)
seen = {}
count = 0
while True:
    word = "".join(rng.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(6))
    if word in seen.values():
        continue
    count += 1
    code = poly_hash(word, B, small)
    if code in seen:
        print(f"modulus {small:,}: '{seen[code]}' and '{word}' both hash to {code}, after {count} strings")
        break
    seen[code] = word

Output

after 'k': h = 107
after 'o': h = 14,128
after 'c': h = 1,850,867
after 'h': h = 242,463,681
after 'i': h = 762,742,099
poly_hash('kochi') = 762,742,099
modulus 1,009: 'picrja' and 'bgfvtk' both hash to 889, after 50 strings

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 poly_hash.py

The second part shows why M must be large: with only 1,009 possible hashes, two random six-letter strings shared one after 50 strings, close to the birthday estimate of about 40. With M = 2⁶¹ − 1 the same effect needs billions.

Hash flooding: collisions chosen on purpose

A fixed hash function is public: it is in the language’s documentation or source code. An attacker can compute, offline, thousands of keys that land in one bucket and send them as the fields of a web form or the keys of a JSON object. Each insertion then compares the new key with all the earlier ones, and n keys cost O(n²) work. Crosby and Wallach described such algorithmic complexity attacks in 2003; in 2011 the oCERT-2011-003 advisory reported hash flooding against many web platforms at once.

The same 2,000 chosen keys pile into one bucket under a fixed hash but spread out under a hash chosen at random.Keys chosen by an attacker1,024 · 1, 1,024 · 2, … 1,024 · 2,000Fixed hashk mod 1,024Hash chosen at random((a·k + b) mod p) mod 1,024Every key in bucket 0:each insertion compares withall earlier keys, O(n²) in allKeys spread out:longest bucket 3 to 6 keys,O(1) expected per insertion

Chosen keys under a fixed hash and under a randomly chosen hash

Text description of the diagram

At the top are 2,000 keys chosen by an attacker: 1,024 times 1, 1,024 times 2, and so on up to 1,024 times 2,000. Two arrows lead down from them to the two hash functions.

Under the fixed hash k mod 1,024, every one of these keys lands in bucket 0, so each insertion compares the new key with all the earlier keys, which makes O(n²) work in total.

Under a hash chosen at random when the table is created, ((a·k + b) mod p) mod 1,024, the same keys spread over the buckets; in the lesson's runs the longest bucket held 3 to 6 keys, so each insertion stays O(1) on average.

The defences fall into a few families, each in one sentence:

  • Universal hashing chooses the hash function at random from a family when the table is created, so the attacker cannot know which keys collide.
  • Keyed hashes such as SipHash mix a secret random key into every hash; CPython does this for strings and bytes.
  • Tree bins turn a long bucket into a balanced tree, as Java’s HashMap has done since JEP 180.
  • Input limits cap how many keys a single request may create, which bounds the damage.

Which hashes are salted: a check in CPython

Python’s data model says that str and bytes hashes are salted with a random value, constant within one process, to protect a dict against chosen inputs. Other types are not covered:

String hashes change from process to process; int hashes do not Python · python/salted.py
"""Which of CPython's hashes are salted: str changes from process to process, int does not.

Each line marked "process" comes from a fresh Python started with a fixed PYTHONHASHSEED, so this file prints the
same every time, while three ordinary runs would print three unpredictable values.
"""
import os
import subprocess
import sys

show = "print(hash('kochi'), hash(12345))"
for seed in ("1", "2", "3"):
    env = {**os.environ, "PYTHONHASHSEED": seed}
    out = subprocess.run([sys.executable, "-c", show], env=env, capture_output=True, text=True, check=True).stdout.split()
    print(f"process with PYTHONHASHSEED={seed}: hash('kochi') = {int(out[0]):>20}, hash(12345) = {out[1]}")

info = sys.hash_info
print(f"string algorithm: {info.algorithm}, hash width: {info.width} bits")
print(f"int hashes are taken modulo {info.modulus:,} (2**61 - 1)")
for k in (2**61 - 1, 2**61, 3 * (2**61 - 1)):
    print(f"hash({k:,}) = {hash(k)}")

Output

process with PYTHONHASHSEED=1: hash('kochi') = -4334537901555037316, hash(12345) = 12345
process with PYTHONHASHSEED=2: hash('kochi') =   245045104673291647, hash(12345) = 12345
process with PYTHONHASHSEED=3: hash('kochi') = -8355054294460551174, hash(12345) = 12345
string algorithm: siphash13, hash width: 64 bits
int hashes are taken modulo 2,305,843,009,213,693,951 (2**61 - 1)
hash(2,305,843,009,213,693,951) = 0
hash(2,305,843,009,213,693,952) = 1
hash(6,917,529,027,641,081,853) = 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 salted.py

Three processes with different PYTHONHASHSEED values gave three unrelated hashes for “kochi” and the same 12345 for the number 12345. On a 64-bit build an int’s hash is the number modulo 2⁶¹ − 1, so every multiple of 2⁶¹ − 1 hashes to 0, and a table keyed by numbers that an outsider chooses is open to flooding.

Universal hashing in brief

Carter and Wegman introduced universal hashing in 1979. A family of hash functions is universal if, for any two different keys x and y, a function picked at random from the family makes them collide with probability at most 1/m, where m is the number of buckets. One such family for integer keys takes a prime p larger than any key and random numbers a and b:

ha,b(k)=((a⋅k+b)modp)modmh_{a,b}(k) = \big((a \cdot k + b) \bmod p\big) \bmod m

The guarantee is about the random choice, not the keys: whatever keys an attacker picks in advance, each pair collides with probability at most 1/m. A random base B does the same for the polynomial string hash.

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 Which property must every hash function used by a hash table have?

    Choose one answer.

    Show the answer to question 1

    Answer: Keys that compare equal get equal hashes

    Equal keys must hash alike, or a lookup could search the wrong bucket. Collisions between different keys are allowed and handled by the table, hashes may change between runs (CPython's string hashes do), and resistance to finding collisions is what cryptographic hashes add at a much higher cost.

  2. Question 2 of 5 A polynomial hash uses M = 1,009. By the birthday estimate √(πM / 2), about how many random strings are hashed before two share a hash?

    Type a number.

    Show the answer to question 2

    Answer: 40 strings (anything from 38 to 42 counts)

    √(π × 1,009 / 2) = √1,585 ≈ 39.8, so about 40. The example's run found its first pair after 50 strings, which is within the normal spread of such a random experiment.

  3. Question 3 of 5 Why does choosing the hash function at random, when the table is created, defeat keys that were computed in advance to collide?

    Choose one answer.

    Show the answer to question 3

    Answer: The keys were chosen without knowing the function, so they collide no more often than random keys

    Precomputed collisions only work against a function the attacker knows. With universal hashing, any two keys collide with probability at most 1/m over the table's random choice, whatever keys were picked beforehand.

  4. Question 4 of 5 In CPython on a 64-bit build, which of these int keys has the hash 0?

    Choose one answer.

    Show the answer to question 4

    Answer: 2**61 - 1

    CPython hashes an int as the number modulo 261 − 1 on 64-bit builds, so 261 − 1 and its multiples hash to 0, while 2**61 hashes to 1. Those hashes are not salted, which is what makes int keys floodable.

  5. Question 5 of 5 Which of these defend a server's hash tables against hash flooding?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • A hash keyed with a random secret, such as SipHash
    • A limit on how many keys one request may create
    • Turning a long bucket into a balanced tree

    A keyed hash hides which keys collide, tree bins cap the damage in a flooded bucket for keys that can be ordered, and limits bound the size of any flood. A bigger table with the same public function still lets an attacker compute keys that share a bucket.

Universal hashing: the collision bound, proved and measured Pro: opens with a Pro, Premium or Ultimate pass

Universal hashing replaces “a good hash function” with a probability that holds for every input. This part checks that probability on keys built to defeat a fixed hash, proves it for the Carter–Wegman family, and puts numbers on random bases for string hashes.

In this part:

  • The collision bound, checked against chosen keys
  • Why the family works: a proof sketch
  • Random bases for polynomial hashes, with numbers
  • Choosing the modulus and the base in practice
  • What the bound buys: expected bucket length

3 runnable examples (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Hash flooding reproduced in Python, Java and C++, and the defences that work Pro: opens with a Pro, Premium or Ultimate pass

Flooding is easiest to believe when you watch it happen. This part reproduces it with integer keys in CPython, colliding strings in Java and identity hashing in C++, times each one, and compares the defences.

In this part:

  • Colliding int keys in CPython, timed
  • A defence for int keys in Python, measured
  • Java: tree bins rescue comparable keys only
  • C++: identity hashing, and a seeded fix
  • The defences compared
  • Practise: count distinct substrings with a rolling hash
  • Count distinct substrings with a rolling hash
    • Results of the sample tests

4 runnable examples (C++, Java, Python) · 1 exercise with sample tests · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What must a hash function do to work well inside a hash table, and how is that different from a cryptographic hash?

    It must give the same hash for the same key throughout a run, give equal keys equal hashes, spread different keys evenly over the buckets and be fast, because every insert, find and delete computes it. It may collide: two different keys can share a hash, and the table settles that by comparing the keys. A cryptographic hash such as SHA-256 adds a much stronger promise, that nobody can find two inputs with the same output, which costs far more work per key and gives longer outputs than a table needs. When a table's keys can come from an attacker, it also needs unpredictability: a hash keyed with a random seed, so that colliding keys cannot be worked out in advance.

  2. Mid to senior

    You find repeated substrings by comparing polynomial hashes modulo 2⁶¹ − 1 with a random base. One run makes about 10⁷ comparisons of substrings up to 10⁵ characters long. How likely is a false match, and when would you still compare the strings themselves?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Your web service parses each JSON request body into a hash map. A client sends a body with 200,000 keys chosen so that they all collide. What happens to the server, and which defences would you put in place?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Your service randomises its hash seed at start-up, so the iteration order of its maps changes between runs, and one test fails about one run in five. A colleague proposes fixing the seed in production so that runs are reproducible. What do you advise?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A table’s hash must be consistent within a run, equal for equal keys, well spread and fast, but not cryptographic.
  • A polynomial hash reads a string’s codes as digits in base B modulo a large prime M.
  • A fixed, known hash can be flooded: chosen keys make n insertions cost O(n²).
  • Universal hashing picks the function at random, so every pair of keys collides with probability at most 1/m.
  • CPython salts str and bytes hashes but not int hashes.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.