天天看点

[LeetCode]146. LRU缓存机制

题目

运用你所掌握的数据结构,设计和实现一个

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 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。具体方法如下:

1)对于 get 操作,首先判断 key 是否存在:

1.1)如果 key 不存在,则返回 -1;

1.2)如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

2)对于 put 操作,首先判断 key 是否存在:

2.1)如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

2.2)如果 key 不存在,先判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项。再使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。

上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步,都可以在 O(1) 时间内完成。

在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

复杂度分析:

时间复杂度:对于 put 和 get 都是 O(1)。

空间复杂度:O(capacity),因为哈希表和双向链表最多存储 capacity 个元素。

代码

# 定义双向链表结构
class Node:
    def __init__(self, key, val):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None


class LRUCache:

    def __init__(self, capacity: int):
        self.dic = {}
        self.capacity = capacity
        # 初始化头节点
        self.head = Node(None, None)
        # 初始化尾节点
        self.tail = Node(None, None)
        # 将头节点和尾节点连接起来
        self.head.next = self.tail
        self.tail.prev = self.head


    # 在链表头部添加节点
    def insert(self, node):
        node.next = self.head.next
        node.prev = self.head
        temp = self.head.next
        self.head.next = node
        temp.prev = node

        
    # 删除节点
    def delete(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev


    def get(self, key: int) -> int:
        # 如果不在字典中,返回-1
        if not key in self.dic:
            return -1
        node = self.dic[key]
        # 先删除,再插入
        self.delete(node)
        self.insert(node)
        return node.val


    def put(self, key: int, value: int) -> None:
        # 如果在字典中,直接更改value,再执行删除,插入两步骤,相当于把节点移到了双向链表的开头。
        if key in self.dic:
            node = self.dic[key]
            node.val = value
            self.delete(node)
            self.insert(node)
            # return
        # 如果不在字典中
        else:
            # 先看缓存满了没有,如果达到缓存容量限制,删除最久未使用的数据值,即最后一个节点
            if len(self.dic) == self.capacity:
                node = self.tail.prev
                self.delete(node)
                del self.dic[node.key]
            # 插入节点
            node = Node(key, value)
            self.dic[key] = node
            self.insert(node)





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