5 个版本
0.1.1 | 2020 年 6 月 4 日 |
---|---|
0.1.0 | 2020 年 5 月 27 日 |
0.0.3 | 2020 年 4 月 14 日 |
0.0.2 | 2020 年 4 月 13 日 |
0.0.1 | 2020 年 4 月 9 日 |
#1889 在 数据结构 中
在 2 个包中使用 (通过 myxine-core)
71KB
621 行
跳跳球队列:在标记、有序数据中跳跃和跳过
一个 hopscotch::Queue<K, V>
是一个先进先出的队列,其中队列中的每个 Item<K, V>
具有值
- 类型为
V
, - 一个不可变的标签类型为
K
,以及 - 一个唯一的
index
类型为u64
,它在创建队列时按插入顺序分配,从 0 开始。
除了支持 FIFO 队列的普通 push
、pop
和 get
方法外,跳跳球队列还支持特殊、优化的方法 after
、after_mut
、before
和 before_mut
。
这些方法查找队列中标签等于给定标签集的任何标签的下一个(或上一个)Item
(或 ItemMut
)。例如,after
的签名是
pub fn after<'a, Tags>(&self, index: u64, tags: Tags) -> Option<Item<K, V>>
where
Tags: IntoIterator<Item = &'a K>,
K: 'a,
如果我们用 &[K]
作为 Tags
,则此签名简化为
pub fn after(&self, index: u64, tags: &[K]) -> Option<Item<K, V>>
这些方法是使用跳跳球队列而不是其他数据结构的真正好处。它们的渐近时间复杂度是
- 与查询的标签数量成线性关系,
- 与队列中不同标签的总数成对数关系,
- 与队列长度成常数关系,以及
- 与具有相同标签的连续项之间的距离成常数关系。
跳跳球队列还提供了灵活的迭代器 Iter(Mut)
,以有效地遍历其内容的部分,通过 Iter(Mut)::until
和 Iter(Mut)::after
按索引切片,并使用 Iter(Mut)::matching_only
通过所需的标签集过滤。
我什么时候会使用这个?
该队列在以下情况下表现良好:
- 队列中总标签集较小
- 查询操作的数量大于插入/删除操作的数量
- 可以承受一点额外的内存用于记录
此类结构的一个用例是实现一个有界pub/sub事件缓冲区,其中对特定事件子集感兴趣的客户会重复查询下一个符合其所需子集的事件。这种场景充分发挥了跳转队列的优势,因为每个查询都可以非常快地完成,无论缓冲区的内容或大小如何。
与VecDeque<K, T>
的等价操作相比,push
和pop
操作的速度要慢得多,但代价是几乎可以以恒定时间跳过和跳转到标签集。
在只有几百个标签的场景中,我的机器(较新的MacBook Pro)上的push
和pop
操作大约需要低个位数的微秒(每秒数百万次操作),而after/
before
查询大约需要几十纳秒(每秒数千万次操作),具体取决于队列中标签的数量。更详细和科学的基准测试即将到来,请随时贡献!
如果这与您的需求相似,这种数据结构可能适合您!
我应该什么时候不使用这个?
- 如果您对队列中数据的访问模式不经常查询带有特定标签的下一个/上一个/第一个/最后一个,则该队列的表现将不如简单的
VecDeque<K, T>
。 - 如果您确实需要此类查询方法,但标签的分布使得标签不太可能频繁重复在队列中(标签数量接近队列的最大大小),则该队列的表现将不如由值
VecDeque<T>
和从标签到有序索引向量的映射组成的数据结构。 - 如果大多数标签都将接近它们相同种类的其他标签,并且您可以容忍偶尔的线性扫描,则简单的
VecDeque<K, T>
也可能足够。 - 如果您需要更改队列中已存在的项目的标签,则该数据结构不允许此操作。允许这种可能性将以常数因子的方式降低队列上所有其他操作的性能,但未来的版本可能会将其作为可选标志包含在内。
- 如果您需要从队列的“输入”端移除项目,向“输出”端添加项目,或在队列中间插入项目,这些操作对于跳转队列将是线性时间或更差,虽然我们可能会最终将它们添加为方法,但它们不会高效。
依赖项
~480KB
~10K SLoC