【问题标题】:How to implement LFU cache using STL?如何使用 STL 实现 LFU 缓存?
【发布时间】:2012-07-10 08:41:01
【问题描述】:

我正在尝试使用纯 STL 实现 LFU(最不常用)缓存(我不想使用 Boost!)。

要求是:

  • 使用Key 对任何元素进行关联访问,例如std::map
  • 能够释放最低优先级项目(使用其UsesCount 属性)。
  • 能够更新任何项目的优先级 (UsesCount)。

问题是:

  • 如果我使用std::vector 作为项目容器(KeyValueUsesCount)、std::map 作为用于关联访问的向量的迭代器容器和std::make_heapstd::push_heapstd::pop_heap 作为vector内的优先队列实现,map中的迭代器在堆操作后无效。
  • 如果我在之前的配置中使用std::list(或std::map)而不是std::vector,则无法编译std::make_heap等,因为它们的迭代器不支持算术运算。
  • 如果我想使用std::priority_queue,我无法更新项目优先级。

问题是:

  • 我是否遗漏了一些显而易见的问题如何解决?
  • 您能否以一些纯 C++/STL 实现的 LFU 缓存满足之前的要求为例?

感谢您的见解。

【问题讨论】:

  • “堆操作后映射中的迭代器无效” - 通过相反的方式解决此问题 - 将数据放在 map 中,它甚至不会被移动如果其他元素被插入/删除。然后将映射迭代器放入您的向量中并从中构建一个堆。但是,您可能仍然缺少有效更新项目优先级的能力,因此这不是答案。
  • 感谢您提出另一个我没有想到的想法。但是,如果我有std::vectorstd::map 迭代器,我该如何定义它们的比较运算符,这将在UsesCount 属性的指针对象内部查看,以便能够在插入项目后使用std::make_heap 修复堆或UsesCount更新?
  • 使用比较函子,例如:bool operator()(MapIter a, MapIterB) { return a->second.UseCount < b->second.UseCount; }
  • 只是在迂腐方面,你确定你不是指经常使用(MFU)/最少最近使用(LRU) ?最不常用的,为什么要缓存这些?
  • 引用维基百科“LFU 计算需要的项目的频率。最不经常使用的项目首先被丢弃。”,这意味着它就是我想要的。

标签: c++ algorithm caching stl


【解决方案1】:

您使用 *_heap 函数和向量的 make 实现似乎很合适。虽然它会导致更新缓慢。对于使用向量作为底层数据结构的每个容器,您遇到的有关迭代器失效的问题是正常的。这也是boost::heap::priority_queue 采用的方法,但由于上述原因,它没有提供可变接口。其他boost::heap data-structures 提供更新堆的能力。

看起来有点奇怪:即使您能够使用std::priority_queue,您仍然会面临迭代器失效问题。

直接回答您的问题:您并没有遗漏一些明显的东西。 std::priority_queue 没有应有的用处。最好的方法是编写您自己的支持更新的堆实现。使其完全兼容 STL(尤其是分配器感知)相当棘手,而且不是一项简单的任务。最重要的是,实现 LFU 缓存。

第一步,查看 Boost 实现以了解工作量。我不知道第二个的任何参考实现。

要解决迭代器失效问题,您始终可以选择间接进入另一个容器,但您应该尽量避免它,因为它会产生额外的成本并且会变得非常混乱。

【讨论】:

  • 如何使用*_heap 更新优先级?我认为不仅仅是priority_queue 不能在这里完成这项工作:标准堆函数不能。所以提问者需要不同的堆实现。不过,我可能是错的。
  • @SteveJessop 也许我在这里错了,但是在更新优先级调用 make_heap 之后应该修复堆。不过,这可能远非最佳。
  • 同意。这将恢复堆不变量,但它是O(n)。其他堆实现可以在O(log n)中增加/减少/更新。
  • @SteveJessop 我在答案中添加了这一点,希望它变得更好一些。
  • @Blackhex: make_heap 可以在每次更新时调用。它所做的只是重新排序你给它的范围内的元素。不需要在插入时调用它,因为push_heap 可以有效地处理插入。
【解决方案2】:

比保留两个数据结构更简单的方法:

  • 只需保留一个映射,它将您的键映射到它们的值/使用计数对。
  • 缓存已满时:
    • 为地图元素创建一个迭代器向量 (O(n))
    • 使用std::nth_element 找出最差的10% (O(n))
    • 将它们全部从地图中删除 (O(n log n))

因此,向缓存中添加新元素是常见情况O(log n),最坏情况O(n log n),摊销O(log n)

在 LFU 缓存中删除最差的 10% 可能有点过激,因为新条目必须进入前 90%,否则它们会被删除。再说一次,如果您只删除一个元素,那么新条目仍然需要在下一个新条目之前从底部下降,否则它们会被删除,并且他们这样做的时间更少。因此,取决于为什么 LFU 是适合您的缓存策略,我对它的更改可能是错误的策略,或者仍然可以。

【讨论】:

  • @DeadMG:好点,提问者指定了 STL,所以肯定有一个可用的。 C++03 中没有(没有 Boost 或 TR1)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-31
  • 2019-04-01
  • 2023-04-07
  • 1970-01-01
  • 1970-01-01
  • 2018-08-12
  • 1970-01-01
相关资源
最近更新 更多