84 LRU缓存-最近最少使用策略(LRU Cache)

1 题目

题目:LRU缓存-最近最少使用策略(LRU Cache)
描述:为最近最少使用(LRU)缓存策略设计一个数据结构,它应该支持以下操作:获取数据和写入数据。
get(key) 获取数据:如果缓存中存在key,则获取其数据值(通常是正数),否则返回-1。
set(key, value) 写入数据:如果key还没有在缓存中,则设置或插入其数据值。当缓存达到上限,它应该在写入新数据之前删除最近最少使用的数据用来腾出空闲位置。
最终, 你需要返回每次 get 的数据.

lintcode题号——134,难度——hard

样例1:

输入:
LRUCache(2)
set(2, 1)
set(1, 1)
get(2)
set(4, 1)
get(1)
get(2)
输出:[1,-1,1]
解释:
cache上限为2,set(2,1),set(1, 1),get(2) 然后返回 1,set(4,1) 然后 delete (1,1),因为 (1,1)最少使用,get(1) 然后返回 -1,get(2) 然后返回 1。

样例2:

输入:
LRUCache(1)
set(2, 1)
get(2)
set(3, 2)
get(2)
get(3)
输出:[1,-1,2]
解释:
cache上限为 1,set(2,1),get(2) 然后返回 1,set(3,2) 然后 delete (2,1),get(2) 然后返回 -1,get(3) 然后返回 2。

2 解决方案

2.1 思路

  数据结构的设计题,最近最少使用——使用频率低的元素排在前面,按照题意需要底层结构能够移动元素到末尾,考虑使用链表来减少移动带来的开销,底层元素是(key, value)的二元组,可以考虑将二元组包装成一个类,再加上next指针,还需要支持get和set操作,由于链表的查找需要遍历,get和set都需要查找当前元素是否已经存在,为了减少耗时,考虑使用哈希表来支持get和set操作。

因为在移动节点到链表尾部的操作中,需要修改前一个节点的next指针,所以需要有从当前节点能够获取前一个节点的方式。本题底层数据结构可以使用单链表或者双链表,单链表需要用map映射来记录每个节点的前一节点;双链表用prev指针记录每个节点的前一节点。

2.3 时间复杂度

  使用链表做为底层数据结构,get操作在哈希表中查找耗时O(1),并移动链表中的一个节点耗时O(1),所以get操作的时间复杂度为O(1);set操作在哈希表中查找耗时O(1),并移动链表中的一个节点耗时O(1),所以set操作的时间复杂度也为O(1)。

由于哈希表的优势,所以大大降低了时间复杂度。

2.4 空间复杂度

  使用了哈希表和链表数据结构,空间复杂度为O(n)。

3 源码

  使用双链表的方式更好理解一些,单链表在移动节点的同时还需要对哈希表进行维护。

3.1 双链表算法

细节:

  1. 最近最少使用——用得最少的节点放在最前面,注意链表顺序。
  2. 因为头节点会变动,所以需要新建一个dummyNode,链表的头部为dummyNode的next。
  3. 因为需要把中间节点移至尾部,所以需要一个endNode来指示尾部的位置。
  4. get和set操作的时候可以先进行链表的移动和插入,将被操作节点移动至尾部,再获取和改动尾节点的值,这样更简便。
  5. 代码里unordered_map的作用是构建哈希表,在单链表算法中,哈希表不仅用于加速set和get操作中的查找。
class LRUCache {
public:
    class Node
    {
        public:
        Node(int key, int val) : key(key), val(val) {}
        int key = 0;
        int val = 0;
        Node * prev = nullptr;
        Node * next = nullptr;
    };
    int capacity = 0;
    Node * dummyNode = nullptr; // 链表第一个元素的上一个元素
    Node * endNode = nullptr; // 链表最后一个元素的下一个元素
    unordered_map<int, Node *> nodeMap; // 用于快速查找

    // 移除头部节点
    void removeFrontNode()
    {
        Node * node = dummyNode->next;

        node->prev->next = node->next; // 移除node
        node->next->prev = node->prev;

        nodeMap.erase(node->key); // 更新nodeMap
    }

    // 将节点移至尾部
    void moveNodeToBack(Node * cur)
    {
        cur->prev->next = cur->next; // 移除cur
        cur->next->prev = cur->prev;
        
        Node * backNode = endNode->prev;
        backNode->next = cur; // 在backNode和endNode之间加入cur
        cur->next = endNode;
        endNode->prev = cur;
        cur->prev = backNode;
    }

    // 在末尾加入新节点
    void addNodeToBack(int key, int val)
    {
        Node * node = new Node(key, val);
        Node * backNode = endNode->prev;

        backNode->next = node; // 在backNode和endNode之间加入node
        node->next = endNode;
        endNode->prev = node;
        node->prev = backNode;

        nodeMap.insert({key, node}); // 更新nodeMap

        if (nodeMap.size() > capacity)
        {
            removeFrontNode();
        }
    }

    /*
    * @param capacity: An integer
    */LRUCache(int capacity) {
        // do intialization if necessary
        this->capacity = capacity;
        dummyNode = new Node(-1, -1);
        endNode = new Node(-1, -1);
        dummyNode->next = endNode;
        endNode->prev = dummyNode;
    }

    /*
    * @param key: An integer
    * @return: An integer
    */
    int get(int key) {
        // write your code here
    int result = -1;

    if (nodeMap.find(key) != nodeMap.end())
    {
        moveNodeToBack(nodeMap.at(key));
        result = endNode->prev->val; // 获取最后一个节点的值
    }

    return result;
    }

    /*
    * @param key: An integer
    * @param value: An integer
    * @return: nothing
    */
    void set(int key, int value) {
        // write your code here
    if (nodeMap.find(key) != nodeMap.end())
    {
        moveNodeToBack(nodeMap.at(key)); // 移动当前节点到末尾
        endNode->prev->val = value; // 修改最后一个节点的值
    }
    else
    {
        addNodeToBack(key, value);
    }
    }
};

3.2 单链表算法

细节:

  1. 最近最少使用——用得最少的节点放在最前面,注意链表顺序。
  2. 因为头节点会变动,所以需要新建一个dummyNode,链表的头部为dummyNode的next。
  3. 因为需要把中间节点移至尾部,所以需要一个backNode来指示尾部的位置。
  4. get和set操作的时候可以先进行链表的移动和插入,将被操作节点移动至尾部,再获取和改动尾节点的值,这样更简便。
  5. 代码里unordered_map的作用是构建哈希表,在单链表算法中,哈希表不仅用于加速set和get操作中的查找,作用还在于移动节点时能够获取到当前节点的前一个节点。

这里的backNode直接就是最后一个节点,注意与双链表的endNode(指向最后一个节点的下一个节点)不同。
哈希表的作用多了一项,即获取前一个节点,所以在节点移动时候也需要同时维护。

C++版本:

class LRUCache {
public:
    class Node
    {
        public:
        Node(int key, int val) : key(key), val(val) {}
        int key = 0;
        int val = 0;
        Node * next = nullptr;
    };
    int capacity = 0;
    Node * dummyNode = nullptr; // 链表第一个元素的上一个元素
    Node * backNode = nullptr; // 链表最后一个元素
    unordered_map<int, Node *> prevNodeMap; // 用于快速查找以及获取当前节点的上一个节点

    // 移除头部节点
    void removeFrontNode()
    {
        prevNodeMap.erase(dummyNode->next->key); // 更新prevNodeMap
        dummyNode->next = dummyNode->next->next; // 在链表中移除

        if (dummyNode->next != nullptr)
        {
            prevNodeMap.at(dummyNode->next->key)  = dummyNode; // 更新prevNodeMap
        }
    }

    // 将当前节点移至尾部
    void moveNodeToBack(Node * cur)
    {
        if (cur == backNode)
        {
            return; // 当前节点已经是最后一个节点
        }

        Node * prevNode = prevNodeMap.at(cur->key);
        prevNode->next = cur->next; // 移除当前节点
		
        backNode->next = cur; // 在末尾加入当前节点
        cur->next = nullptr;

        prevNodeMap.at(cur->key) = backNode; // 更新prevNodeMap
        prevNodeMap.at(prevNode->next->key) = prevNode;

        backNode = cur; // 更新尾节点
    }

    // 在末尾加入新节点
    void addNodeToBack(int key, int value)
    {
        Node * node = new Node(key, value);
        backNode->next = node;
        prevNodeMap.insert({key, backNode}); // 更新prevNodeMap
        backNode = node; // 更新尾节点

        // 如果超过缓存容量,则弹出头节点
        if (prevNodeMap.size() > capacity)
        {
            removeFrontNode();
        }
    }

    /*
    * @param capacity: An integer
    */LRUCache(int capacity) {
        // do intialization if necessary
        this->capacity = capacity;
        dummyNode = new Node(-1, -1);
        backNode = dummyNode;
    }

    /*
    * @param key: An integer
    * @return: An integer
    */
    int get(int key) {
        // write your code here
        int result = -1;

        if (prevNodeMap.find(key) != prevNodeMap.end()) // 若存在节点
        {
            moveNodeToBack(prevNodeMap.at(key)->next); // 将当前节点移至尾部
            result = backNode->val; // 再获取尾节点的值
        }

        return result;
    }

    /*
    * @param key: An integer
    * @param value: An integer
    * @return: nothing
    */
    void set(int key, int value) {
        // write your code here
        if (prevNodeMap.find(key) != prevNodeMap.end()) // 若存在节点
        {
            moveNodeToBack(prevNodeMap.at(key)->next); // 将当前节点移至尾部
            backNode->val = value; // 再修改尾节点的值
            return;
        }
        else // 未存在节点
        {
            addNodeToBack(key, value); // 在末尾加入新节点
        }
    }
};

猜你喜欢

转载自blog.csdn.net/SeeDoubleU/article/details/124656824
今日推荐