LFU(LeastFrequently Used),即最近最多使用算法。它是基於“如果一個數據在最近一段時間內使用次數很少,那麼在將來一段時間內被使用的可能性也很小”的思路。LFU算法需要維護一個隊列記錄所有數據的訪問記錄,每個數據都需要維護引用計數。LFU算法需要記錄所有數據的訪問記錄,內存消耗較高;需要基於引用計數排序,性能消耗較高。
LFU的每個數據塊都有一個引用計數,所有數據塊按照引用計數排序,具有相同引用計數的數據塊則按照時間排序。具體實現如圖4-1所示。
圖4-1所示的操作包括:
(1)新加入數據插入到隊列尾部(因爲引用計數爲1);
(2)隊列中的數據被訪問後,引用計數增加,隊列重新排序;
(3)當需要淘汰數據時,將已經排序的列表最後的數據塊刪除。
注意LFU和下一小節要介紹的LRU算法之間存在的不同之處,LRU的淘汰規則是基於訪問時間,而LFU是基於訪問次數的。舉個簡單的例子,假設緩存大小爲3,數據訪問序列爲set(2,2)、set(1,1)、get(2)、get(1)、get(2)、set(3,3)、set(4,4),則在set(4,4)時對於LFU算法應該淘汰(3,3),而LRU應該淘汰(1,1)。LRU關鍵是看頁面最後一次被使用到發生調度的時間長短,而LFU關鍵是看一定時間段內頁面被使用的頻率。
那麼基於LFU算法的Cache設計應該支持的操作如。
n get(key):如果Cache中存在該key,則返回對應的value值,否則,返回-1;
n set(key,value):如果Cache中存在該key,則重置value值;如果不存在該key,則將該key插入到到Cache中,若Cache已滿,則淘汰最少訪問的數據。
爲了能夠淘汰最少使用的數據,LFU算法最簡單的一種設計思路就是利用一個數組存儲數據項,用HashMap存儲每個數據項在數組中對應的位置,然後爲每個數據項設計一個訪問頻次,當數據項被命中時,訪問頻次自增,在淘汰的時候淘汰訪問頻次最少的數據。這樣一來的話,在插入數據和訪問數據的時候都能達到O(1)的時間複雜度,在淘汰數據的時候,通過選擇算法得到應該淘汰的數據項在數組中的索引,並將該索引位置的內容替換爲新來的數據內容即可,這樣的話,淘汰數據的操作時間複雜度爲O(n)。
另外還有一種實現思路就是利用最小堆和HashMap兩者的優勢,最小堆中根結點的鍵值是所有堆結點鍵值中的最小者。最小堆插入、刪除操作都能達到O(logn)時間複雜度,因此效率相比第一種實現方法更加高效。代碼清單4-3所示的代碼是最小堆實現的一個示例。
代碼清單4-3 最小堆實現示例
public classSmallHeapDemo {
final static int MAX_LEN = 100;
private int queue[] = newint[MAX_LEN];
private int size;
public void add(int e){
if(size >= MAX_LEN)
{
System.err.println("overflow");
return;
}
int s = size++;
shiftup(s,e);
}
public int size(){
return size;
}
private void shiftup(int s, int e){
while(s > 0){
int parent = (s - 1)/2;
if(queue[parent] < e){
break;
}
queue[s] = queue[parent];
s = parent;
}
queue[s] = e;
}
public int poll(){
if(size <= 0)
return -1;
int ret = queue[0];
int s = --size;
shiftdown(0, queue[s]);
queue[s] = 0;
return ret;
}
private void shiftdown(int i, int e){
int half = size /2;
while(i < half ){
int child = 2*i +1;
int right = child +1;
if(right < size &&queue[child] > queue[right]){
child = right;
}
if(e < queue[child]){
break;
}
queue[i] = queue[child];
i = child;
}
queue[i] = e;
}
public static void main(Stringargs[]){
SmallHeapDemo hs = newSmallHeapDemo();
hs.add(4);
hs.add(3);
hs.add(7);
hs.add(2);
int size = hs.size();
for(int i=0; i< size; i++){
System.out.println(hs.poll());
}
}
}
程序運行輸出爲“2347”。
一般情況下,LFU效率要優於LRU,且能夠避免週期性或者偶發性的操作導致緩存命中率下降的問題。但LFU需要記錄數據的歷史訪問記錄,一旦數據訪問模式改變,LFU需要更長時間來適用新的訪問模式,即LFU存在歷史數據影響將來數據的“緩存污染”效用。
歡迎關注麥克叔叔每晚10點說,感興趣的朋友可以關注公衆號,讓我們一起交流與學習。