LeetCode-146-LRU缓存机制-Python

7次阅读

共计 2878 个字符,预计需要花费 8 分钟才能阅读完成。

146. LRU 缓存机制


题目来源:力扣(LeetCode)https://leetcode-cn.com/problems/lru-cache

题目


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

获取数据 get(key) – 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) – 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥 / 数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

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

解题思路


思路:哈希表、双向链表

首先,先解释一下 LRU,LRU(Least Recently Used),最近最少使用。它是一种缓存淘汰策略。

比如,正常来说缓存容量是有限的,如果希望删除一些内容时腾出空间时,这个时候就应该考虑删除哪些内容?哪些内容是没用的,可以删除的?哪些应该保留,是有用的?

LRU 这种策略,就认为最近使用的数据是有用的,而哪些长时间未使用的就是没用的,当缓存不足时,就会优先删除这些长时间未使用的。

这就是 LRU 这个策略的简单描述,当然还有其他的策略,比如 LFU 等。

再看本题,因为题目有个要求,【是否可以在 O(1) 时间复杂度内完成获取数据 get 和写入数据 put 操作?】

在这里就应该考虑,要实现时间复杂度 O(1) 内完成上面的操作,我们要使用的数据结构条件就应该具有以下的特点:查找、插入、删除都要快。

我们知道哈希表,查找速度快,但是没有顺序之分。而链表,插入删除快,有顺序,但是查找慢。此时,我们考虑将两者结合使用。

在这里,因为当缓存容量达到上限的时候,我们需要执行删除操作,此时删除节点,不仅要当前节点本身的指针,还需要前驱节点的指针,这里则需要使用双向链表,才能够在删除并查找前驱时,实现 O(1) 的时间复杂度。

那么现在来看,如果使用哈希表 + 双向链表实现功能设计:

  • 首先双向链表部分,按照使用顺序存储键值对。靠近头部为最近使用,靠近尾部为非最近使用。
  • 哈希表,键映射其在双向链表的位置。

具体实现的方法:

  • get:首先要判断 key 是否存在:

    • 不存在返回 -1
    • 存在时,此时对应的节点则定为最近使用。那么先定位它在双向链表的位置,将其移动到头部,返回该节点的值。
  • put:同样判断 key 是否存在:

    • 不存在,创建新节点,在双向链表头部添加该节点,同时也要添加到哈希表中。同时还需要判断双向链表容量是否超出,如果超出,将末尾节点删除,同时删除哈希表对应部分。
    • 存在时,通过哈希表定位在双向链表的位置,更新它的值,同时移动到双向链表头部。

具体的实现代码如下。

代码实现


class Node(object):
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class DoubleLinkedList(object):
    def __init__(self):
        self.head = Node(0, 0)
        self.tail = Node(0, 0)
        self.head.next = self.tail
        self.tail.prev = self.head
        self.size = 0
    
    def add_to_head(self, node):
        """添加节点到头部"""
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node
        self.size += 1
    
    def remove_node(self, node):
        """删除节点"""
        node.prev.next = node.next
        node.next.prev = node.prev
        self.size -= 1
    
    def remove_tail(self):
        """删除尾部节点"""
        if self.size == 0:
            return None
        node = self.tail.prev
        self.remove_node(node)
        return node
    
    def get_size(self):
        return self.size

class LRUCache:

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.hashmap = {}
        self.cache = DoubleLinkedList()

    def get(self, key: int) -> int:
        # 判断 key 是否存在,分情况处理
        if key not in self.hashmap:
            return -1
        # 通过哈希表定位其在双向链表的位置
        value = self.hashmap[key].value
        # 这里实现的逻辑在 put 操作体现
        # put 操作在键存在时,同样需要移至链表头部
        self.put(key, value)
        return value

    def put(self, key: int, value: int) -> None:
        # 先创建节点
        node = Node(key, value)
        # 同样判断 key 是否存在
        # 分情况处理
        if key in self.hashmap:
            self.cache.remove_node(self.hashmap[key])
            self.cache.add_to_head(node)
            self.hashmap[key] = node
        else:
            # 判断缓存容量是否不够
            if self.capacity == self.cache.get_size():
                # 删除最后的节点
                tail = self.cache.remove_tail()
                self.hashmap.pop(tail.key)
            # 添加到头部
            self.cache.add_to_head(node)
            self.hashmap[key] = node



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

实现结果


总结


  • 使用哈希表 + 双向链表,实现题意要求时间复杂度为 O(1) 的操作;
  • 双向链表按照使用顺序存储键值对,最近使用在头部,非最近使用在尾部;
  • 哈希表,键映射其存储在双向链表的位置。

欢迎关注微信公众号《书所集录》

正文完
 0