日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

实现 LRU 缓存机制

發布時間:2024/4/11 编程问答 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 实现 LRU 缓存机制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

實現 LRU 緩存機制

文章目錄

    • 實現 LRU 緩存機制
    • 一、什么是 LRU 算法
    • 二、LRU 算法描述
    • 三、LRU 算法設計
    • 四、代碼實現

一、什么是 LRU 算法

LRU 就是一種緩存淘汰策略。(比較常見的內存替換算法有:FIFO(先進先出淘汰算法),LRU最近最少使用替換算法),LFU(最不經常訪問淘汰算法),LRU-K(最久未使用K次淘汰算法),2Q(類似LRU-2))

  • 計算機的緩存容量有限,如果緩存滿了就要刪除一些內容,給新內容騰位置。但問題是,刪除哪些內容呢?我們肯定希望刪掉那些沒什么用的緩存,而把有用的數據繼續留在緩存里,方便之后繼續使用。那么,什么樣的數據,我們判定為「有用的」的數據呢?
  • LRU 緩存淘汰算法就是一種常用策略。LRU 的全稱是 Least Recently Used,也就是說我們認為 最近使用過的數據應該是是「有用的」,很久都沒用過的數據應該是無用的,緩存滿了就優先刪那些很久沒用過的數據。

舉個簡單的例子,安卓手機都可以把軟件放到后臺運行,比如我先后打開了「設置」「手機管家」「日歷」,那么現在他們在后臺排列的順序是這樣的:


但是這時候如果我訪問了一下「設置」界面,那么「設置」就會被提前到第一個,變成這樣:


假設我的手機只允許我同時開 3 個應用程序,現在已經滿了。那么如果我新開了一個應用「時鐘」,就必須關閉一個應用為「時鐘」騰出一個位置,關那個呢?

按照 LRU 的策略,就關最底下的「手機管家」,因為那是最久未使用的,然后把新開的應用放到最上面:

二、LRU 算法描述



這是一道 LRU 算法設計的題目。讓你設計一種數據結構,首先構造函數接收一個 capacity 參數作為緩存的最大容量,然后實現兩個 API:

  • 一個是 put(key, val) 方法插入新的或更新已有鍵值對,如果緩存已滿的話,要刪除那個最久沒用過的鍵值對以騰出位置插入。
  • 另一個是 get(key) 方法獲取 key 對應的 val,如果 key 不存在則返回 -1。

注意哦,get 和 put 方法必須都是 O(1) 的時間復雜度,我們舉個具體例子來看看 LRU 算法怎么工作。

/* 緩存容量為 2 */ LRUCache cache = new LRUCache(2); // 你可以把 cache 理解成一個隊列 // 假設左邊是隊頭,右邊是隊尾 // 最近使用的排在隊頭,久未使用的排在隊尾 // 圓括號表示鍵值對 (key, val)cache.put(1, 1); // cache = [(1, 1)]cache.put(2, 2); // cache = [(2, 2), (1, 1)]cache.get(1); // 返回 1 // cache = [(1, 1), (2, 2)] // 解釋:因為最近訪問了鍵 1,所以提前至隊頭 // 返回鍵 1 對應的值 1cache.put(3, 3); // cache = [(3, 3), (1, 1)] // 解釋:緩存容量已滿,需要刪除內容空出位置 // 優先刪除久未使用的數據,也就是隊尾的數據 // 然后把新的數據插入隊頭cache.get(2); // 返回 -1 (未找到) // cache = [(3, 3), (1, 1)] // 解釋:cache 中不存在鍵為 2 的數據cache.put(1, 4); // cache = [(1, 4), (3, 3)] // 解釋:鍵 1 已存在,把原始值 1 覆蓋為 4 // 不要忘了也要將鍵值對提前到隊頭

三、LRU 算法設計

  • 分析上面的操作過程,要讓 put 和 get 方法的時間復雜度為 O(1),我們可以總結出 cache這個數據結構必要的條件:查找快,插入快,刪除快,有順序之分。
  • 因為顯然 cache 必須有順序之分,以區分最近使用的和久未使用的數據;而且我們要在 cache 中查找鍵是否已存在;如果容量滿了要刪除最后一個數據;每次訪問還要把數據插入到隊頭。
  • 那么,什么數據結構同時符合上述條件呢?哈希表查找快,但是數據無固定順序;鏈表有順序之分,插入刪除快,但是查找慢。所以結合一下,形成一種新的數據結構:哈希鏈表。

LRU 緩存算法的核心數據結構就是哈希鏈表,雙向鏈表和哈希表的結合體。這個數據結構長這樣:


思想很簡單,就是借助哈希表賦予了鏈表快速查找的特性嘛:可以快速查找某個 key 是否存在緩存(鏈表)中,同時可以快速刪除、添加節點。回想剛才的例子,這種數據結構是不是完美解決了 LRU 緩存的需求?

也許讀者會問,為什么要是雙向鏈表,單鏈表行不行?另外,既然哈希表中已經存了 key,為什么鏈表中還要存鍵值對呢,只存值不就行了?

想的時候都是問題,只有做的時候才有答案。這樣設計的原因,必須等我們親自實現 LRU 算法之后才能理解,所以我們開始看代碼吧~

四、代碼實現

很多編程語言都有內置的哈希鏈表或者類似 LRU 功能的庫函數,但是為了幫大家理解算法的細節,我們用 Java 自己造輪子實現一遍 LRU 算法。

首先,我們把雙鏈表的節點類寫出來,為了簡化,key 和 val 都認為是 int 類型:

class Node {public int key, val;public Node next, prev;public Node(int k, int v) {this.key = k;this.val = v;} }

然后依靠我們的 Node 類型構建一個雙鏈表,實現幾個要用到的 API,這些操作的時間復雜度均為 O(1) :

class DoubleList { // 在鏈表頭部添加節點 xpublic void addFirst(Node x);// 刪除鏈表中的 x 節點(x 一定存在)public void remove(Node x);// 刪除鏈表中最后一個節點,并返回該節點public Node removeLast();// 返回鏈表長度public int size(); }

到這里就能回答剛才“為什么必須要用雙向鏈表”的問題了,因為我們需要刪除操作。刪除一個鏈表節點不光要得到該節點本身的指針,也需要操作其前驅節點的指針,而雙向鏈表才能支持直接查找前驅,保證操作的時間復雜度 O(1)。

有了雙向鏈表的實現,我們只需要在 LRU 算法中把它和哈希表結合起來即可。我們先把邏輯理清楚:

// key 映射到 Node(key, val) HashMap<Integer, Node> map;// Node(k1, v1) <-> Node(k2, v2)... DoubleList cache;int get(int key) {if (key 不存在) {return -1;} else { 將數據 (key, val) 提到開頭;return val;} }void put(int key, int val) {Node x = new Node(key, val);if (key 已存在) {把舊的數據刪除;將新節點 x 插入到開頭;} else {if (cache 已滿) {刪除鏈表的最后一個數據騰位置;刪除 map 中映射到該數據的鍵;} 將新節點 x 插入到開頭;map 中新建 key 對新節點 x 的映射;} }

如果能夠看懂上述邏輯,翻譯成代碼就很容易理解了:

class LRUCache {// key -> Node(key, val)private HashMap<Integer, Node> map;// Node(k1, v1) <-> Node(k2, v2)...private DoubleList cache;// 最大容量private int cap;public LRUCache(int capacity) {this.cap = capacity;map = new HashMap<>();cache = new DoubleList();}public int get(int key) {if (!map.containsKey(key))return -1;int val = map.get(key).val;// 利用 put 方法把該數據提前put(key, val);return val;}public void put(int key, int val) {// 先把新節點 x 做出來Node x = new Node(key, val);if (map.containsKey(key)) {// 刪除舊的節點,新的插到頭部cache.remove(map.get(key));cache.addFirst(x);// 更新 map 中對應的數據map.put(key, x);} else {if (cap == cache.size()) {// 刪除鏈表最后一個數據Node last = cache.removeLast();map.remove(last.key);}// 直接添加到頭部cache.addFirst(x);map.put(key, x);}} }

這里就能回答之前的問答題“為什么要在鏈表中同時存儲 key 和 val,而不是只存儲 val”,注意這段代碼:

if (cap == cache.size()) {// 刪除鏈表最后一個數據Node last = cache.removeLast();map.remove(last.key); }
  • 當緩存容量已滿,我們不僅僅要刪除最后一個 Node 節點,還要把 map 中映射到該節點的 key 同時刪除,而這個 key 只能由 Node 得到。如果 Node 結構中只存儲 val,那么我們就無法得知 key 是什么,就無法刪除 map中的鍵,造成錯誤。
  • 至此,你應該已經掌握 LRU 算法的思想和實現了,很容易犯錯的一點是:處理鏈表節點的同時不要忘了更新哈希表中對節點的映射。
  • C++代碼
struct LRUNode {LRUNode(int key, int val) :key(key),val(val), pre(nullptr), next(nullptr) {}LRUNode(int key, int val, LRUNode* pre, LRUNode* next) :key(key), val(val), pre(pre), next(next) {}//類似與雙向鏈表的雙指針LRUNode* next;LRUNode* pre;//鍵值對int key, val; };class LRUCache { public:LRUCache(int capacity) {n = capacity;cnt = 0;head->pre = tail;}int get(int key) {if (mp.count(key) == 0) return -1;movefrommid(mp[key]);movetohead(mp[key]);return mp[key]->val;}void put(int key, int value) {if (mp.count(key)) {mp[key]->val = value;movefrommid(mp[key]);movetohead(mp[key]);}else {if (cnt == n) deletetail();else ++cnt;addhead(key, value);}}private:void movefrommid(LRUNode* mid) {LRUNode* l = mid->pre;LRUNode* r = mid->next;mid->pre = nullptr;mid->next = nullptr;l->next = r;r->pre = l;}void movetohead(LRUNode* mid) {LRUNode* l = head->pre;LRUNode* r = head;mid->pre = l;mid->next = r;l->next = mid;r->pre = mid;}void deletetail() {LRUNode* l = tail;LRUNode* mid = tail->next;LRUNode* r = tail->next->next;l->next = r;r->pre = l;mp.erase(mid->key);delete mid;}void addhead(int& key, int& value) {LRUNode* l = head->pre;LRUNode* r = head;LRUNode* mid = new LRUNode(key, value, l, r);l->next = mid;r->pre = mid;mp[key] = mid;}private:int n, cnt;LRUNode* head = new LRUNode(100, 100);LRUNode* tail = new LRUNode(-1, -1, nullptr, head);unordered_map<int, LRUNode*> mp; };/*** 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);*/
  • 用C++中STL的list實現
class LRUCache { public:LRUCache(int capacity):_capacity(capacity){ }int get(int key) {auto it = lru_ump.find(key);//如果沒有找到if(it == lru_ump.end()){return -1;}else {//根據key獲取他在list中對迭代器pair<int,int> temp = *lru_ump[key];//刪除已存在對keylru_list.erase(lru_ump[key]);//重新插入key到list的前面lru_list.push_front(temp);//更新key在ump中的位置lru_ump[key] = lru_list.begin();//返回valuereturn temp.second;}}void put(int key, int value) {auto it = lru_ump.find(key);//如果沒找到了if(it == lru_ump.end()){//如果list已經滿了,需要淘汰keyif(lru_list.size() == _capacity){//取list中最后一個auto temp = lru_list.back();//分別在ump和list中刪除lru_ump.erase(temp.first);lru_list.pop_back();}//插入新數據key在list的前面lru_list.push_front(pair<int ,int >(key,value));//ump中插入新數據key在list中的位置迭代器lru_ump[key] = lru_list.begin();}//找到了,就換到list的隊頭else {//刪除list的舊位置值lru_list.erase(lru_ump[key]);//list中插入在list的頭部lru_list.push_front(make_pair(key,value));//更新在ump中的迭代器值lru_ump[key] = lru_list.begin();}}private:int _capacity;//哈希表,注意第二個是list的迭代器,代表key在list中的位置unordered_map<int,list<pair<int ,int>>::iterator> lru_ump;//雙向鏈表list< pair<int ,int>>lru_list; };

總結

以上是生活随笔為你收集整理的实现 LRU 缓存机制的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。