146. LRU緩存機制

題目描述

運用你所掌握的數據結構,設計和實現一個LRU (最近最少使用) 緩存機制。它應該支持以下操作: 獲取數據 get 和 寫入數據 put

  • 獲取數據 get(key) - 如果密鑰 (key) 存在於緩存中,則獲取密鑰的值(總是正數),否則返回 -1。
  • 寫入數據 put(key, value) - 如果密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據之前刪除最近最少使用的數據值,從而爲新的數據值留出空間。

進階:

你是否可以在 O(1) 時間複雜度內完成這兩種操作?

示例:

LRUCache cache = new LRUCache( 2 /* 緩存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 該操作會使得密鑰 2 作廢
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 該操作會使得密鑰 1 作廢
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

算法

這題的時間複雜度的好壞比較依賴於所選擇的數據結構。

LRU是操作系統中提出的一種應用於頁置換的算法,這裏不過多介紹,舉個實際例子即可知道本題要求實現的功能需要什麼步驟:

想象有一個隊列的最大允許空間爲3,
依次入隊的順序爲 2,3,2,1,2,4;求LRU算法下隊列的演變過程。
---------------------------------------------------
- 隊列初始爲空,2進入後隊列情況爲:2
- 隊列還有2個剩餘位置,3進入後隊列情況爲:2 3
- 隊列還有1個剩餘位置,這次入隊的數據爲2,它本來就已在隊列中,根據LRU算法,需要將2調到隊列末尾,因此隊列情況爲:3 2
- 隊列還有1個剩餘位置,這次入隊數據爲1,入隊後隊列情況爲:2 3 1
- 隊列已經沒有剩餘位置,但是入隊數據爲2,它本來就在隊列中,根據LRU算法,需要將2調到隊列末尾,因此隊列情況爲:3 1 2
- 隊列已經沒有剩餘位置,新進入的數據爲4,根據LRU需要淘汰最近最少被使用的數據,即隊首的數據3,更新後隊列情況爲:1 2 4
---------------------------------------------------
上面即爲LRU算法的一個例子

選擇hash表與雙向鏈表作爲實現主體功能的兩個數據結構,主要是因爲雙向鏈表便於插入刪除,而hash表可以較快查找到需要返回的value。具體一點,整個LRUCache可能長下面這樣:

代碼

#include <iostream>
#include <list>
#include <unordered_map>
using namespace std;
struct listNode{
    int key, value;
    listNode *pre, *next;
    listNode(int _key, int _value): key(_key), value(_value)
    { 
        pre = next = NULL;
    }
};

class LRUCache {
public:
    // hash_table末端保存最近剛被使用的節點,前端保存最近最少被使用節點
    unordered_map<int, listNode*> hash_table;
    listNode *head, *tail;
    int cap, size;

    LRUCache(int capacity) {
        cap = capacity;
        size = 0;
        head = new listNode(-1, -1);
        tail = new listNode(-1, -1);
        head->next = tail;
        tail->pre = head;
    }
    
    int get(int key) {
        if (hash_table.find(key) == hash_table.end())
            return -1;
        else
        {
            // 記錄該ID指向節點的指針
            listNode *tmp = hash_table[key];

            /*** 更改節點在表中的順序 ***/

            // 1. 刪除hash_table[key]
            delNode(tmp);
            // 2. 將hash_table[key]插入末尾
            pushNodeBack(tmp);

            return tmp->value;
        }
    }
    
    void put(int key, int value) {
        // 這個key本身保存在表中
        if (hash_table.find(key) != hash_table.end())
        {
            listNode *tmp = hash_table[key];
            // 從鏈表頭部去掉這個點
            delNode(tmp);
            // 更新表中key對應鏈表節點的value
            tmp->value = value;
            // 從鏈表尾部插入這個點
            pushNodeBack(tmp);
            return;
        }
        // 鏈表的空間已滿
        if (cap == size)
        {
            // 空間不夠,踢出隊列最前端的ID
            listNode *tmp = head->next;
            // 在表中刪除這個點
            hash_table.erase(tmp->key);
            // 從鏈表頭部去掉這個點
            delNode(tmp);
            
            // 釋放被刪除的點的空間
            delete tmp;
        }
        else
            size++;
        listNode *node = new listNode(key, value);
        hash_table[key] = node;
        pushNodeBack(node);
    }

    void delNode(listNode *node)
    {
        node->pre->next = node->next;
        node->next->pre = node->pre;
    }

    void pushNodeBack(listNode *node)
    {
        tail->pre->next = node;
        node->pre = tail->pre;
        node->next = tail;
        tail->pre = node;
    }
};


int main()
{
    LRUCache *cache = new LRUCache(2);
    cache->put(1, 1);
    cache->put(2, 2);
    cout << cache->get(1) << endl;       // 返回  1
    cache->put(3, 3);                    // 該操作會使得密鑰 2 作廢
    cout << cache->get(2) << endl;       // 返回 -1 (未找到)
    cache->put(4, 4);                    // 該操作會使得密鑰 1 作廢
    cout << cache->get(1) << endl;       // 返回 -1
    cout << cache->get(3) << endl;       // 返回  3
    cout << cache->get(4) << endl;       // 返回  4
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章