# Efficient Probabilistic Data Structures in Go: HyperLogLog and Count-Min Sketch
HyperLogLog estimates the number of unique elements in a data stream with 1-3% error using just 1 KB of memory. The algorithm hashes elements, analyzes the number of leading zeros in their binary representation, and stores statistics in an array of registers. This provides O(1) insertion time and O(m) for estimation, where m is the number of registers (typically 1024).
Key parameters:
- m = 1024 registers (8 bits per register)
- b = 8 bits for register indexing
- Hash functions: fnv64a with bit shuffling
package main
import (
"fmt"
"hash/fnv"
"math"
"math/rand"
"strconv"
)
const (
m = 1024 // count registtrov. This osnovnoy parametr, vliyayuschiy on tochnost.
b = 8 // number bit, ispolzuemykh for opredeleniya indexa register from hash.
)
type HyperLogLog struct {
// when asynchronous interaction worth think about mutexes
registers [m]byte
}
func NewHyperLogLog() *HyperLogLog {
return &HyperLogLog{}
}
// hash returns dva 32-bitnykh numbers from stroki
func hash(s string) (uint32, uint32) {
h := fnv.New64a()
h.Write([]byte(s))
v := h.Sum64()
// Shuffling bits, so that uluchshit raspredelenie
w := uint32(v >> 32)
z := uint32(v)
// Dobavlyaem peremeshivanie, uluchshaet kachestvo sluchaynosti and pomogaet izbezhat problem with plokhim raspredeleniem hashes
w ^= z<<13 | z>>(32-13)
z ^= w<<7 | w>>(32-7)
w ^= z<<17 | z>>(32-17)
return w, z
}
// Withchitaet count zeros left in dvoichnoy record numbers,
// if all bits — nuli, vozvraschaetsya 32.
func countLeadingZeros(x uint32) byte {
for i := 0; i < 32; i++ {
if (x>>(31-i))&1 == 1 {
return byte(i)
}
}
return 32
}
// Kheshiruem stroku.
// Beryom chast hash h1, so that define, in kakoy registtr popadyot element.
// By vtoroy chasti h2 schitaem, how many veduschikh zeros (rho), pribavlyaem 1.
// If eto wartość greater, than uzhe est in registtre — update ego.
func (hll *HyperLogLog) Add(s string) {
h1, h2 := hash(s)
// opredelyaem index register
idx := h1 % m
// vychislyaem wartość for update register
rho := countLeadingZeros(h2) + 1
if rho > hll.registers[idx] {
hll.registers[idx] = rho
}
}
// Withlozhnye formuly, by essence eta function
// delaet otsenku kolichestva unique elements
func (hll *HyperLogLog) Estimate() float64 {
sum := 0.0
for _, val := range hll.registers {
sum += 1 / math.Pow(2, float64(val))
}
estimate := alpha(m) * m * m / sum
// korrektsiya for malykh znacheniy, usessya metod
// Linear Counting : than greater nulevykh registtrov — then
// menshe realnaya kardinalnost.
if estimate <= 5*m/2 {
zeros := 0
for _, val := range hll.registers {
if val == 0 {
zeros++
}
}
if zeros != 0 {
estimate = float64(m) * math.Log(float64(m)/float64(zeros))
}
}
return estimate
}
// alpha — popravochnyy koeffitsient, kompensiruyuschiy systemticheskuyu oshibku
func alpha(m int) float64 {
switch m {
case 16:
return 0.673
case 32:
return 0.697
case 64:
return 0.709
default:
return 0.7213 / (1 + 1.079/float64(m))
}
}
func main() {
// Withzdayom HLL.
hll := NewHyperLogLog()
seen := make(map[string]struct{})
for i := 0; i < 1_000_000; i++ {
key := strconv.Itoa(rand.Intn(1_000_000))
seen[key] = struct{}{}
hll.Add(key)
}
nonZero := 0
for _, r := range hll.registers {
if r > 0 {
nonZero++
}
}
fmt.Printf("Realnoe count unique elements: %d\n", len(seen))
fmt.Printf("Otsenka unique elements: %.2f\n", hll.Estimate())
fmt.Printf("Zapolnennykh registtrov: %d/%d\n", nonZero, m)
}
For 1 million unique elements, the relative error was 0.84%, with 100% registers filled. Memory: 1 KB.
Count-Min Sketch for Frequency Counting
Count-Min Sketch estimates element frequencies in a stream using a 2D table of counters (depth × width). Each row uses a separate hash function with a salt. On insertion, the element increments counters in all rows; on query, the minimum is taken—this minimizes collision effects.
Implementation parameters:
- width = 10,000 columns
- depth = 5 rows
- Hash: fnv64a with seed shuffling
Query methods:
- Minimum — basic, accurate for positive updates
- Average — robust to deletions
- Count-mean-min — corrects bias
package main
import (
"fmt"
"hash/fnv"
"math"
)
type CountMinSketch struct {
width int
depth int
// when asynchronous interaction worth think about mutexes
table [][]int
hashes []uint64
}
func NewCountMinSketch(width, depth int) *CountMinSketch {
return &CountMinSketch{
width: width,
depth: depth,
table: make([][]int, depth),
hashes: make([]uint64, depth),
}
}
func (cms *CountMinSketch) Init() {
for i := range cms.table {
cms.table[i] = make([]int, cms.width)
cms.hashes[i] = uint64(i+1) // prostye soli for raznykh hashes
}
}
// getHashIndex generiruet khesh and returns index in table
func (cms *CountMinSketch) getHashIndex(s string, seed uint64) int {
h := fnv.New64a()
h.Write([]byte(s))
v := h.Sum64()
// Shuffling bits with seed
mixed := v ^ seed ^ (seed << 32)
return int(mixed % uint64(cms.width))
}
// Add uvelichivaet schetchik for element
func (cms *CountMinSketch) Add(s string, count int) {
for i := 0; i < cms.depth; i++ {
idx := cms.getHashIndex(s, cms.hashes[i])
cms.table[i][idx] += count
}
}
// Count returns otsenochnoe number vkhozhdeniy element
func (cms *CountMinSketch) Count(s string) int {
min := math.MaxInt32
for i := 0; i < cms.depth; i++ {
idx := cms.getHashIndex(s, cms.hashes[i])
if cms.table[i][idx] < min {
min = cms.table[i][idx]
}
}
return min
}
func main() {
width := 10_000
depth := 5
cms := NewCountMinSketch(width, depth)
cms.Init()
seen := make(map[string]int)
for i := 0; i < 5_000_000; i++ {
key := fmt.Sprintf("item-%d", i%1100)
seen[key]++
cms.Add(key, 1)
}
// vyvodim frequency
for _, word := range []string{"item-1", "item-0", "item-1000", "item--1"} {
fmt.Printf("Withglasno cms slovo '%s' meetingsaetsya primerno %d raz\t By faktu: %d\n", word, cms.Count(word), seen[word])
}
}
For 5 million insertions, memory: 200 KB. Operations are O(depth) for both insertion and query.
Comparison of Data Structures
| Characteristic | HyperLogLog | Count-Min Sketch |
|---------------|-------------|------------------|
| Task | Cardinality | Frequency |
| Memory | O(log(log N)) | O(d×w) |
| Error | 1-3% | Depends on d,w |
| Insertion | O(1) | O(depth) |
| Query | O(m) | O(depth) |
Key points:
- Both structures are suitable for streaming processing of millions of elements
- HyperLogLog uses fixed memory regardless of data volume
- Count-Min Sketch overestimates frequencies due to collisions but provides point queries
- For distributed systems, add mutexes or use atomic operations
- Test with real data: error depends on hash distribution
— Editorial Team
No comments yet.