【问题标题】:How to design a latest recently used cache?如何设计最近使用的最新缓存?
【发布时间】:2012-01-09 03:19:50
【问题描述】:

如何设计一个最近使用的缓存?

假设您访问了一些项目。你需要设计一个数据结构来保存 这些项目。每个项目都与最近访问时间相关联。

每次访问一个项目时,在数据结构中检查它。如果该项目已在缓存中,则更新其访问时间。否则,将其插入缓存。缓存大小是固定的,如果已满,则删除最旧的项。

我的解决方案:

  1. 使用地图

  2. 初始化:使用 f(visitTime) 降序对地图进行排序。 O(nlg n)

  3. 如果一个项目被访问,用 O(lg n) 在地图中搜索它。

  4. 如果已经在地图中,更新时间 O(1)。排序地图 O(lg n)。

  5. 如果不是,则将其插入地图然后排序。 O(lg n)

  6. 如果地图大小>固定大小,删除最后一个元素O(1)。

另一种解决方案:

  1. 使用哈希表

  2. 排序 O(n lgn)。

  3. 如果一个项目被访问,用 O(1) 在 talbe 中搜索它。

  4. 如果已经在表中,更新时间O(1)。对表格进行排序 O(n lg n)。

  5. 如果不是,则将其插入表中,然后排序。 O(n lg n)

  6. 如果表格大小>固定大小,删除最后一个元素O(1)。

有更好的解决方案吗?在) ?

【问题讨论】:

  • “排序”是什么意思?为什么要对 (unordered_) 地图进行排序(并复制到哪里?)?线性搜索将找到最旧的要删除的项目。
  • 您打算在这里使用多大的缓存? Big-O 处理渐近复杂性,但缓存通常足够小,以至于复杂性很少是唯一(甚至通常是最重要的)问题。

标签: c++ algorithm data-structures hash lru


【解决方案1】:

如果您使用双向链表,您将获得 O(1) 插入(搜索后)、O(1) 删除、O(n) 搜索。

假设您在前面插入新项目:

如果缓存没有满,就加到前面(O(1))。

如果需要更新一个item,找到它(O(n)),从链表中移除(O(1)),然后添加到最前面(O(1))。

如果需要删除最旧的插入新项,删除结束元素(O(1)),插入到最前面(O(1))【注意:这里需要先搜索列表判断项目是否已经在缓存中,所以 O(n)].

链接列表也可以为您提供相同的时间,因为搜索会将您留在最后一个元素。

【讨论】:

  • 由于 O(n) 搜索步骤,这不能很好地扩展。另一个答案中讨论的 Python 实现将其减少到 O(1) 步骤。
  • 他要求 O(n),所以这就是我给他的。也是一个简单的解决方案。
  • 很公平。希望其他搜索这个问题的人能有更高的目标:-)
  • 下面是可以在 C++ 中实现的 O(1) 解决方案的描述。您只需要一个map 和一个list
【解决方案2】:

Python's LRU cache 有 O(1) 的插入、删除和搜索。它的设计使用双向链接的条目列表(按从旧到新排列)和哈希表来定位特定链接。

这是一个简化(但速度很快)的版本,包含 40 行以下非常基本的 Python。将 Python 的解决方案翻译成 C++ 应该不难:

class LRU_Cache(object):

    def __init__(self, original_function, maxsize=1000):
        self.original_function = original_function
        self.maxsize = maxsize
        self.mapping = {}

        PREV, NEXT, KEY, VALUE = 0, 1, 2, 3
        self.head = [None, None, None, None]        # oldest
        self.tail = [self.head, None, None, None]   # newest
        self.head[NEXT] = self.tail

    def __call__(self, *key):
        PREV, NEXT, KEY, VALUE = 0, 1, 2, 3
        mapping, head, tail = self.mapping, self.head, self.tail
        sentinel = object()

        link = mapping.get(key, sentinel)
        if link is sentinel:
            value = self.original_function(*key)
            if len(mapping) >= self.maxsize:
                oldest = head[NEXT]
                next_oldest = oldest[NEXT]
                head[NEXT] = next_oldest
                next_oldest[PREV] = head
                del mapping[oldest[KEY]]
            last = tail[PREV]
            link = [last, tail, key, value]
            mapping[key] = last[NEXT] = tail[PREV] = link
        else:
            link_prev, link_next, key, value = link
            link_prev[NEXT] = link_next
            link_next[PREV] = link_prev
            last = tail[PREV]
            last[NEXT] = tail[PREV] = link
            link[PREV] = last
            link[NEXT] = tail
        return value

if __name__ == '__main__':
    p = LRU_Cache(ord, maxsize=3)
    for c in 'abcdecaeaa':
        print(c, p(c))

【讨论】:

  • 令人印象深刻。改变 "if size > maxsize:" => "if size >= maxsize:" 怎么样?
【解决方案3】:

您可以使用 java.util.LinkedHashSet 在 Java 中完成此操作。它是一个哈希表和一个链表,它保留了插入项目的顺序。如果密钥分散运行良好,您应该获得(预期的)恒定时间查找、插入和删除。

您可能还想查看WeakHashMap,它实现了一种可以对元素进行垃圾收集的自动化机制。

【讨论】:

  • +1 这本质上是 Python 版本所做的。这比没有哈希表的双向链表要好得多。
【解决方案4】:

使用共享相同数据的两个集合。有一个哈希表和一个列表。使用 hashtable 验证 item 是否存在,并在列表中找到它(hash map 的值是列表迭代器)。使用列表来维护项目之间的顺序。同步两个集合(从列表中删除项目时,从哈希表中删除相应的项目)。列表迭代器必须保证在您重新定位列表中的项目时它不会改变。

编辑:std::list 迭代器在元素的添加和删除过程中都是有效的,前提是不删除正在引用的元素迭代器。请参阅 Wikipedia 中 Capacity and Allocation 部分的最后几行。

【讨论】:

  • 如果列表项被重定位,迭代器将被改变。每次在前面插入一个新项目,或从列表中删除一个新项目时,部分甚至所有列表项目的位置都可能发生变化。如何保留哈希表以获取每个项目的新位置?它是 O(n)。
  • @user1002288,我更新了我的答案。 std::list 迭代器是安全的。
【解决方案5】:

您实际上不必对容器进行分类。只需将项目添加到地图或矢量中,然后线性遍历它以找到所需的项目(或最旧的项目)。

那么它将是O(n)

【讨论】:

    【解决方案6】:

    看看boost::multi_index。它显示的示例之一是MRU List

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-08
      • 2018-01-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      相关资源
      最近更新 更多