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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

LFU缓存详解

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

LFU緩存詳解

文章目錄

  • LFU緩存詳解
    • 一、LFU概念
    • 二、分析
      • 方法一:哈希表 + 平衡二叉樹
      • 方法二:雙哈希表

一、LFU概念

LRU詳解:https://blog.csdn.net/wolfGuiDao/article/details/105862106

  • LRU 算法相當于把數據按照時間排序,這個需求借助鏈表很自然就能實現,你一直從鏈表頭部加入元素的話,越靠近頭部的元素就是新的數據,越靠近尾部的元素就是舊的數據,我們進行緩存淘汰的時候只要簡單地將尾部的元素淘汰掉就行了。

  • 而 LFU 算法相當于是淘汰訪問頻次最低的數據,如果訪問頻次最低的數據有多條,需要淘汰最舊的數據。把數據按照訪問頻次進行排序,而且頻次還會不斷變化,這可不容易實現。

  • 從題面上感覺并不難,不就是排個序嘛,如果這么想就錯了:因為要求O(1) 時間復雜度內執行兩項操作


二、分析

方法一:哈希表 + 平衡二叉樹

  • 因為我們剛剛分析過,LFU算法關系到數據的訪問頻次和數據的新舊(即數據的訪問時間),所以我們封裝一個數據結構:
struct Node {//數據的訪問頻次int cnt;//數據的訪問時間int time;//數據int key, value;// 我們需要實現一個 Node 類的比較函數// 將 cnt(使用頻率)作為第一關鍵字,time(最近一次使用的時間)作為第二關鍵字// 因為我們要在數據頻率相等時淘汰數據舊的,也就是時間小的bool operator< (const Node& rhs) const {return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;} };
  • 比較直觀的想法就是我們用哈希表 key_table 以鍵 key 為索引存儲緩存,根據 (cnt,time) 雙關鍵字建立一個平衡二叉樹 S 來保持緩存
  • 由于在 C++ 中,我們可以使用 STL 提供的 std::set 類,set 背后的實現是紅黑樹:
  • 對于 get(key) 操作,我們只要查看一下哈希表 key_table 是否有 key 這個鍵即可,有的話需要同時更新哈希表和集合中該緩存的使用頻率以及使用時間,否則返回 -1。

  • 對于 put(key, value) 操作,首先需要查看 key_table 中是否已有對應的鍵值。如果有的話操作基本等同于 get(key),不同的是需要更新緩存的 value 值。如果沒有的話相當于是新插入一個緩存,這時候需要先查看是否達到緩存容量 capacity,如果達到了的話,需要刪除最近最少使用的緩存,即平衡二叉樹中最左邊的結點,同時刪除 key_table 中對應的索引,最后向 key_table 和 S 插入新的緩存信息即可。
  • 完整代碼
struct Node {//頻率int cnt;//時間int time;//數據int key, value;//構造Node(int _cnt, int _time, int _key, int _value):cnt(_cnt), time(_time), key(_key), value(_value){}//自定義比較規則:以頻率為第一關鍵字,時間為第二關鍵字bool operator < (const Node& rhs) const {return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;} };class LFUCache {// 緩存容量int capacity;//時間戳int time;//關鍵字和數據信息的哈希索引unordered_map<int, Node> key_table;//平衡搜索樹set<Node> S;public:LFUCache(int _capacity) {capacity = _capacity;time = 0;key_table.clear();S.clear();}int get(int key) {if (capacity == 0) return -1;auto it = key_table.find(key);// 如果哈希表中沒有鍵 key,返回 -1if (it == key_table.end()) return -1;// 從哈希表中得到舊的緩存Node cache = it -> second;// 從平衡二叉樹中刪除舊的緩存S.erase(cache);// 將舊緩存更新cache.cnt += 1;cache.time = ++time;// 將新緩存重新放入哈希表和平衡二叉樹中S.insert(cache);//更新哈希表當中key對應的緩存數據it -> second = cache;//返回return cache.value;}void put(int key, int value) {if (capacity == 0) return;auto it = key_table.find(key);if (it == key_table.end()) {// 如果到達緩存容量上限if (key_table.size() == capacity) {// 從哈希表和平衡二叉樹中刪除最近最少使用的緩存key_table.erase(S.begin() -> key);S.erase(S.begin());}// 創建新的緩存Node cache = Node(1, ++time, key, value);// 將新緩存放入哈希表和平衡二叉樹中key_table.insert(make_pair(key, cache));S.insert(cache);}else {// 這里和 get() 函數類似//代表找到了key,取對應的緩存數據Node cache = it -> second;//在紅黑樹S中刪除舊的緩存數據S.erase(cache);//在舊的緩存基礎上更新緩存數據cache.cnt += 1;cache.time = ++time;cache.value = value;//重新把緩存數據插到紅黑樹S當中S.insert(cache);//跟新哈希表對應key的緩存數據it -> second = cache;}} };

方法二:雙哈希表

  • 我們定義兩個哈希表

  • 第一個 freq_table 以頻率 freq 為索引,每個索引存放一個雙向鏈表,這個鏈表里存放所有使用頻率為 freq 的緩存,緩存里存放三個信息,分別為鍵 key,值 value,以及使用頻率 freq。

  • 第二個 key_table 以鍵值 key 為索引,每個索引存放對應緩存在 freq_table 中鏈表里的內存地址,這樣我們就能利用兩個哈希表來使得兩個操作的時間復雜度均為 O(1)。

  • 同時需要記錄一個當前緩存最少使用的頻率 minFreq,這是為了刪除操作服務的。

  • 對于 get(key) 操作,我們能通過索引 key 在 key_table 中找到緩存在 freq_table 中的鏈表的內存地址,如果不存在直接返回 -1,否則我們能獲取到對應緩存的相關信息,這樣我們就能知道緩存的鍵值還有使用頻率,直接返回 key 對應的值即可。

  • 但是我們注意到 get 操作后這個緩存的使用頻率加一了,所以我們需要更新緩存在哈希表 freq_table 中的位置。

  • 已知這個緩存的鍵 key,值 value,以及使用頻率 freq,那么該緩存應該存放到 freq_table 中 freq + 1 索引下的鏈表中。

  • 所以我們在當前鏈表中 刪除該緩存對應的節點,根據情況更新 minFreq 值,然后將其插入到 freq + 1 索引下的鏈表頭完成更新。這其中的操作復雜度均為 O(1)。

  • 你可能會疑惑更新的時候為什么是插入到鏈表頭,這其實是為了保證緩存在當前鏈表中從鏈表頭到鏈表尾的插入時間是有序的,為下面的刪除操作服務。

  • 對于 put(key, value) 操作,我們先通過索引 key在 key_table 中查看是否有對應的緩存,如果有的話,其實操作等價于 get(key) 操作,唯一的區別就是我們需要將當前的緩存里的值更新為 value。如果沒有的話,相當于是新加入的緩存,如果緩存已經到達容量,需要先刪除最近最少使用的緩存,再進行插入。

  • 先考慮插入,由于是新插入的,所以緩存的使用頻率一定是 1,所以我們將緩存的信息插入到 freq_table 中 1 索引下的列表頭即可,同時更新 key_table[key] 的信息,以及更新 minFreq = 1。

  • 那么剩下的就是刪除操作了,由于我們實時維護了 minFreq,所以我們能夠知道 freq_table 里目前最少使用頻率的索引,同時因為我們保證了鏈表中從鏈表頭到鏈表尾的插入時間是有序的,所以 freq_table[minFreq] 的鏈表中鏈表尾的節點即為使用頻率最小且插入時間最早的節點,我們刪除它同時根據情況更新 minFreq ,整個時間復雜度均為O(1)。

  • 完整代碼

// 緩存的節點信息 struct Node {//數據int key, val;//頻率int freq;Node(int _key,int _val,int _freq): key(_key), val(_val), freq(_freq){}}; class LFUCache {//緩存的最小頻率int minfreq;//容量int capacity;//哈希表;key為關鍵字key,value為key在freq_table中的位置unordered_map<int, list<Node>::iterator> key_table;//哈希表;key為緩存數據的頻率,value為緩存數據的鏈unordered_map<int, list<Node>> freq_table;public:LFUCache(int _capacity) {minfreq = 0;capacity = _capacity;key_table.clear();freq_table.clear();}int get(int key) {if (capacity == 0) return -1;auto it = key_table.find(key);//沒找到就直接返回if (it == key_table.end()) return -1;//代表找到了,取key對應的緩存數據在freq_table中某條鏈上的位置list<Node>::iterator node = it -> second;//保存舊的緩存數據int val = node -> val, freq = node -> freq;//把舊的緩存數據從freq_table對應頻率的鏈上刪除freq_table[freq].erase(node);// 如果刪除后,當前鏈表為空,我們需要在哈希表中刪除,且更新minFreqif (freq_table[freq].size() == 0) {freq_table.erase(freq);if (minfreq == freq) minfreq += 1;}// 插入到 freq + 1 中頻率的鏈頭,因為該緩存數據get了一次freq_table[freq + 1].push_front(Node(key, val, freq + 1));//同時更新該緩存數據在key_table中的信息key_table[key] = freq_table[freq + 1].begin();return val;}void put(int key, int value) {if (capacity == 0) return;auto it = key_table.find(key);//如果沒找到該緩存數據,就需要插入緩存,但是需要判斷滿否?if (it == key_table.end()) {// 如果緩存已滿,需要進行刪除操作if (key_table.size() == capacity) {// 通過最小緩存頻率 minFreq 拿到其在 freq_table[minFreq] 鏈表的末尾節點auto it2 = freq_table[minfreq].back();//把使用最少最舊的緩存數據在key_table和freq_table中淘汰掉key_table.erase(it2.key);freq_table[minfreq].pop_back();//如果刪除后對應頻率的鏈表為空,也需要在freq_table中刪除if (freq_table[minfreq].size() == 0) {freq_table.erase(minfreq);}}//執行插入緩存操作:在freq_table中頻率為1的鏈頭插入新的緩存數據 freq_table[1].push_front(Node(key, value, 1));//在key_table中保存該緩存數據在鏈的迭代器key_table[key] = freq_table[1].begin();//更新最小的頻率minfreq = 1;} else {//代表找到了緩存數據// 與 get 操作基本一致,除了需要更新緩存的值//取該緩存數據在freq_table中某條鏈的迭代器list<Node>::iterator node = it -> second;//從迭代器中取到該緩存的頻率int freq = node -> freq;//從freq_table中對應鏈刪除該緩存freq_table[freq].erase(node);//同樣需要判斷刪除后該鏈是否為空if (freq_table[freq].size() == 0) {freq_table.erase(freq);if (minfreq == freq) minfreq += 1;}//把該緩存數據放大freq_table的頻率 + 1 的鏈頭freq_table[freq + 1].push_front(Node(key, value, freq + 1));//同時更新該緩存數據在key_table中的位置key_table[key] = freq_table[freq + 1].begin();}} };










總結

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

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