use std::borrow::Borrow;
use std::cmp::{Ordering, Reverse};
use std::collections::{BinaryHeap, HashMap};
use std::fmt;
use std::hash::Hash;
use std::iter::FusedIterator;
#[derive(Debug, Clone)]
struct HeapEntry<K, S> {
score: S,
seq: u64,
key: K,
}
impl<K, S> PartialEq for HeapEntry<K, S>
where
S: Ord,
{
fn eq(&self, other: &Self) -> bool {
self.score == other.score && self.seq == other.seq
}
}
impl<K, S> Eq for HeapEntry<K, S> where S: Ord {}
impl<K, S> PartialOrd for HeapEntry<K, S>
where
S: Ord,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<K, S> Ord for HeapEntry<K, S>
where
S: Ord,
{
fn cmp(&self, other: &Self) -> Ordering {
match self.score.cmp(&other.score) {
Ordering::Equal => self.seq.cmp(&other.seq),
ordering => ordering,
}
}
}
#[derive(Debug, Clone)]
pub struct LazyMinHeap<K, S> {
scores: HashMap<K, ScoreEntry<S>>,
heap: BinaryHeap<Reverse<HeapEntry<K, S>>>,
seq: u64,
}
impl<K, S> LazyMinHeap<K, S>
where
K: Eq + Hash + Clone,
S: Ord + Clone,
{
pub fn new() -> Self {
Self {
scores: HashMap::new(),
heap: BinaryHeap::new(),
seq: 0,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
scores: HashMap::with_capacity(capacity),
heap: BinaryHeap::with_capacity(capacity),
seq: 0,
}
}
pub fn reserve(&mut self, additional: usize) {
self.scores.reserve(additional);
self.heap.reserve(additional);
}
pub fn shrink_to_fit(&mut self) {
self.scores.shrink_to_fit();
self.heap.shrink_to_fit();
}
pub fn clear(&mut self) {
self.scores.clear();
self.heap.clear();
}
pub fn clear_shrink(&mut self) {
self.clear();
self.scores.shrink_to_fit();
self.heap.shrink_to_fit();
}
pub fn len(&self) -> usize {
self.scores.len()
}
pub fn is_empty(&self) -> bool {
self.scores.is_empty()
}
pub fn heap_len(&self) -> usize {
self.heap.len()
}
pub fn iter(&self) -> Iter<'_, K, S> {
Iter {
inner: self.scores.iter(),
}
}
pub fn score_of<Q>(&self, key: &Q) -> Option<&S>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.scores.get(key).map(|entry| &entry.score)
}
pub fn update(&mut self, key: K, score: S) -> Option<S> {
let seq = self.seq;
self.seq = self.seq.wrapping_add(1);
let previous = self.scores.insert(
key.clone(),
ScoreEntry {
score: score.clone(),
seq,
},
);
self.push_entry_with_seq(key, score, seq);
previous.map(|entry| entry.score)
}
pub fn remove<Q>(&mut self, key: &Q) -> Option<S>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.scores.remove(key).map(|entry| entry.score)
}
pub fn pop_best(&mut self) -> Option<(K, S)> {
loop {
let Reverse(entry) = self.heap.pop()?;
match self.scores.get(&entry.key) {
Some(current) if current.score == entry.score && current.seq == entry.seq => {
self.scores.remove(&entry.key);
return Some((entry.key, entry.score));
},
_ => continue,
}
}
}
pub fn rebuild(&mut self) {
self.heap.clear();
let entries: Vec<(K, ScoreEntry<S>)> = self
.scores
.iter()
.map(|(key, entry)| (key.clone(), entry.clone()))
.collect();
for (key, entry) in entries {
self.push_entry_with_seq(key, entry.score, entry.seq);
}
}
pub fn maybe_rebuild(&mut self, factor: usize) {
let factor = factor.max(1);
if self.heap.len() > self.scores.len().saturating_mul(factor) {
self.rebuild();
}
}
#[cfg(test)]
fn debug_snapshot(&self) -> LazyHeapSnapshot {
LazyHeapSnapshot {
len: self.len(),
heap_len: self.heap_len(),
}
}
pub fn approx_bytes(&self) -> usize {
std::mem::size_of::<Self>()
+ self.scores.capacity() * std::mem::size_of::<(K, ScoreEntry<S>)>()
+ self.heap.capacity() * std::mem::size_of::<std::cmp::Reverse<HeapEntry<K, S>>>()
}
#[cfg(test)]
fn debug_snapshot_scores(&self) -> Vec<(K, S)>
where
K: Clone,
S: Clone,
{
self.scores
.iter()
.map(|(key, entry)| (key.clone(), entry.score.clone()))
.collect()
}
#[cfg(test)]
fn debug_validate_invariants(&self) {
assert_eq!(self.len(), self.scores.len());
if self.is_empty() {
assert!(self.scores.is_empty());
}
}
fn push_entry_with_seq(&mut self, key: K, score: S, seq: u64) {
let entry = HeapEntry { score, seq, key };
self.heap.push(Reverse(entry));
}
}
#[derive(Debug, Clone)]
struct ScoreEntry<S> {
score: S,
seq: u64,
}
pub struct Iter<'a, K, S> {
inner: std::collections::hash_map::Iter<'a, K, ScoreEntry<S>>,
}
impl<K: fmt::Debug, S: fmt::Debug> fmt::Debug for Iter<'_, K, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Iter").finish_non_exhaustive()
}
}
impl<'a, K, S> Iterator for Iter<'a, K, S> {
type Item = (&'a K, &'a S);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(key, entry)| (key, &entry.score))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<K, S> ExactSizeIterator for Iter<'_, K, S> {}
impl<K, S> FusedIterator for Iter<'_, K, S> {}
pub struct IntoIter<K, S> {
inner: std::collections::hash_map::IntoIter<K, ScoreEntry<S>>,
}
impl<K: fmt::Debug, S: fmt::Debug> fmt::Debug for IntoIter<K, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IntoIter").finish_non_exhaustive()
}
}
impl<K, S> Iterator for IntoIter<K, S> {
type Item = (K, S);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(key, entry)| (key, entry.score))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<K, S> ExactSizeIterator for IntoIter<K, S> {}
impl<K, S> FusedIterator for IntoIter<K, S> {}
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct LazyHeapSnapshot {
len: usize,
heap_len: usize,
}
impl<K, S> Default for LazyMinHeap<K, S>
where
K: Eq + Hash + Clone,
S: Ord + Clone,
{
fn default() -> Self {
Self::new()
}
}
impl<K, S> FromIterator<(K, S)> for LazyMinHeap<K, S>
where
K: Eq + Hash + Clone,
S: Ord + Clone,
{
fn from_iter<I: IntoIterator<Item = (K, S)>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let mut heap = Self::with_capacity(lower);
for (key, score) in iter {
heap.update(key, score);
}
heap
}
}
impl<K, S> Extend<(K, S)> for LazyMinHeap<K, S>
where
K: Eq + Hash + Clone,
S: Ord + Clone,
{
fn extend<I: IntoIterator<Item = (K, S)>>(&mut self, iter: I) {
for (key, score) in iter {
self.update(key, score);
}
}
}
impl<K, S> IntoIterator for LazyMinHeap<K, S>
where
K: Eq + Hash + Clone,
S: Ord + Clone,
{
type Item = (K, S);
type IntoIter = IntoIter<K, S>;
fn into_iter(self) -> Self::IntoIter {
IntoIter {
inner: self.scores.into_iter(),
}
}
}
impl<'a, K, S> IntoIterator for &'a LazyMinHeap<K, S>
where
K: Eq + Hash + Clone,
S: Ord + Clone,
{
type Item = (&'a K, &'a S);
type IntoIter = Iter<'a, K, S>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lazy_heap_skips_stale_entries() {
let mut heap = LazyMinHeap::new();
heap.update("a", 5);
heap.update("a", 2);
heap.update("b", 3);
assert_eq!(heap.pop_best(), Some(("a", 2)));
assert_eq!(heap.pop_best(), Some(("b", 3)));
}
#[test]
fn lazy_heap_remove_and_rebuild() {
let mut heap = LazyMinHeap::new();
heap.update("a", 5);
heap.update("b", 1);
heap.remove(&"b");
heap.maybe_rebuild(1);
assert_eq!(heap.pop_best(), Some(("a", 5)));
assert_eq!(heap.pop_best(), None);
}
#[test]
fn lazy_heap_update_overwrites_score_and_len() {
let mut heap = LazyMinHeap::new();
assert_eq!(heap.len(), 0);
assert_eq!(heap.update("a", 10), None);
assert_eq!(heap.len(), 1);
assert_eq!(heap.score_of(&"a"), Some(&10));
assert_eq!(heap.update("a", 3), Some(10));
assert_eq!(heap.len(), 1);
assert_eq!(heap.score_of(&"a"), Some(&3));
}
#[test]
fn lazy_heap_pop_best_removes_key() {
let mut heap = LazyMinHeap::new();
heap.update("a", 2);
heap.update("b", 1);
assert_eq!(heap.pop_best(), Some(("b", 1)));
assert_eq!(heap.score_of(&"b"), None);
assert_eq!(heap.len(), 1);
assert_eq!(heap.pop_best(), Some(("a", 2)));
assert!(heap.is_empty());
}
#[test]
fn lazy_heap_tie_breaks_by_seq() {
let mut heap = LazyMinHeap::new();
heap.update("a", 1);
heap.update("b", 1);
heap.update("c", 1);
assert_eq!(heap.pop_best(), Some(("a", 1)));
assert_eq!(heap.pop_best(), Some(("b", 1)));
assert_eq!(heap.pop_best(), Some(("c", 1)));
}
#[test]
fn lazy_heap_update_same_score_refreshes_order() {
let mut heap = LazyMinHeap::new();
heap.update("a", 1);
heap.update("b", 1);
heap.update("a", 1); assert_eq!(heap.pop_best(), Some(("b", 1)));
assert_eq!(heap.pop_best(), Some(("a", 1)));
}
#[test]
fn lazy_heap_remove_does_not_touch_heap_until_pop() {
let mut heap = LazyMinHeap::new();
heap.update("a", 2);
heap.update("b", 1);
assert_eq!(heap.remove(&"b"), Some(1));
assert_eq!(heap.len(), 1);
assert_eq!(heap.pop_best(), Some(("a", 2)));
assert_eq!(heap.pop_best(), None);
}
#[test]
fn lazy_heap_rebuild_cleans_stale_entries() {
let mut heap = LazyMinHeap::new();
heap.update("a", 5);
heap.update("a", 4);
heap.update("a", 3);
heap.update("b", 2);
assert!(heap.heap_len() > heap.len());
heap.rebuild();
assert_eq!(heap.heap_len(), heap.len());
assert_eq!(heap.pop_best(), Some(("b", 2)));
assert_eq!(heap.pop_best(), Some(("a", 3)));
}
#[test]
fn lazy_heap_maybe_rebuild_triggers_on_factor() {
let mut heap = LazyMinHeap::new();
heap.update("a", 3);
heap.update("a", 2);
heap.update("a", 1);
heap.update("b", 4);
assert!(heap.heap_len() > heap.len());
heap.maybe_rebuild(1);
assert_eq!(heap.heap_len(), heap.len());
assert_eq!(heap.pop_best(), Some(("a", 1)));
}
#[test]
fn lazy_heap_debug_invariants_hold() {
let mut heap = LazyMinHeap::new();
heap.update("a", 2);
heap.update("b", 1);
heap.remove(&"b");
heap.debug_validate_invariants();
}
#[test]
fn lazy_heap_debug_snapshots() {
let mut heap = LazyMinHeap::new();
heap.update("a", 2);
heap.update("b", 1);
let snapshot = heap.debug_snapshot();
assert_eq!(snapshot.len, 2);
assert!(snapshot.heap_len >= snapshot.len);
let scores = heap.debug_snapshot_scores();
assert_eq!(scores.len(), 2);
}
#[test]
fn lazy_heap_iter_visits_live_entries() {
let mut heap = LazyMinHeap::new();
heap.update("a", 3);
heap.update("b", 1);
heap.update("a", 2);
let mut entries: Vec<_> = heap.iter().collect();
entries.sort();
assert_eq!(entries, vec![(&"a", &2), (&"b", &1)]);
}
#[test]
fn lazy_heap_into_iter_yields_live_entries() {
let mut heap = LazyMinHeap::new();
heap.update("a", 3);
heap.update("b", 1);
heap.update("a", 2);
let mut entries: Vec<_> = heap.into_iter().collect();
entries.sort();
assert_eq!(entries, vec![("a", 2), ("b", 1)]);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_min_heap_ordering(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 0..50)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in entries {
heap.update(key, score);
}
let mut last_score = None;
while let Some((_key, score)) = heap.pop_best() {
if let Some(prev_score) = last_score {
prop_assert!(score >= prev_score);
}
last_score = Some(score);
}
prop_assert!(heap.is_empty());
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_tie_breaking_fifo(
keys in prop::collection::vec(any::<u32>(), 3..20)
) {
let mut heap = LazyMinHeap::new();
let score = 1u32;
for key in &keys {
heap.update(*key, score);
}
for expected_key in keys {
if let Some((key, s)) = heap.pop_best() {
prop_assert_eq!(s, score);
prop_assert_eq!(key, expected_key);
}
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_update_overwrites(
key in any::<u32>(),
scores in prop::collection::vec(any::<u32>(), 1..20)
) {
let mut heap = LazyMinHeap::new();
for score in &scores {
heap.update(key, *score);
prop_assert_eq!(heap.score_of(&key), Some(score));
prop_assert_eq!(heap.len(), 1);
}
let popped = heap.pop_best();
prop_assert!(popped.is_some());
let (k, s) = popped.unwrap();
prop_assert_eq!(k, key);
prop_assert_eq!(s, *scores.last().unwrap());
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_update_idempotent(
key in any::<u32>(),
score in any::<u32>(),
repeat_count in 1usize..10
) {
let mut heap = LazyMinHeap::new();
for _ in 0..repeat_count {
heap.update(key, score);
prop_assert_eq!(heap.score_of(&key), Some(&score));
prop_assert_eq!(heap.len(), 1);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_update_returns_old_score(
key in any::<u32>(),
score1 in any::<u32>(),
score2 in any::<u32>()
) {
let mut heap = LazyMinHeap::new();
let old = heap.update(key, score1);
prop_assert_eq!(old, None);
let old = heap.update(key, score2);
prop_assert_eq!(old, Some(score1));
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_remove_decreases_length(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 1..30)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in &entries {
heap.update(*key, *score);
}
for (key, score) in entries {
let old_len = heap.len();
let removed = heap.remove(&key);
if removed == Some(score) {
prop_assert_eq!(heap.len(), old_len - 1);
prop_assert_eq!(heap.score_of(&key), None);
}
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_remove_makes_unavailable(
key in any::<u32>(),
score in any::<u32>()
) {
let mut heap = LazyMinHeap::new();
heap.update(key, score);
prop_assert_eq!(heap.score_of(&key), Some(&score));
let removed = heap.remove(&key);
prop_assert_eq!(removed, Some(score));
prop_assert_eq!(heap.score_of(&key), None);
prop_assert!(heap.is_empty());
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_remove_missing_returns_none(
insert_keys in prop::collection::vec(0u32..20, 1..10),
query_key in 20u32..40
) {
let mut heap = LazyMinHeap::new();
for key in insert_keys {
heap.update(key, 1);
}
let removed = heap.remove(&query_key);
prop_assert_eq!(removed, None);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_pop_decreases_length(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 1..30)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in entries {
heap.update(key, score);
}
while !heap.is_empty() {
let old_len = heap.len();
let popped = heap.pop_best();
prop_assert!(popped.is_some());
prop_assert_eq!(heap.len(), old_len - 1);
}
prop_assert_eq!(heap.pop_best(), None);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_pop_removes_key(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 1..30)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in entries {
heap.update(key, score);
}
while let Some((key, _score)) = heap.pop_best() {
prop_assert_eq!(heap.score_of(&key), None);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_pop_empty_returns_none(_unit in any::<()>()) {
let mut heap: LazyMinHeap<u32, u32> = LazyMinHeap::new();
prop_assert_eq!(heap.pop_best(), None);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_stale_entries_skipped(
updates in prop::collection::vec((0u32..10, any::<u32>()), 10..50)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in updates {
heap.update(key, score);
}
let mut seen_keys = std::collections::HashSet::new();
while let Some((key, _score)) = heap.pop_best() {
prop_assert!(!seen_keys.contains(&key));
seen_keys.insert(key);
}
prop_assert!(heap.is_empty());
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_rebuild_preserves_order(
updates in prop::collection::vec((0u32..20, any::<u32>()), 10..50)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in updates {
heap.update(key, score);
}
let len_before = heap.len();
heap.rebuild();
prop_assert_eq!(heap.len(), len_before);
prop_assert_eq!(heap.heap_len(), heap.len());
let mut last_score = None;
while let Some((_key, score)) = heap.pop_best() {
if let Some(prev_score) = last_score {
prop_assert!(score >= prev_score);
}
last_score = Some(score);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_maybe_rebuild_factor(
key in any::<u32>(),
updates in prop::collection::vec(any::<u32>(), 3..20)
) {
let mut heap = LazyMinHeap::new();
for score in updates {
heap.update(key, score);
}
let heap_len_before = heap.heap_len();
let len = heap.len();
if heap_len_before > len {
heap.maybe_rebuild(1);
prop_assert_eq!(heap.heap_len(), heap.len());
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_len_tracks_unique_keys(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 0..50)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in &entries {
heap.update(*key, *score);
}
let unique_count = {
let mut unique = std::collections::HashSet::new();
for (key, _) in entries {
unique.insert(key);
}
unique.len()
};
prop_assert_eq!(heap.len(), unique_count);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_is_empty_consistent(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 0..30)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in entries {
heap.update(key, score);
if heap.is_empty() {
prop_assert_eq!(heap.len(), 0);
} else {
prop_assert!(!heap.is_empty());
}
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_score_of_returns_current(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 1..30)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in &entries {
heap.update(*key, *score);
}
for (key, expected_score) in entries {
if let Some(&actual_score) = heap.score_of(&key) {
prop_assert_eq!(actual_score, expected_score);
}
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_score_of_removed_is_none(
key in any::<u32>(),
score in any::<u32>()
) {
let mut heap = LazyMinHeap::new();
heap.update(key, score);
heap.remove(&key);
prop_assert_eq!(heap.score_of(&key), None);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_clear_resets_state(
entries in prop::collection::vec((any::<u32>(), any::<u32>()), 1..30)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in entries {
heap.update(key, score);
}
heap.clear_shrink();
prop_assert!(heap.is_empty());
prop_assert_eq!(heap.len(), 0);
prop_assert_eq!(heap.pop_best(), None);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_usable_after_clear(
entries1 in prop::collection::vec((any::<u32>(), any::<u32>()), 1..20),
entries2 in prop::collection::vec((any::<u32>(), any::<u32>()), 1..20)
) {
let mut heap = LazyMinHeap::new();
for (key, score) in entries1 {
heap.update(key, score);
}
heap.clear_shrink();
for (key, score) in &entries2 {
heap.update(*key, *score);
}
let unique_count = {
let mut unique = std::collections::HashSet::new();
for (key, _) in entries2 {
unique.insert(key);
}
unique.len()
};
prop_assert_eq!(heap.len(), unique_count);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_matches_binary_heap(
operations in prop::collection::vec((0u8..3, any::<u32>(), any::<u32>()), 0..50)
) {
let mut heap = LazyMinHeap::new();
let mut reference = std::collections::BinaryHeap::new();
let mut live_keys = std::collections::HashSet::new();
use std::cmp::Reverse;
for (op, key, score) in operations {
match op % 3 {
0 => {
heap.update(key, score);
reference.retain(|&Reverse((_s, k))| k != key);
reference.push(Reverse((score, key)));
live_keys.insert(key);
}
1 => {
let heap_val = heap.pop_best();
let mut ref_val = None;
while let Some(Reverse((score, key))) = reference.pop() {
if live_keys.contains(&key) {
ref_val = Some((key, score));
live_keys.remove(&key);
break;
}
}
prop_assert_eq!(heap_val, ref_val);
}
2 => {
heap.remove(&key);
live_keys.remove(&key);
}
_ => unreachable!(),
}
prop_assert_eq!(heap.len(), live_keys.len());
prop_assert_eq!(heap.is_empty(), live_keys.is_empty());
}
}
}
}