讀完本文,你不僅學會了演算法思路,還可以順便去 leetcode 上拿下如下題目:460.lfu快取機制
lru 演算法的淘汰策略是 least recently used,也就是每次淘汰那些最久沒被使用的資料;而 lfu 演算法的淘汰策略是 least frequently used,也就是每次淘汰那些使用次數最少的資料。
本方法需要使用到「平衡二叉樹」。在 c++ 語言中,我們可以直接使用 std::set 類作為平衡二叉樹;
思路和演算法
首先我們定義快取的資料結構:
struct node
};
其中 cnt 表示快取使用的頻率,time 表示快取的使用時間,key 和 value 表示快取的鍵值。
比較直觀的想法就是我們用雜湊表 key_table 以鍵 key 為索引儲存快取,建立乙個平衡二叉樹 s 來保持快取根據 (cnt,time) 雙關鍵字由於。在 c++ 中,我們可以使用 stl 提供的 std::set 類,set 背後的實現是紅黑樹:
struct node
bool operator <
(const node& rhs)
const};
class lfucache
intget
(int key)
void
put(
int key,
int value)
// 建立新的快取
node cache =
node(1
,++time, key, value)
;// 將新快取放入雜湊表和平衡二叉樹中
key_table.
insert
(make_pair
(key, cache));
s.insert
(cache);}
else}}
;
複雜度分析
時間複雜度:get 時間複雜度o(logn),put 時間複雜度o(logn),操作的時間複雜度瓶頸在於平衡二叉樹的插入刪除均需要o(logn) 的時間。
空間複雜度:o(capacity),其中capacity 為 lfu 的快取容量。雜湊表和平衡二叉樹不會存放超過快取容量的鍵值對。
思路和演算法
我們定義兩個雜湊表,第乙個 freq_table 以頻率 freq 為索引,每個索引存放乙個雙向鍊錶,這個煉表裡存放所有使用頻率為 freq 的快取,快取裡存放三個資訊,分別為鍵 key,值 value,以及使用頻率 freq。第二個 key_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在 key_table 中檢視是否有對應的快取,如果有的話,其實操作等價於 get(key) 操作,唯一的區別就是我們需要將當前的快取裡的值更新為 value。如果沒有的話,相當於是新加入的快取,如果快取已經到達容量,需要先刪除最近最少使用的快取,再進行插入。
先考慮插入,由於是新插入的,所以快取的使用頻率一定是 1,所以我們將快取的資訊插入到 freq_table 中 1 索引下的列表頭即可,同時更新 key_table[key] 的資訊,以及更新 minfreq = 1。
那麼剩下的就是刪除操作了,由於我們實時維護了 minfreq,所以我們能夠知道 freq_table 裡目前最少使用頻率的索引,同時因為我們保證了鍊錶中從煉表頭到鍊錶尾的插入時間是有序的,所以 freq_table[minfreq] 的鍊錶中鍊錶尾的節點即為使用頻率最小且插入時間最早的節點,我們刪除它同時根據情況更新 minfreq ,整個時間複雜度均為 o(1)o(1)。
**實現:
// 快取的節點資訊
struct node };
class lfucache
intget
(int key)
// 插入到 freq + 1 中
freq_table[freq +1]
.push_front
(node
(key, val, freq +1)
);key_table[key]
= freq_table[freq +1]
.begin()
;return val;
}void
put(
int key,
int value)
} freq_table[1]
.push_front
(node
(key, value,1)
);key_table[key]
= freq_table[1]
.begin()
; minfreq =1;
}else
freq_table[freq +1]
.push_front
(node
(key, value, freq +1)
);key_table[key]
= freq_table[freq +1]
.begin()
;}}}
;
複雜度分析
時間複雜度:get 時間複雜度 o(1),put 時間複雜度 o(1)。由於兩個操作從頭至尾都只利用了雜湊表的插入刪除還有鍊錶的插入刪除,且它們的時間複雜度均為 o(1),所以保證了兩個操作的時間複雜度均為 o(1)。
空間複雜度:o(capacity),其中 capacity 為 lfu 的快取容量。雜湊表中不會存放超過快取容量的鍵值對。
C 多型及其實現原理
1.多型的定義 多型含義為乙個事物有多種形態。在c 程式設計中,多型性是指具有不同功能的函式可以用同乙個函式名,這樣就可以用乙個函式名呼叫不同內容的函式,主要分為靜態多型和動態多型 動態多型 就是通過繼承重寫基類的虛函式實現的多型,因為是在執行時確定決議,所以稱為動態多型。執行時在虛函式表中尋找呼叫...
c 中多型及其實現原理
有繼承 有虛函式 virtual 重寫 有父類指標 引用 指向子類物件。在基類中使用virtual定義虛函式,告訴編譯器這個函式要支援多型 而不是根據指標型別判斷如何呼叫 而是要根據引用或指標所繫結的物件的真實型別。一旦某個函式被宣告為虛函式,則在所有派生類中它都是虛函式 不管有沒有virtual ...
堆排序原理及其實現 C
我們知道簡單選擇排序的時間複雜度為o n 2 熟悉各種排序演算法的朋友都知道,這個時間複雜度是很大的,所以怎樣減小簡單選擇排序的時間複雜度呢?簡單選擇排序主要操作是進行關鍵字的比較,所以怎樣減少比較次數就是改進的關鍵。簡單選擇排序中第i趟需要進行n i次比較,如果我們用到前面已排好的序列a 1.i ...