use std::{collections::HashMap, hash::Hash};
#[derive(Clone)]
pub struct TopK<K: Hash + Eq + Clone> {
k: usize,
counts: HashMap<K, u64>,
}
impl<K: Hash + Eq + Clone> TopK<K> {
pub fn new(k: usize) -> Self {
assert!(k > 0, "k must be > 0");
Self {
k,
counts: HashMap::new(),
}
}
pub fn new_unbounded() -> Self {
Self::new(usize::MAX)
}
pub fn observe(&mut self, key: K) {
self.observe_n(key, 1);
}
pub fn observe_n(&mut self, key: K, count: u64) {
if count == 0 {
return;
}
if let Some(c) = self.counts.get_mut(&key) {
*c += count;
return;
}
if self.counts.len() < self.k {
self.counts.insert(key, count);
return;
}
let mut to_evict: Vec<K> = Vec::new();
for (k, c) in self.counts.iter_mut() {
if *c <= count {
to_evict.push(k.clone());
} else {
*c -= count;
}
}
for k in to_evict {
self.counts.remove(&k);
}
if self.counts.len() < self.k {
self.counts.insert(key, count);
}
}
pub fn top(&self) -> Vec<(&K, u64)> {
let mut v: Vec<(&K, u64)> = self.counts.iter().map(|(k, c)| (k, *c)).collect();
v.sort_by_key(|b| std::cmp::Reverse(b.1));
v
}
pub fn estimate(&self, key: &K) -> u64 {
self.counts.get(key).copied().unwrap_or(0)
}
pub fn clear(&mut self) {
self.counts.clear();
}
pub fn len(&self) -> usize {
self.counts.len()
}
pub fn is_empty(&self) -> bool {
self.counts.is_empty()
}
}
impl<K: Hash + Eq + Clone> crate::correlate::Mergeable for TopK<K> {
fn merge(&mut self, other: Self) {
assert_eq!(
self.k, other.k,
"TopK::merge requires matching k (lhs={}, rhs={})",
self.k, other.k
);
for (k, c) in other.counts {
*self.counts.entry(k).or_insert(0) += c;
}
if self.counts.len() <= self.k {
return;
}
let mut counts: Vec<u64> = self.counts.values().copied().collect();
counts.sort_unstable_by(|a, b| b.cmp(a)); let pivot = counts[self.k];
let to_remove: Vec<K> = self
.counts
.iter()
.filter(|&(_, &c)| c <= pivot)
.map(|(k, _)| k.clone())
.collect();
for k in to_remove {
self.counts.remove(&k);
}
for c in self.counts.values_mut() {
*c = c.saturating_sub(pivot);
}
}
}
impl<K: Hash + Eq + Clone + std::fmt::Debug> std::fmt::Debug for TopK<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TopK")
.field("k", &self.k)
.field("counts", &self.counts)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_counts_when_under_capacity() {
let mut t: TopK<u32> = TopK::new(4);
for &k in &[1, 1, 2, 3, 3, 3] {
t.observe(k);
}
assert_eq!(t.estimate(&1), 2);
assert_eq!(t.estimate(&2), 1);
assert_eq!(t.estimate(&3), 3);
}
#[test]
fn top_returns_descending() {
let mut t: TopK<u32> = TopK::new(4);
for &k in &[1, 1, 2, 3, 3, 3] {
t.observe(k);
}
let top = t.top();
assert_eq!(top.len(), 3);
assert_eq!(*top[0].0, 3);
assert_eq!(*top[1].0, 1);
assert_eq!(*top[2].0, 2);
}
#[test]
fn over_capacity_keeps_heaviest_hitters() {
let mut t: TopK<u32> = TopK::new(2);
t.observe_n(1, 1);
t.observe_n(2, 2);
t.observe_n(3, 3);
let top = t.top();
assert!(top.iter().any(|(k, _)| **k == 3));
}
#[test]
fn clear_resets_state() {
let mut t: TopK<u32> = TopK::new(2);
t.observe(1);
t.clear();
assert!(t.is_empty());
assert_eq!(t.estimate(&1), 0);
}
#[test]
fn observe_n_zero_is_noop() {
let mut t: TopK<u32> = TopK::new(2);
t.observe_n(1, 0);
assert!(t.is_empty());
}
#[test]
fn new_unbounded_never_evicts() {
let mut t: TopK<u32> = TopK::new_unbounded();
for i in 0..1000 {
t.observe(i);
}
for i in 0..1000 {
assert_eq!(t.estimate(&i), 1);
}
}
}