use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
const P: u32 = 6;
const M: usize = 1 << P;
const Q: u32 = 64 - P;
const ALPHA_MM: f64 = 0.709;
const LINEAR_COUNTING_THRESHOLD: f64 = M as f64 * 2.5;
#[derive(Debug, Clone)]
pub struct HyperLogLog {
registers: [u8; M],
}
impl Default for HyperLogLog {
fn default() -> Self {
Self::new()
}
}
impl HyperLogLog {
pub fn new() -> Self {
Self { registers: [0u8; M] }
}
pub fn insert<T: Hash>(&mut self, value: &T) {
let hash = Self::hash_value(value);
let idx = (hash & ((1 << P) - 1)) as usize; let w = (hash >> P) | (1u64 << Q); let leading_zeros = (w.trailing_zeros() + 1) as u8; if leading_zeros > self.registers[idx] {
self.registers[idx] = leading_zeros;
}
}
pub fn insert_hash(&mut self, hash: u64) {
let idx = (hash & ((1 << P) - 1)) as usize;
let w = (hash >> P) | (1u64 << Q);
let leading_zeros = (w.trailing_zeros() + 1) as u8;
if leading_zeros > self.registers[idx] {
self.registers[idx] = leading_zeros;
}
}
pub fn merge(&mut self, other: &HyperLogLog) {
for i in 0..M {
if other.registers[i] > self.registers[i] {
self.registers[i] = other.registers[i];
}
}
}
pub fn count(&self) -> u64 {
let z_inv: f64 = self.registers.iter().map(|&r| 2.0f64.powi(-(r as i32))).sum();
let raw_estimate = ALPHA_MM * (M as f64).powi(2) / z_inv;
if raw_estimate <= LINEAR_COUNTING_THRESHOLD {
let zero_regs = self.registers.iter().filter(|&&r| r == 0).count() as f64;
if zero_regs > 0.0 {
return (M as f64 * (M as f64 / zero_regs).ln()).round() as u64;
}
}
raw_estimate.round() as u64
}
pub fn non_zero_registers(&self) -> usize {
self.registers.iter().filter(|&&r| r > 0).count()
}
pub fn clear(&mut self) {
self.registers = [0u8; M];
}
fn hash_value<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_hll() {
let hll = HyperLogLog::new();
assert_eq!(hll.count(), 0);
}
#[test]
fn test_single_element() {
let mut hll = HyperLogLog::new();
hll.insert(&42i64);
let count = hll.count();
assert!((1..=5).contains(&count), "Expected ~1, got {}", count);
}
#[test]
fn test_small_set() {
let mut hll = HyperLogLog::new();
for i in 0..100i64 {
hll.insert(&i);
}
let count = hll.count();
let error = ((count as f64 - 100.0) / 100.0).abs();
assert!(
error < 0.30,
"Expected ~100, got {} (error: {:.1}%)",
count,
error * 100.0
);
}
#[test]
fn test_large_set() {
let mut hll = HyperLogLog::new();
for i in 0..10_000i64 {
hll.insert(&i);
}
let count = hll.count();
let error = ((count as f64 - 10_000.0) / 10_000.0).abs();
assert!(
error < 0.15,
"Expected ~10000, got {} (error: {:.1}%)",
count,
error * 100.0
);
}
#[test]
fn test_merge() {
let mut hll1 = HyperLogLog::new();
let mut hll2 = HyperLogLog::new();
for i in 0..500i64 {
hll1.insert(&i);
}
for i in 250..750i64 {
hll2.insert(&i);
}
hll1.merge(&hll2);
let count = hll1.count();
let error = ((count as f64 - 750.0) / 750.0).abs();
assert!(
error < 0.35,
"Expected ~750, got {} (error: {:.1}%)",
count,
error * 100.0
);
}
#[test]
fn test_clear() {
let mut hll = HyperLogLog::new();
for i in 0..1_000i64 {
hll.insert(&i);
}
assert!(hll.count() > 0);
hll.clear();
assert_eq!(hll.count(), 0);
}
}