題目描述
請你爲 最不經常使用(LFU)緩存算法設計並實現數據結構。它應該支持以下操作:get 和 put。
get(key) - 如果鍵存在於緩存中,則獲取鍵的值(總是正數),否則返回 -1。
put(key, value) - 如果鍵不存在,請設置或插入值。當緩存達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除 最近 最少使用的鍵。
「項的使用次數」就是自插入該項以來對其調用 get 和 put 函數的次數之和。使用次數會在對應項被移除後置爲 0 。
進階:
你是否可以在 O(1) 時間複雜度內執行兩項操作?
示例:
LFUCache cache = new LFUCache( 2 /* capacity (緩存容量) */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 去除 key 2
cache.get(2); // 返回 -1 (未找到key 2)
cache.get(3); // 返回 3
cache.put(4, 4); // 去除 key 1
cache.get(1); // 返回 -1 (未找到 key 1)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
解析
當緩存達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除 最近 最少使用的鍵。
首先看題目描述中的這句話,就可以知道我們需要設計數據結構時要有節點出現的頻率,還有最近一次使用該節點的時間。這樣才能確保當容器滿的時候,我們能夠正確的刪除節點。
方法一 :哈希表+平衡二叉樹
我們自定義節點
struct Value
{
Value(int count_, int time_, int key_, int value_)
: count(count_),
key(key_),
value(value_),
time(time_) {}
//重載小於運算符,爲了set排序時使用
bool operator < (const Value& a) const
{
return count == a.count ? time < a.time : count < a.count;
}
int key;
int value;
int count; //頻率
int time; //時間
};
在c++中,我們可以使用std::set來對節點進行排序。std::set底層是紅黑樹實現的(平衡二叉樹的一種)。
哈希表我們就用unordered_map(其底層就是可擴展的拉鍊法實現的)。
- 對於get操作,我們需要檢查哈希表中是否有該元素,如果沒有就返回-1。有的話就需要更新Value中的頻率和time,然後重新加入到set中。
- 對於put操作,我們也需要檢查哈希表中是否有該元素。
- 沒有的話,我們首先需要判斷哈希表的容量是否等於LFU歸定的容量,等於的話我們就需要將不活躍的從表中刪除再進行插入,否則直接插入即可。
- 有該元素的話,其實操作和get很類似,只不過需要更新key對應的value的值。
代碼實現。
struct Value
{
Value(int count_, int time_, int key_, int value_)
: count(count_),
key(key_),
value(value_),
time(time_) {}
bool operator < (const Value& a) const
{
return count == a.count ? time < a.time : count < a.count;
}
int key;
int value;
int count;
int time;
};
class LFUCache
{
public:
LFUCache(int capacity_)
: capacity(capacity_),
time(0) {}
int get(int key)
{
if(capacity == 0)
return -1;
auto it = table.find(key);
if(it == table.end())
return -1;
Value cache = it->second;
judge.erase(cache);
cache.count++;
cache.time = ++time;
judge.insert(cache);
it->second = cache;
return cache.value;
}
void put(int key, int value)
{
if(capacity == 0)
return;
auto it = table.find(key);
if(it == table.end())
{
if(table.size() == capacity)
{
//先將不活躍的元素刪除
table.erase(judge.begin()->key);
judge.erase(judge.begin());
}
Value put_(0, ++time, key, value);
table.insert({key, put_});
judge.insert(put_);
}
else
{
Value temp = it->second;
judge.erase(temp);
Value put_(++temp.count, ++time, key, value);
it->second = put_;
judge.insert(put_);
}
}
private:
const int capacity;
int time;
unordered_map<int, Value> table;
set<Value> judge;
};
- 時間複雜度分析:因爲set的底層是紅黑樹,所以插入刪除操作都是O(logN),上述LFU設計的數據結構在get和put時間複雜度也是O(logN)。
方法二 : 雙哈希表+雙向鏈表
思路和算法
-
我們定義兩個哈希表,第一個 freq_table 以頻率 freq 爲索引,每個索引存放一個雙向鏈表,這個鏈表裏存放所有使用頻率爲 freq 的緩存,緩存裏存放三個信息,分別爲鍵 key,值 value,以及使用頻率 freq。第二個 iter_table 以鍵值 key 爲索引,每個索引存放對應緩存在 freq_table 中鏈表裏的內存地址,這樣我們就能利用兩個哈希表來使得兩個操作的時間複雜度均爲 O(1)O(1)。同時需要記錄一個當前緩存最少使用的頻率 minFreq,這是爲了刪除操作服務的。
-
對於 get(key) 操作,我們能通過索引 key 在 key_table 中找到緩存在 freq_table 中的鏈表的內存地址,如果不存在直接返回 -1,否則我們能獲取到對應緩存的相關信息,這樣我們就能知道緩存的鍵值還有使用頻率,直接返回 key 對應的值即可。
-
但是我們注意到 get 操作後這個緩存的使用頻率加一了,所以我們需要更新緩存在哈希表 freq_table 中的位置。已知這個緩存的鍵 key,值 value,以及使用頻率 freq,那麼該緩存應該存放到 freq_table 中 freq + 1 索引下的鏈表中。所以我們在當前鏈表中 O(1)O(1) 刪除該緩存對應的節點,根據情況更新 minFreq 值,然後將其O(1)O(1) 插入到 freq + 1 索引下的鏈表頭完成更新。這其中的操作複雜度均爲 O(1)O(1)。你可能會疑惑更新的時候爲什麼是插入到鏈表頭,這其實是爲了保證緩存在當前鏈表中從鏈表頭到鏈表尾的插入時間是有序的,爲下面的刪除操作服務。
-
對於 put(key, value) 操作,我們先通過索引 key在 iter_table 中查看是否有對應的緩存,如果有的話,其實操作等價於 get(key) 操作,唯一的區別就是我們需要將當前的緩存裏的值更新爲 value。如果沒有的話,相當於是新加入的緩存,如果緩存已經到達容量,需要先刪除最近最少使用的緩存,再進行插入。
-
先考慮插入,由於是新插入的,所以緩存的使用頻率一定是 1,所以我們將緩存的信息插入到 freq_table 中 1 索引下的列表頭即可,同時更新 iter_table[key] 的信息,以及更新 minFreq = 1。
-
那麼剩下的就是刪除操作了,由於我們實時維護了 minFreq,所以我們能夠知道 freq_table 裏目前最少使用頻率的索引,同時因爲我們保證了鏈表中從鏈表頭到鏈表尾的插入時間是有序的,所以 freq_table[minFreq] 的鏈表中鏈表尾的節點即爲使用頻率最小且插入時間最早的節點,我們刪除它同時根據情況更新 minFreq ,整個時間複雜度均爲 O(1)O(1)。
class LFUCache
{
public:
LFUCache(int capacity_)
: capacity(capacity_),
minfreq(0)
{
iter_table.clear();
freq_table.clear();
}
int get(int key)
{
if(capacity == 0)
return -1;
auto it = iter_table.find(key);
if(it == iter_table.end())
return -1;
list<Value>::iterator iter = it->second;
//在此處一定要先存儲iter的值,因爲在下面刪除list中的結點iter時會導致iter迭代器失效
int value = iter->value;
int freq = iter->freq;
Value new_node(key, value, freq +1);
//刪除iter->freq中key對應的元素,此處操作會導致迭代器iter失效
freq_table[freq].erase(iter);
if(freq_table[freq].size() == 0)
{
freq_table.erase(freq);
if(minfreq == freq)
minfreq += 1;
}
freq_table[freq+1].push_front(new_node);
iter_table[key] = freq_table[freq+1].begin();
return new_node.value;
}
void put(int key, int value)
{
if(capacity == 0)
return;
auto it = iter_table.find(key);
if(it == iter_table.end())
{
//元素已滿
if(iter_table.size() == capacity)
{
auto it2 = freq_table[minfreq].back();
iter_table.erase(it2.key);
freq_table[minfreq].pop_back();
if(freq_table[minfreq].size() == 0)
{
freq_table.erase(minfreq);
}
}
freq_table[1].push_front(Value{key, value, 1});
iter_table[key] = freq_table[1].begin();
minfreq = 1;
}
else
{
list<Value>::iterator iter = it->second;
//創建新元素
int freq = iter->freq;
Value new_node(iter->key, value, freq +1);
//將freq_table中iter->freq所對應的元素刪除
freq_table[iter->freq].erase(iter);
//list中元素爲空,則將該list刪除
if(freq_table[freq].size() == 0)
{
freq_table.erase(freq);
if(minfreq == freq)
minfreq += 1;
}
freq_table[freq+1].push_front(new_node);
iter_table[key] = freq_table[freq+1].begin();
}
}
private:
struct Value
{
Value(int key_, int value_, int freq_)
: key(key_),
value(value_),
freq(freq_) {}
int key;
int value;
int freq;
};
int capacity;
int minfreq;
unordered_map<int, list<Value>::iterator> iter_table;
unordered_map<int, list<Value> > freq_table;
};