📜  LRU 近似(二次机会算法)(1)

📅  最后修改于: 2023-12-03 14:44:04.419000             🧑  作者: Mango

LRU 近似(二次机会算法)

什么是 LRU?

LRU,全称为 Least Recently Used(最近最少使用)算法,是一种常见的缓存淘汰算法。其思想是根据数据访问的时间顺序,将最近最少使用的数据淘汰掉,从而保留最有用数据的算法。

LRU 实现原理

LRU 缓存淘汰算法的核心思想是维护一张缓存清单,将最近被访问的数据插入到缓存清单的队首,当缓存清单满了之后再加入新数据时,淘汰掉缓存队尾的数据即可。

LRU 算法的核心是有序链表,链表头部是最常访问的数据,链表尾部是最近最少使用的数据。每次新访问一个数据时,如果数据在链表中存在,则将该数据移到链表头部;如果不存在,则在链表头部插入该数据。每次淘汰数据时,删除链表尾部的数据即可。

而 LRU 近似 (二次机会算法) 则是对 LRU 算法进行了优化,"LRU 近似(二次机会算法)"本质上是把节点使用的频率用两个 bit 来进行记录,第一个 bit 充当一个访问位,记录节点是否被访问过,每次被访问后该 bit 会被置为 1;第二个 bit 充当一个过期位,记录节点是否过期,初始值为 0。所谓过期,是指双链表的元素被访问而且已经经过了一段时间,所以需要将元素标记为过期,等待被淘汰。

LRU 近似 (二次机会算法) 实现原理

LRU 近似 (二次机会算法) 的实现基于双向链表和哈希表,使用双向链表可以保证在插入、查找和删除操作上的高效性,而哈希表可以迅速定位某个节点是否存在。

算法初始化时,哈希表和双链表都是空的。每次访问节点时,先在哈希表中查找该节点是否存在。如果存在,则将该节点从链表中移动到链表头部,并将“访问位”置为 1;如果不存在,则在链表头部插入该节点并将其“访问位”置为 1。如果链表已满,那么需要选择最长时间没有被访问过的节点进行删除,如果该节点的“过期位”为 0,则将其置为 1;若该节点已经“过期位”为 1,则将其删除。

代码实现

以下为示例代码实现,其中 hash_map 为 STL 提供的哈希表实现。

#define MAXSIZE 5
typedef struct TListNode
{
    int key; // 节点 key 值
    int value; // 节点 value 值
    int bit_access; // 节点第一个 bit(访问位)
    int bit_expire; // 节点第二个 bit(过期位)
    struct TListNode *prev; // 前驱节点
    struct TListNode *next; // 后继节点
}ListNode;

class LRUCache 
{
    private:
        int curSize; // 当前已使用的缓存空间
        int maxSize; // 缓存空间的最大容量
        ListNode *head; // 头结点
        ListNode *tail; // 尾结点
        unordered_map<int, ListNode*> hash_map; // 哈希表
        void removeNodeFromList(ListNode* node) // 从链表中删除某个节点
        {
            node->prev->next = node->next;
            node->next->prev = node->prev;
        }
        void insertNodeAtHead(ListNode* node) // 将节点插入链表头部
        {
            node->next = head->next;
            node->prev = head;
            head->next->prev = node;
            head->next = node;
        }

    public:
        LRUCache(int capacity) 
        {
            curSize = 0;
            maxSize = capacity;
            head = new ListNode;
            tail = new ListNode;
            head->next = tail;
            tail->prev = head;
        }
    
        int get(int key) 
        {
            if(hash_map.find(key) == hash_map.end())
            {
                return -1; // 如果哈希表中不存在该节点,则返回 -1
            }
            else
            {
                ListNode* node = hash_map[key];
                node->bit_access = 1; // 将节点访问位置为 1
                removeNodeFromList(node);
                insertNodeAtHead(node);
                return node->value;
            }
        }
    
        void put(int key, int value) 
        {
            if(hash_map.find(key) == hash_map.end())
            {
                ListNode* newNode = new ListNode;
                newNode->key = key;
                newNode->value = value;
                newNode->bit_access = 1; // 将节点访问位置为 1
                newNode->bit_expire = 0; // 将节点过期位置为 0
                hash_map[key] = newNode;  // 在哈希表中插入该节点
                insertNodeAtHead(newNode); // 将节点插入链表头部
                ++curSize; // 更新当前已使用的缓存空间
                if(curSize > maxSize) // 如果当前已使用的缓存空间超过了最大容量
                {
                    ListNode* lastNode = tail->prev;
                    while(lastNode != head) // 寻找最长时间没有被访问过的节点
                    {
                        if(lastNode->bit_access == 1) // 如果该节点的访问位为 1,则将其访问位置为 0
                        {
                            lastNode->bit_access = 0;
                        }
                        else if(lastNode->bit_expire == 0) // 如果该节点的过期位为 0,则将其过期位置为 1,表示待删除
                        {
                            lastNode->bit_expire = 1;
                            break;
                        }
                        else // 如果该节点的过期位为 1,则删除该节点
                        {
                            ListNode* nodeToRemove = lastNode;
                            lastNode = nodeToRemove->prev;
                            removeNodeFromList(nodeToRemove);
                            hash_map.erase(nodeToRemove->key);
                            delete nodeToRemove;
                            --curSize;
                        }
                    }
                }
            }
            else
            {
                ListNode* node = hash_map[key];
                node->value = value;
                node->bit_access = 1; // 将节点访问位置为 1
                removeNodeFromList(node);
                insertNodeAtHead(node);
            }
        }
};

以上为 LRU 近似 (二次机会算法) 的代码实现,算法的时间复杂度为 O(1),空间复杂度为 O(n)。