LFU算法

Posted zmycoco2

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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点说,感兴趣的朋友可以关注公众号,让我们一起交流与学习。

以上是关于LFU算法的主要内容,如果未能解决你的问题,请参考以下文章

LFU算法

吃透Redis:缓存淘汰篇-LFU算法

吃透Redis:缓存淘汰篇-LFU算法

吃透Redis:缓存淘汰篇-LFU算法

字节二面,让手写一个LFU缓存策略算法,当场我不干了!

Cache替换算法:LRU与LFU的区别