Leetcode算法题解——LRU缓存机制

Leetcode算法题解——LRU缓存机制

题目信息

链接:leetcode-cn.com/problem

题目描述:

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。

当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

PS:描述信息中”删除最近最少使用的数据值“表达可能会让人产生误解,此题中想表达的是”删除最长时间未使用的数据值“。

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

题目分析

在解题前,我们先对题目进行充分的分析。根据题干描述的信息,这是一道偏重于数据结构的题。

我们可以从题中所需的 key-value 类型数据中得出我们可能需要使用的数据结构是哈希表(Hash Table)。基础的哈希表虽具备读写 key-value 数据的功能,但是 key 的存储是无序的,而本题中当 LRU 存满时,再次存储时需要删除掉最久未使用的数据,这就需要我们的数据结构能够保存特定的顺序信息。因此,我们可以考虑一种有序的哈希表

通常,我们会使用双向链表去记录哈希表中键的顺序,每个键都有拥有指向前一个键的指针和指向后一个键的指针

来源:Wikipedia-双向链表

当我们进行 set & get 操作时,只需要把当前节点调整到链表尾,而需要 pop 操作的时候,将链表首弹出即可。

实现分析

在 Java 中,LinkedHashMap 是上述有序哈希表(双向链表+哈希表)的实现,网上有许多关于 LinkedHashMap 源码的讲解,例如:LinkedHashMap 的实现原理

在 Python 中,有一种类似的实现,是 OrderedDict 。和 Java 中的 LinkedHashMap 不同,OrderedDict 只会在 set 新键时进行顺序的调整,当键已经出现在 OrderedDict 时,其原生实现并不会主动调整顺序,需要我们使用额外的方法进行主动调整。

我们当然也可以手动造一个新的轮子来完整实现该数据结构,但是在我们真正学会了该数据结构后,实现也并不是特别难的事情。此处我采用了自己的思路,将实现轮子的精力花在了研读源码上。

代码实现

利用已有的轮子,能通过非常简洁的方式完成本问题,这也是 Python 的魅力所在吧哈哈。

from collections import OrderedDict


class LRUCache:
    """Implement LRUCache using OrderedDict"""
    def __init__(self, capacity: int):
        self._ordered_dict = OrderedDict()
        self._capacity = capacity

    def get(self, key: int) -> int:
        self._move_to_end_if_exist(key) 
        
        return self._ordered_dict.get(key, -1)
        
    def put(self, key: int, value: int) -> None:
        self._move_to_end_if_exist(key) 
        
        self._ordered_dict[key] = value 
        if len(self._ordered_dict) > self._capacity:
            self._ordered_dict.popitem(last=False)  # popitem支持弹出头部或尾部
            
    def _move_to_end_if_exist(self, key: int) -> None:
        if key in self._ordered_dict:
            self._ordered_dict.move_to_end(key)


# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

源码分析

由于我们使用的轮子实现的该功能,所以想学习到数据结构的精髓还是应该去翻阅源码,因此我们分析一下 OrderedDict 的核心源码。

OrderedDict 是一个继承自 dict 的对象,在 Python 中,dict 就是用哈希表实现的,因此 OrderedDict 本质上是一种更加强大的哈希表

class OrderedDict(dict):
    'Dictionary that remembers insertion order'
    ···

在 set 方法中,实现了双向链表插入操作。

    def __setitem__(self, key, value,
                    dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
        'od.__setitem__(i, y) <==> od[i]=y'
        # Setting a new item creates a new link at the end of the linked list,
        # and the inherited dictionary is updated with the new key/value pair.
        if key not in self:
            self.__map[key] = link = Link()
            root = self.__root
            last = root.prev
            link.prev, link.next, link.key = last, root, key
            last.next = link
            root.prev = proxy(link)
        dict_setitem(self, key, value)

在 del 方法中,实现了双向链表删除操作。

    def __delitem__(self, key, dict_delitem=dict.__delitem__):
        'od.__delitem__(y) <==> del od[y]'
        # Deleting an existing item uses self.__map to find the link which gets
        # removed by updating the links in the predecessor and successor nodes.
        dict_delitem(self, key)
        link = self.__map.pop(key)
        link_prev = link.prev
        link_next = link.next
        link_prev.next = link_next
        link_next.prev = link_prev
        link.prev = None
        link.next = None

此外还有 popitem 和 move_to_end 等 API 的实现,因为哈希表的实现已经通过继承 dict 完成,所以这些 API 的核心都在于双向链表的添加和删除。

    def popitem(self, last=True):
        '''Remove and return a (key, value) pair from the dictionary.

        Pairs are returned in LIFO order if last is true or FIFO order if false.
        '''
        if not self:
            raise KeyError('dictionary is empty')
        root = self.__root
        if last:
            link = root.prev
            link_prev = link.prev
            link_prev.next = root
            root.prev = link_prev
        else:
            link = root.next
            link_next = link.next
            root.next = link_next
            link_next.prev = root
        key = link.key
        del self.__map[key]
        value = dict.pop(self, key)
        return key, value

    def move_to_end(self, key, last=True):
        '''Move an existing element to the end (or beginning if last==False).

        Raises KeyError if the element does not exist.
        When last=True, acts like a fast version of self[key]=self.pop(key).

        '''
        link = self.__map[key]
        link_prev = link.prev
        link_next = link.next
        soft_link = link_next.prev
        link_prev.next = link_next
        link_next.prev = link_prev
        root = self.__root
        if last:
            last = root.prev
            link.prev = last
            link.next = root
            root.prev = soft_link
            last.next = link
        else:
            first = root.next
            link.prev = root
            link.next = first
            first.prev = soft_link
            root.next = link

主要参考资料

  1. Python官方文档 —— OrderedDict
  2. LinkedHashMap 的实现原理
编辑于 2019-02-27

文章被以下专栏收录