阅读 123

146 -- LRU缓存机制 - Java + Python

运用你所掌握的数据结构,设计和实现一个 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。之前在操作系统和Spring Boot中的缓存及实现原理源码解读 一文中已经对于LRU以及操作系统中其他的内存替换策略做了介绍,这里就不再赘述。简而言之,LRU每次都选择最少最近使用的数据进行替换。如下所示:


在这里插入图片描述

例如,当页面访问到第一个3时未命中,而此时缓存中已满。根据LRU的替换思想,选择最少最近未使用的进行替换,那么缓存中的0、1、2只有2使用次数最少,而且使用时间最早。

如果需要使用一种数据结构来实现LRU,那么我们需要考虑如下几个问题:

  • 如果维护缓存中不同数据之间的访问记录?
  • 如何维护页面访问的次数和时间来为替换时提供依据?

对于第一个问题来说,由于数据访问具有时序性,因此,我们可以使用双向链表的方式来维护数据之间的时序关系,越早访问的元素越靠近链表头部。对于第二个问题,我们需要根据访问的时间和次数对数据进行排列,并且要求时间复杂度为O(1),那么最好的选择就是哈希表。因此,整体上可以使用链表 + 哈希表的结构来实现LRU。而且,我们需要在O(1)的时间复杂度下完成数据的读和写,所以链表节点的应该包含数据的keyvalue两部分信息。另外,哈希表中只需要保存key,根据key就可以访问到链表中对应的value

链表 + 哈希表的数据结构正好对应Java中的LinkedHashMap,当然也可以自定义链表和哈希表,并建立两者之间的映射关系。下面直接使用LinkedHashMap来作为解题的选型,并且也会使用Python语言来自定义链表和哈希表实现LRU。

Java解题代码:

class LRUCache {
	// 缓存容量大小                                                                                       
    private int capacity;
    LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
    
    public LRUCache(int capacity) {
        // 初始化内存空间大小
        this.capacity = capacity;
    }
    
    public int get(int key) {
        // 首先判断给定的key是否存在
        if(cache.containsKey(key)){
            // 更改状态为最近使用
            makeRecently(key);
            // 直接从cache中取值
            return cache.get(key);
        }
        return -1;
    }
    
    public void put(int key, int value) {
        // 如果给定的key中存在值,则只需更新值
        if(cache.containsKey(key)){
            // 传入的value会覆盖掉旧的value
            cache.put(key, value);
            // 修改该key的状态为最近使用
            makeRecently(key);
            return;
        }
		
        // 如果此时缓存已满,则需要进行淘汰
        if(cache.size() >= this.capacity){
        	// 最老的数据即cache的首元素
            int oldestKey = cache.keySet().iterator().next();
            cache.remove(oldestKey);
        }
		// 否则,直接保存到缓存中
        cache.put(key, value);

    }

    public void makeRecently(int key){
        int value = cache.get(key);
		// 先移除在加到尾部
        cache.remove(key);
        cache.put(key, value);
    }
}
复制代码

Python解题代码[官方题解],这里使用了伪首部和伪尾部来设置链表的界限,并且将最近访问的元素作为链表的头部元素。如果改成了上面相同的逻辑,只需要修改更新链表插入元素的操作即可:

class DLinkedNode:
    def __init__(self, key=None, value=None):
        self.key = key
        self.value = value
        self.pre = None
        self.next = None


class LRUCache:

    def __init__(self, capacity):
        self.cache = dict()
        self.capacity = capacity
        self.size = 0
  
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head
        

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
            
        # 如果 key 存在,先通过哈希表定位,再移到头部
        node = self.cache[key]
        self.moveToHead(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        if key not in self.cache:
            # 如果 key 不存在,创建一个新的节点
            node = DLinkedNode(key, value)
            # 添加进哈希表
            self.cache[key] = node
            # 添加至双向链表的头部
            self.addToHead(node)
            self.size += 1
            if self.size > self.capacity:
                # 如果超出容量,删除双向链表的尾部节点
                removed = self.removeTail()
                # 删除哈希表中对应的项
                self.cache.pop(removed.key)
                self.size -= 1
        else:
            # 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node = self.cache[key]
            node.value = value
            self.moveToHead(node)
    
    def addToHead(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
    
    def removeNode(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def moveToHead(self, node):
        self.removeNode(node)
        self.addToHead(node)

    def removeTail(self):
        node = self.tail.prev
        self.removeNode(node)
        return node
复制代码
文章分类
后端
文章标签