python中的LFU缓存实现

时间:2014-08-17 14:02:06

标签: python caching heap priority-queue

我已经在python中实现了LFU缓存,并在下面给出了优先级队列实现 https://docs.python.org/2/library/heapq.html#priority-queue-implementation-notes

我在帖子的最后给出了代码。

但我觉得代码有一些严重的问题:
1.给出一个场景,假设只有一个页面不断被访问(比如说50次)。但是此代码将始终将已添加的节点标记为“已删除”并再次将其添加到堆中。所以基本上它将为同一页面提供50个不同的节点。因此极大地增加了堆大小 这个问题几乎与电话采访的Q1相似 http://www.geeksforgeeks.org/flipkart-interview-set-2-sde-2/ 并且有人提到,与堆相比,双向链表可以提供更好的效率。谁能解释我,怎么样?

from llist import dllist
import sys
from heapq import heappush, heappop

class LFUCache:
    heap = []
    cache_map = {}
    REMOVED = "<removed-task>"

    def __init__(self, cache_size):
        self.cache_size = cache_size

    def get_page_content(self, page_no):
        if self.cache_map.has_key(page_no):
            self.update_frequency_of_page_in_cache(page_no)  
        else:
            self.add_page_in_cache(page_no)

        return self.cache_map[page_no][2]       

    def add_page_in_cache(self, page_no):
        if (len(self.cache_map) == self.cache_size):
            self.delete_page_from_cache() 

        heap_node = [1, page_no, "content of page " + str(page_no)]
        heappush(self.heap, heap_node)
        self.cache_map[page_no] = heap_node

    def delete_page_from_cache(self):
        while self.heap:
            count, page_no, page_content = heappop(self.heap)
            if page_content is not self.REMOVED:
                del self.cache_map[page_no]
                return

    def update_frequency_of_page_in_cache(self, page_no): 
        heap_node = self.cache_map[page_no]
        heap_node[2] = self.REMOVED
        count = heap_node[0]

        heap_node = [count+1, page_no, "content of page " + str(page_no)]
        heappush(self.heap, heap_node)
        self.cache_map[page_no] = heap_node

def main():
    cache_size = int(raw_input("Enter cache size "))
    cache = LFUCache(cache_size)

    while 1:
        page_no = int(raw_input("Enter page no needed "))
        print cache.get_page_content(page_no)
        print cache.heap, cache.cache_map, "\n"


if __name__ == "__main__":
    main() 

1 个答案:

答案 0 :(得分:4)

效率是一件棘手的事情。在实际应用中,使用最简单,最简单的算法通常是一个好主意,只有在速度非常慢时才开始优化。然后通过分析来优化以找出代码缓慢的位置。

如果您正在使用CPython,它会变得特别棘手,因为即使是在C中实现的低效算法也可以击败由于大常数因素而在Python中实现的有效算法;例如Python中实现的双链表往往比简单使用普通的Python列表要慢得多,即使理论上它应该更快。

简单算法:

对于LFU,最简单的算法是使用将键映射到(项目,频率)对象的字典,并更新每次访问的频率。这使访问速度非常快(O(1)),但修剪缓存的速度较慢,因为您需要按频率排序以切断最少使用的元素。对于某些使用特性,这实际上比其他“更智能”的解决方案更快。

您可以通过不仅简单地将LFU缓存修剪到最大长度来优化此模式,而是在它变得太大时将其修剪为最大长度的50%。这意味着您的修剪操作不经常调用,因此与读取操作相比可能效率低下。

使用堆:

在(1)中,您使用了堆,因为这是存储优先级队列的有效方式。但是您没有实现优先级队列。生成的算法针对修剪进行了优化,但不是访问:您可以轻松找到n个最小的元素,但如何更新现有元素的优先级并不是那么明显。理论上,您必须在每次访问后重新平衡堆,这非常低效。

为了避免这种情况,你通过保留元素来添加一个技巧,即使它们被删除了。但这会在太空中进行交易。

如果您不想及时交易,可以就地更新频率,并在修剪缓存之前简单地重新平衡堆。您可以以较慢的修剪时间为代价重新获得快速访问时间,就像上面的简单算法一样。 (我怀疑这两者之间有什么速度差异,但我没有测量过这个。)

使用双链表:

(2)中提到的双链表利用了这里可能的变化的性质:一个元素被添加为最低优先级(0个访问),或者一个现有元素的优先级被精确地增加为1。如果您设计这样的数据结构,可以使用这些属性:

您有一个双链接的元素列表,按元素的频率排序。此外,您还有一个字典,可以将项目映射到该列表中的元素。

访问元素则意味着:

  • 要么它不在字典中,也就是说,它是一个新项目,在这种情况下你可以简单地将它附加到双链表的末尾(O(1))
  • 或它在字典中,在这种情况下,你增加元素中的频率并向左移动通过双链表,直到列表再次排序(O(n)最坏情况,但通常更接近O( 1))。

要修剪缓存,只需从列表末尾(O(n))中删除n个元素。

相关问题