Algorithm 为什么对LRU缓存而不是Deque使用双链接列表和哈希映射?

Algorithm 为什么对LRU缓存而不是Deque使用双链接列表和哈希映射?,algorithm,data-structures,Algorithm,Data Structures,我已经使用传统的方法(双链表+哈希映射)在LeetCode上实现了LRU缓存问题的设计。对于不熟悉此问题的人,此实现类似于: 我理解为什么使用这种方法(两端快速移除/插入,中间快速访问)。我不理解的是,为什么人们会使用HashMap和Link列表,而只需使用基于数组的DeQE(Java java语言,C++简单地DEQE)。这个DIGO允许在两端方便插入/删除,并在中间快速访问,这正是LRU缓存所需的。您还将使用更少的空间,因为您不需要存储指向每个节点的指针 与Deque/ArrayDeque

我已经使用传统的方法(双链表+哈希映射)在LeetCode上实现了LRU缓存问题的设计。对于不熟悉此问题的人,此实现类似于:

我理解为什么使用这种方法(两端快速移除/插入,中间快速访问)。我不理解的是,为什么人们会使用HashMap和Link列表,而只需使用基于数组的DeQE(Java java语言,C++简单地DEQE)。这个DIGO允许在两端方便插入/删除,并在中间快速访问,这正是LRU缓存所需的。您还将使用更少的空间,因为您不需要存储指向每个节点的指针


与Deque/ArrayDeque方法相比,LRU缓存几乎普遍使用后一种方法设计(至少在大多数教程中是这样),这有什么原因吗?HashMap/LinkedList方法有什么好处吗?

LRU缓存的目的是在
O(1)
时间内支持两个操作:
get(key)
put(key,value)
,另外一个约束条件是最近使用最少的键首先被丢弃。通常,键是函数调用的参数,值是该调用的缓存输出

无论您如何处理这个问题,我们都同意您必须使用hashmap。您需要一个hashmap来将缓存中已经存在的键映射到
O(1)
中的值

为了处理最近使用最少的键被丢弃的附加约束,您可以使用LinkedList或ArrayQue。然而,由于我们实际上不需要访问中间位置,LinkedList更好,因为您不需要调整大小

编辑:

Timmermans先生在回答中讨论了为什么
ArrayDeques
不能用于
LRU缓存
,因为必须将元素从中间移动到末端。这里所说的是一个
LRU缓存的实现,它只使用
deque
中的appends和poplefts成功地在leetcode上提交。请注意,python的
collections.deque
是作为一个双链表实现的,但是我们只在
collections.deque
中使用操作,它们也是循环数组中的
O(1)
,因此算法保持不变

from collections import deque

class LRUCache:

    def __init__(self, capacity: 'int'):
        self.capacity = capacity
        self.hashmap = {}
        self.deque = deque()

    def get(self, key: 'int') -> 'int':
        res = self.hashmap.get(key, [-1, 0])[0]
        if res != -1:
            self.put(key, res)
        return res

    def put(self, key: 'int', value: 'int') -> 'None':

        self.add(key, value)
        while len(self.hashmap) > self.capacity:
            self.remove()

    def add(self, key, value):
        if key in self.hashmap:
            self.hashmap[key][1] += 1
            self.hashmap[key][0] = value
        else:
            self.hashmap[key] = [value, 1]
        self.deque.append(key)

    def remove(self):
        k = self.deque.popleft()
        self.hashmap[k][1] -=1
        if self.hashmap[k][1] == 0:
            del self.hashmap[k]
我同意Timmermans先生的观点,即使用LinkedList方法更可取,但我想强调的是,使用
ArrayDeque
构建LRU缓存是可能的

我和Timmermans先生之间的主要混淆是我们如何解释能力。我把capacity理解为缓存最后一个
N
get/put请求,而Timmermans先生把它理解为缓存最后一个
N
唯一项

上面的代码在
put
中确实有一个循环,这会减慢代码的速度-但这只是为了让代码符合缓存最后的
N
唯一项的要求。如果我们将代码缓存改为最后的
N
请求,我们可以将循环替换为:

if len(self.deque) > self.capacity: self.remove()
这将使它与链表变量一样快(如果不快的话)

无论
maxsize
被解释为什么,上述方法仍然作为
LRU
缓存工作-最不常用的元素首先被丢弃


我只想强调,以这种方式设计
LRU
缓存是可能的。源代码就在那里-尝试在Leetcode上提交它

当LRU缓存已满时,我们丢弃最近使用的

如果我们要丢弃队列前面的项目,那么我们必须确保前面的项目是最长时间未使用的项目

我们通过确保无论何时使用一个项目,都会将其放在队列的后面来确保这一点。前面的项目是最长时间没有移到后面的项目

为此,我们需要在每个
put
get
操作上维护队列:

  • 当我们
    将一个新项目放入缓存时,它将成为最近使用的项目,因此我们将它放在队列的后面

  • 当我们
    获取已在缓存中的项目时,它将成为最近使用的项目,因此我们将其从当前位置移动到队列的后面


将项目从中间移动到末尾不是deque操作,
ArrayDeque
界面不支持该操作。
ArrayDeque
使用的底层数据结构也不能有效地支持它。之所以使用双链表,是因为它们确实有效地支持此操作。

双链表还可以是
堆栈
队列
deque
,除了
链表
双链表
,因此非常方便。例如,Java的
LinkedList
就是这样一种实现。实际上,当您尝试执行
堆栈
队列
时,您会发现大多数操作都可以通过从一个实现良好的双链接列表中包装操作来完成。[续]如果大小经常更改,那么基于数组的结构可能需要经常重新调整大小,这是非常昂贵的,所以这对性能不好。不使用基于数组的结构的另一个主要原因是从非端添加/删除非常昂贵,需要移动以下元素,并且可能会导致调整大小。虽然链表没有这个问题,但是链接列表的问题是它不能快速访问中间的元素,但是如果只访问2个末端,那么双链接列表也是O(1)。@ EricWang只在LRU缓存中从开始和结束中移除。如果您已经编写了LRU缓存,不需要<代码> MOVEIOTtoONEX/<代码>操作,那么您可能已经编写了FIFO缓存,而不是LRU缓存。为什么