LRU缓存


哈希表+双链表实现LRU缓存机制

将有下面四个问题待解决:

  1. 什么是LRU缓存机制?

  2. 为什么要用哈希表存?

  3. 为什么要用双链表,而不用队列?

  4. 如何实现?


什么是LRU缓存机制

LRU_百度百科 (baidu.com)

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

先把题目贴出来吧

146. LRU 缓存 - 力扣(LeetCode)


为什么要用哈希表

因为很明显要用到键值对存key和value


为什么要用双链表而不用队列?

因为题目说,要在满足Cache满的时候将最久未使用过的关键字剔除,所以我一开始是打算用队列来实现的,只要容量满了就把队头元素中的key对应键值对删了(先进先出),但是题目说了 “删除最久未使用的关键字” ,关键点就在这,假设这个关键字是key,只要我们取出该元素,或者插入该元素,或者修改该元素就算使用过,这时候这个元素就不应该在队列的队头,而应该在队尾(刚使用过)。因此队列无法满足要求(wa一次: 8/22);且题目要求在O(1)内完成,而双链表就能在O(1)的平均时间复杂度里执行各种操作。


下面开始实现

初见参考b站【LeetCode 每日一题】146. LRU 缓存 | 手写图解版思路 + 代码讲解_哔哩哔哩_bilibili

准备双链表

//双链表数据结构
struct Node{
    int key, val;
    Node *pre;
    Node *next;

    Node(): key(0),val(0),pre(NULL),next(NULL){}
    Node(int _key,int _val): key(_key),val(_val),pre(NULL),next(NULL){}
};

初始化哈希表和双链表

Node *head, *tail;    //双链表虚拟头尾指针
   unordered_map<int, Node*> cache;
   int capacity, size;   //最大容量,当前元素个数

   LRUCache(int _capacity): capacity(_capacity),size(0){
       head = new Node();
       tail = new Node();
       head->next = tail;
       tail->pre = head;
   }

双链表删除节点以及插入节点到头部的操作

双链表这部分就不多说了,数据结构基础

//删除节点
    void removeNode(Node *node){
        node->pre->next = node->next;
        node->next->pre = node->pre;
    }

    //将节点插到链表头部
    void addToHead(Node* node){
        node->pre = head;
        node->next = head->next;
        head->next->pre = node;
        head->next = node;
    }

最后根据题目逻辑给出完整代码

注释很完整了

//双链表数据结构
struct Node{
    int key, val;
    Node *pre;
    Node *next;

    Node(): key(0),val(0),pre(NULL),next(NULL){}
    Node(int _key,int _val): key(_key),val(_val),pre(NULL),next(NULL){}
};

class LRUCache {
public:
    Node *head, *tail;    //双链表虚拟头尾指针
    unordered_map<int, Node*> cache;
    int capacity, size;   //最大容量,当前元素个数

    LRUCache(int _capacity): capacity(_capacity),size(0){
        head = new Node();
        tail = new Node();
        head->next = tail;
        tail->pre = head;
    }

    int get(int key) {
        //不存在该key键值对
        if(!cache.count(key)) return -1;

        //存在
        Node *node = cache[key];

        //取出后,算使用过,则更新其位置到链表头部
        removeNode(node);
        addToHead(node);
        return node->val;
    }

    void put(int key, int value) {
        if(cache.count(key)){
            //存在key,则修改key对应的value,更新使用状态
            Node* node = cache[key];
            node->val = value;

            removeNode(node);
            addToHead(node);
        }else {
            //不存在,则插入一个新节点,且更新状态

            //1.判断容量是否超过capacity,如果超了需要删去链表尾部元素还有哈希表的对应元素
            if(size == capacity){
                Node *removedNode = tail->pre;
                removeNode(removedNode);
                cache.erase(removedNode->key);    //注意删的不是当前要插入的key!!(这里找了好久的bug,要删掉的是链表头部节点对应的key)
                size--;
            }
            //2.未超过,则将新节点插入哈希表,且插入到链表
            Node *node = new Node(key, value);
            addToHead(node);
            cache[key] = node;
            size++;   
        }
    }

    //删除节点
    void removeNode(Node *node){
        node->pre->next = node->next;
        node->next->pre = node->pre;
    }

    //将节点插到链表头部
    void addToHead(Node* node){
        node->pre = head;
        node->next = head->next;
        head->next->pre = node;
        head->next = node;
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

JAVA 版本

public class LRUCache {
    //双链表  从左往右使用减少
    class DLinkedNode{
        int key, val;
        DLinkedNode pre;
        DLinkedNode next;
        public DLinkedNode(){}
        public DLinkedNode(int _key, int _val) {
            this.key = _key;
            this.val = _val;
        }
    }

    DLinkedNode tail, head;  //双链表头指针和尾部指针
    HashMap<Integer, DLinkedNode> cache = new HashMap<>();
    int size;    //当前元素数量
    int capacity;  //容量

    //1.初始化
    public LRUCache(int _capacity) {
        this.capacity = _capacity;
        this.size = 0;
        tail = new DLinkedNode();
        head = new DLinkedNode();
        head.next = tail;
        tail.pre = head;
    }
    
    public int get(int key) {
        DLinkedNode node = cache.get(key);

        if(node == null){
            //不存在key
            return -1;
        }else {
            //使用了该数,更新缓存
            deleteNode(node);
            addToHead(node);
        }
        return node.val;
    }
    
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);

        //如果存在,修改并更新缓存;
        if(node != null){
            node.val = value;
            deleteNode(node);
            addToHead(node);
        }else {
            //不存在
            //1.判断容量  达到最大容量,删除最近未使用节点(别忘了cache也要删)
            if(size == capacity){
                DLinkedNode removeNode = tail.pre;
                deleteNode(removeNode);
                cache.remove(removeNode.key);
                size--;
            }
            DLinkedNode newNode = new DLinkedNode(key, value);
            size++;
            addToHead(newNode);
            cache.put(key, newNode);
        }
    }

    //删除双链表中的节点
    public void deleteNode(DLinkedNode node){
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    //加入到链表头部
    public void addToHead(DLinkedNode node){
        node.pre = head;
        node.next = head.next;
        head.next.pre = node;
        head.next = node;
    }  
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

文章作者: KTpro
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 KTpro !
  目录