LFU算法

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點說,感興趣的朋友可以關注公衆號,讓我們一起交流與學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章