LRU Cache -- LeetCode

本文介绍了一种高效实现LRU缓存的方法,利用哈希表结合双向链表的数据结构,实现了平均时间复杂度为O(1)的get和set操作。文章详细解释了该数据结构的设计原理及其实现细节。
原题链接: http://oj.leetcode.com/problems/lru-cache/
这是一道非常综合的题目,主要应用在操作系统的资源管理中。
按照题目要求,要实现get和set功能,为了满足随机存储的需求我们首先想到的一般是用数组,如果用链表会有O(n)的访问时间。然而他又有另一个条件就是要维护least used的队列,也就是说经常用的放在前面,用的少的放在后面。这样当资源超过cache的容积的时候就可以把用得最少的资源删掉。这就要求我们要对节点有好的删除和插入操作,这个要求又会让我们想到用链表,因为数组的删除和插入是O(n)复杂度的。
那么我们能不能维护一个数据结构使得访问操作和插入删除操作都是O(1)复杂度的呢?答案是肯定的。这个数据结构比较复杂,是用一个hash表加上一个双向链表来实现。基本思路是这样的,用一个hash表来维护结点的位置关系,也就是hash表的key就是资源本身的key,value是资源的结点(包含key和value的信息)。然后把结点维护成一个双向链表构成的队列,这样子如果我们要访问某一个结点,那么可以通过hash表和key来找到结点,从而取到相应的value。而当我们想要删除或者插入结点时,我们还是通过hash表找到结点,然后通过双向链表和队列的尾结点把自己删除同时插入到队尾。通过hash表访问结点我们可以认为是O(1)的操作(假设hash函数足够好),然后双向链表的插入删除操作也是O(1)的操作。如此我们便实现了用O(1)时间来完成所有LRU cache的操作。空间上就是对于每一个资源有一个hash表的的项以及一个对应的结点(包含前后指针和资源的<key, value>)。代码如下:
public class LRUCache {
    class Node
    {
        Node pre, next;
        int key;
        int val;
        public Node(int key, int value)
        {
            this.key = key;
            this.val = value;
        }
    }
    
    private int capacity;
    private int num;
    private HashMap<Integer, Node> map;
    private Node first, last;
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        num = 0;
        map = new HashMap<Integer, Node>();
        first = null;
        last = null;
    }
    
    public int get(int key) {
        Node node = map.get(key);
        if(node == null)
            return -1;
        else if(node!=last)
        {
            if(node == first)
                first = first.next;
            else
                node.pre.next = node.next;
            node.next.pre = node.pre;
            last.next = node;
            node.pre = last;
            node.next = null;
            last = node;
        }
        return node.val;
    }
    
    public void set(int key, int value) {
        Node node = map.get(key);
        if(node != null)
        {
            node.val = value;
            if(node!=last)
            {
                if(node == first)
                    first = first.next;
                else
                    node.pre.next = node.next;
                node.next.pre = node.pre;
                last.next = node;
                node.pre = last;
                node.next = null;
                last = node;
            }
        }
        else 
        {
            Node newNode = new Node(key,value);

            if(num>=capacity)
            {
                map.remove(first.key);
                first = first.next;
                if(first!=null)
                    first.pre = null;
                else
                    last = null;
                num--;    
            }
            if(first == null || last == null)
            {
                first = newNode;
            }
            else
            {
                last.next = newNode;
            }
            newNode.pre = last;
            last = newNode;
            map.put(key,newNode);
            num++;
        }

    }
}
实现的时候还是有很多细节的,因为我们不经常使用双向链表,插入删除操作要维护前后指针,并且同时要维护成队列,增加了许多注意点。这道题是一道很实际的题目,思路和数据结构都是很适合面试的题目,但是代码量有些偏大,所以一般只在onsite的时候有可能遇到,可能也不会让你完整地写出全部代码,主要还是讲出维护的数据结构和各种操作复杂度的分析。
### LRU缓存策略使用链表实现的原理 LRU(Least Recently Used,最近最少使用)是一种常见的缓存淘汰策略。它通过跟踪每个缓存项的访问时间,确保在缓存容量达到上限时,优先淘汰最长时间未被访问的项。使用链表实现LRU缓存的核心思想是结合哈希表和双向链表的数据结构[^1]。 #### 数据结构的选择 - **哈希表**:用于快速查找缓存项。键为缓存项的标识符,值为指向双向链表节点的指针。 - **双向链表**:用于维护缓存项的访问顺序。最近访问的项放在链表头部,最久未访问的项放在链表尾部。 #### 核心操作 1. **插入新项**: - 如果缓存未满,则将新项插入到链表头部,并更新哈希表。 - 如果缓存已满,则删除链表尾部的项(即最久未使用的项),然后将新项插入到链表头部并更新哈希表[^2]。 2. **访问已有项**: - 通过哈希表快速定位目标项。 - 将该项从当前位置移除,并插入到链表头部,以标记其为最近访问的项。 3. **删除项**: - 直接通过哈希表找到目标项,将其从链表中移除,并从哈希表中删除对应的键值对。 #### 示例代码 以下是一个简单的LRU缓存实现示例: ```python class Node: def __init__(self, key, value): self.key = key self.value = value self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.cache = {} self.head = Node(0, 0) self.tail = Node(0, 0) self.head.next = self.tail self.tail.prev = self.head def _add_node(self, node: Node): """将节点添加到链表头部""" node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _remove_node(self, node: Node): """从链表中移除节点""" prev = node.prev next = node.next prev.next = next next.prev = prev def _move_to_head(self, node: Node): """将节点移动到链表头部""" self._remove_node(node) self._add_node(node) def _pop_tail(self): """弹出链表尾部节点""" res = self.tail.prev self._remove_node(res) return res def get(self, key: int) -> int: if key in self.cache: node = self.cache[key] self._move_to_head(node) return node.value return -1 def put(self, key: int, value: int): if key in self.cache: node = self.cache[key] node.value = value self._move_to_head(node) else: if len(self.cache) >= self.capacity: tail = self._pop_tail() del self.cache[tail.key] new_node = Node(key, value) self.cache[key] = new_node self._add_node(new_node) ``` #### 性能分析 - 插入、删除和访问操作的时间复杂度均为 \(O(1)\),因为哈希表提供了常数时间的查找能力,而双向链表支持常数时间的节点插入和删除[^3]。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值