Hashing, Collision Resolution, Bloom Filters, and String Algorithms

Hashing maps data of arbitrary size to fixed-size values. It provides expected $O(1)$ dictionary lookups, cryptographic message integrity verification, caching layers, and high-performance string matching algorithms (KMP, Rabin-Karp).


⚡ Quick Dive

Hashing & String Matching Complexity Reference

Structure / Algorithm Operation / Problem Average Time Worst Time Space Primary Strength
Hash Table (Chaining) Lookup / Insert / Delete $O(1)$ $O(n)$ $O(n)$ Simple deletion, robust to high load factors
Hash Table (Open Addressing) Lookup / Insert / Delete $O(1)$ $O(n)$ $O(n)$ Superior CPU cache locality, zero pointer overhead
Bloom Filter Probabilistic Set Membership $O(k)$ ($k$ hashes) $O(k)$ $O(m)$ (Bits) Zero false negatives; tiny memory footprint
Knuth-Morris-Pratt (KMP) Substring Pattern Search $O(n + m)$ $O(n + m)$ $O(m)$ Guaranteed linear time; never backtracks text pointer
Rabin-Karp Algorithm Multi-Pattern / Substring Search $O(n + m)$ $O(n \cdot m)$ $O(1)$ Rolling hash allows checking multiple patterns

📖 Extended Guide

1. Hash Table Architecture & Collision Resolution

A hash table maps a key $k$ to a bucket index $i = h(k) \pmod m$ where $m$ is the table capacity.

Key: "user:101" ──► [ Hash Function h(k) ] ──► Hash Code: 894371 ──► % 8 ──► Index: 3

1. Separate Chaining (Linked Lists or Mini-Red-Black Trees per Bucket):
   Index 3: [ "user:101", Data ] ──► [ "user:893", Data ] ──► nil

2. Open Addressing (Linear Probing / Robin Hood):
   Index 3: [ "user:101", Data ]
   Index 4: [ "user:893", Data ]  ◄── Stored in next available contiguous slot
  • Load Factor ($\alpha = n / m$): Ratio of stored elements $n$ to table capacity $m$. When $\alpha \ge 0.75$, the table reallocates to double capacity to preserve $O(1)$ performance.
  • Robin Hood Hashing: During open addressing collisions, elements further from their ideal hash location ("poor") take precedence over elements closer to their ideal spot ("rich"), drastically narrowing worst-case search variance.

2. Probabilistic Data Structures: Bloom Filters

A Bloom Filter is a space-efficient probabilistic data structure used to test whether an element is a member of a set:

  • Returns "Definitely Not in Set" (100% guarantee, Zero False Negatives).
  • Returns "Possibly in Set" (Controlled False Positive Probability $p$).
Bit Array of Size m = 10: [ 0, 1, 0, 0, 1, 0, 1, 0, 0, 0 ]
                           ▲        ▲     ▲
                        hash1(x)  hash2(x) hash3(x)
  • Use Cases: Preventing expensive disk reads in databases (Cassandra / RocksDB SSTables), caching layers (preventing cache stampedes), and malicious URL filtering in web browsers.

3. Knuth-Morris-Pratt (KMP) Substring Search

The naive substring search takes $O(n \cdot m)$ time because it backtracks the text index upon a mismatch. KMP eliminates backtracking by precomputing a Longest Proper Prefix which is also Suffix (LPS / $\pi$) table for the pattern in $O(m)$ time:

package main

import "fmt"

func computeLPSArray(pattern string) []int {
	m := len(pattern)
	lps := make([]int, m)
	length := 0
	i := 1

	for i < m {
		if pattern[i] == pattern[length] {
			length++
			lps[i] = length
			i++
		} else {
			if length != 0 {
				length = lps[length-1]
			} else {
				lps[i] = 0
				i++
			}
		}
	}
	return lps
}

func KMPSearch(text, pattern string) []int {
	matches := []int{}
	n, m := len(text), len(pattern)
	if m == 0 || n < m {
		return matches
	}

	lps := computeLPSArray(pattern)
	i, j := 0, 0 // i for text, j for pattern

	for i < n {
		if pattern[j] == text[i] {
			i++
			j++
		}

		if j == m {
			matches = append(matches, i-j)
			j = lps[j-1]
		} else if i < n && pattern[j] != text[i] {
			if j != 0 {
				j = lps[j-1] // Shift pattern without moving text pointer i
			} else {
				i++
			}
		}
	}

	return matches
}

func main() {
	text := "ABABDABACDABABCABAB"
	pattern := "ABABCABAB"
	fmt.Println("Pattern found at indices:", KMPSearch(text, pattern)) // [10]
}

4. Rabin-Karp Algorithm (Rolling Hash)

Rabin-Karp computes a polynomial rolling hash of the pattern and a sliding window over the text: $$H(S) = (s_0 \cdot b^{k-1} + s_1 \cdot b^{k-2} + \dots + s_{k-1}) \pmod q$$

Advancing the window by one character requires only $O(1)$ arithmetic (subtract outgoing character, multiply by base $b$, add incoming character). This enables searching for hundreds of patterns simultaneously (plagiarism detection) in $O(n)$ average time.