力扣-146-LRU缓存

LRU(Least Recently Used)最近最少使用,缓存这个是在《操作系统》课程上学习过的概念,会有面试要求实现也有所耳闻

需要实现的方法有3个

  1. 初始化方法,以指定的正整数作为LRU缓存结构的初始化容量

  2. get方法,如果键在缓存中,就返回键值;否则返回-1

  3. put方法,key不存在直接插入键值;存在覆盖原值
    如果插入导致超过容量,则逐出最久未使用的关键字

很明显前两个是比较容易实现的,关键就是如何记录最久未使用,以及用于实现的底层数据结构的选型

题解思路

数据结构选型

这里使用hashmap来优化双向链表查询效率低下的问题

为什么选择双向链表?因为这里涉及了很多头(插入)尾(删除)操作,如果只是单向链表的话,删除尾节点会非常麻烦,需要遍历一遍链表
为什么不用数组?数组访问快,但是插入删除就需要大量地移动元素操作,恰恰这里又有大量的插入删除操作
为什么不用栈和队列?因为栈和队列只能单位置操作(栈顶和队尾(不过说回来对头插入其实是可以的吧))

代码逻辑

  • get:如果key存在,则首先查找到这个节点(通过hash映射定位到链表位置),(维护最近使用记录)将节点移动到链表的头部,最后返回节点值

  • put:如果key不存在,则在链表头插入节点,插入完成后检查节点数量是否超过最大容量,超过则删除链表和hashmap中的对应记录
    如果key存在,则找到后覆盖key值,同时将节点移动到链表头部

实现

这里有链表的技巧是:使用额外的“伪头部”和“伪尾部”标记界限,这样就不需要在插入和删除节点时检查相邻节点是否存在

第一遍,跟着敲写注释理解

// 自己实现一个双向链表
struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;

    // 这是构造函数
    // 我第一次见这种赋初值的语法
    DLinkedNode() :key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value) :key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {

private:
    // 用map二次封装了双向链表
    // 这里的key值int是什么?
    unordered_map<int, DLinkedNode*> cache;
    DLinkedNode* head;
    DLinkedNode* tail;
    int size;
    // 为什么这里要有一个size变量?直接访问容器大小属性不行吗
    int capacity;
public:
    LRUCache(int _capacity) :capacity(_capacity), size(0) {
        // 使用头尾伪节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }

    // 辅助方法,前两个一增一删又是为第三个移动到头部服务
    void addToHead(DLinkedNode* node) {
        // 先插入node再改head
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }
    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    void moveToHead(DLinkedNode* node) {
        removeNode(node);
    }
    DLinkedNode* removeTail() {
        // 将伪伪节点的前一个真伪节点删除,并返回被删除掉的元素
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }

    int get(int key) {
        if (!cache.count(key)) {
            // 如果在map中对key技术不为0
            // 说明key存在,返回-1
            return -1;
        }
        // 如果存在,则先将节点移动到头部,再返回键值
        // 将找到的节点复制一份,浅拷贝仅复制了引用
        // 这里为什么要以这种方式创建一个节点?其实不用这个局部变量也可以吧
        // hashmap可以用key值作为下标访问吗?
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (!cache.count(key)) {
            // 如果key不存在
            DLinkedNode* node = new DLinkedNode(key, value);
            // 向hash表添加还能这么添加吗
            cache[key] = node;
            ++size;
            if (size > capacity) {
                // 如果新增节点后大小大于容量,就把链表中的最后一个删除
                DLinkedNode* removed = removeTail();
                // 删除哈希表中的元素
                cache.erase(removed->key);// 链表中其实没有真的删除,只是失去了链接关系,那么垃圾回收是怎么执行的
                // new完delete,防止内存泄露
                delete removed;
                --size;
            }
        }
        else {
            // 如果key存在
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }
};

第二遍,边瞄边写

上面第一遍的代码有些问题,主要是漏了两句语句

以下是第二遍的代码

// 自行实现一个双向链表
    struct DLinkedList{
        int key;
        int value;
        DLinkedList* prev;
        DLinkedList* next;
        // 这里结构体居然有构造函数,真是跟class越来越像了
        // 下面分别是有参和无参的构造函数,而且是用的新语法
        DLinkedList():key(0),value(0),prev(nullptr),next(nullptr){}
        DLinkedList(int _key,int _value):key(_key),value(_value),prev(nullptr),next(nullptr){}
    };

class LRUCache {
    // 本题中使用hashmap封装双向链表实现底层数据结构
    // 最近最少使用,链表中的头表示最近使用的节点数据,顺序表示使用时间的远近
    // 而hashmap则是为了加快链表的检索速度
    private:

    unordered_map<int,DLinkedList*> map;
        DLinkedList* head;
        DLinkedList* tail;
        int size;
        int capacity;

public:
    LRUCache(int _capacity):size(0),capacity(_capacity) {
        // 这里要对两个节点初始化
        head = new DLinkedList();
        tail = new DLinkedList();

        head->next=tail;
        tail->prev=head;

    }

    // 三个辅助方法
    void addToHead(DLinkedList* node){
        // size++要不早写在这里?不要
        node->prev = head;
        node->next = head->next;
        head->next->prev=node;
        head->next=node;
    }

    void removeNode(DLinkedList* node){
        // 
        node->prev->next=node->next;
        node->next->prev=node->prev;
    }

    void moveToHead(DLinkedList* node){
        // 
        removeNode(node);
        addToHead(node);
    }

    DLinkedList* removeTail(){
        // 这里使用一个局部变量主要是为了删除后返回,不然删了之后就没有指针指向,就找不到了
        DLinkedList* node = tail->prev;
        removeNode(node);
        return node;
    }
    
    int get(int key) {
        // 如果key不存在,返回-1
        if(!map.count(key)){
            return -1;
        }else{
            DLinkedList* node = map[key];
            moveToHead(node);
            return node->value;
        }
        // 如果key存在,返回key值并将节点移动到链表头
    }
    
    void put(int key, int value) {
        // 如果key不存在,则插入节点到链表头
        if(!map.count(key)){
            DLinkedList* node = new DLinkedList(key,value);
            // 这里先插入到了哈希表而不是链表,中,如果有原值则会被覆盖
            map[key]=node;
            addToHead(node);
            ++size;
            if(size>capacity){
                DLinkedList* removed =  removeTail();
                map.erase(removed->key);
                delete removed;
                --size;
            }
        }else{
            // 如果key存在就覆盖value,并且把节点移动到链表头
            DLinkedList* temp = map[key];
            temp->value = value;
            moveToHead(temp);
        }
    }
};

感觉这题本质上更像是“实现某种数据结构”的题型

posted @ 2022-07-12 11:27  YaosGHC  阅读(148)  评论(0)    收藏  举报