今天去面试,面试官让我设计一个cache系统,要求保证最近使用的数据不能被移除出cache,也就是每次添加一个cache项的时候,把最旧的cache项移除出去。
我只记得操作系统里貌似有个差不多的cache算法,记不起名字来,更别提数据结构了。一开始我执着于用一种数据结构来实现,可是每说出一种,都被面试官指出这种方式的不足。最后终于开窍了,想出了 哈希表+双向链表 的方法。当时也不知道到底对不对,感觉这种实现还过的去吧,查询和增加cache项复杂度都是O(1)。
回来后一查,原来就是LRU(Least Recent Used)算法。好到事实上就是采用了哈希表+双向链表的组合拳法。文末附上一篇别人写的博文。
另外,我发现Java1.4中有一个LinkedHashMap,就是哈希表+双链表,有一个removeEldestEntry方法,里面没有实现,override这个方法后很容易实现FIFO的Cache,对其它几个方法也override后想必实现LRU也不难。
http://blog.sina.com.cn/s/blog_567842410100nf1g.html Cache(高速缓存), 一个在计算机中几乎随时接触的概念。CPU中Cache能极大提高存取数据和指令的时间,让整个存储器(Cache+内存)既有Cache的高速度,又能有内存的大容量;操作系统中的内存page中使用的Cache能使得频繁读取的内存磁盘文件较少的被置换出内存,从而提高访问速度;数据库中数据查询也用到Cache来提高效率;即便是Powerbuilder的DataWindow数据处理也用到了Cache的类似设计。Cache的算法设计常见的有FIFO(first in first out)和LRU(least recently used),FIFO对CPU的指令序列非常有效,但更多的,对于Memory或者磁盘文件的这种Cache,LRU就更有效多了。FIFO的算法设计很简单明了,就不做讨论,在此只是对LRU展开。Cache中的存储空间被分成若干块,每一块对应内存或者磁盘文件的一段数据(当然也可以是指令数据),形成这种映射关系,当Cache中的存储块被用完,而需要把新的数据Load进Cache的时候,我们就需要设计一种良好的算法来完成数据块的替换。LRU的思想是基于“最近用到的数据被重用的概率比较早用到的大的多”这个设计规则来实现的。Cache中的所有块位置都用双向链表链接起来,当一个位置被命中后,就将通过调整链表的指向将该位置调整到链表的头位置,新加入的内容直接放在链表的头上。这样,在进行过多次查找操作后,最近被命中过的内容就向链表的头移动,而没有被命中的内容就向链表的后面移动。当需要替换时,链表最后的位置就是最近最少被命中的位置,我们只需要将新的内容放在链表前面,淘汰链表最后的位置就是想了。 对于双向链表的使用,基于两个考虑。首先是Cache中块的命中可能是随机的,和Load进来的顺序无关,所以我们需要用链表这种结构来保存位置队列,使得其可以灵活的调整相互间的次序。其次,双向链表使得在知道一个位置的情况下可以很迅速的移到其他的地方,时间复杂度为O(1)。 查找一个链表中元素的时间复杂度是O(n),每次命中的时候,我们就需要花费O(n)的时间来进行查找,如果不添加其他的数据结构,这个就是我们能实现的最高效率了。目前看来,整个算法的瓶颈就是在查找这里了,怎么样才能提高查找的效率呢?Hash表,对,就是它,数据结构中之所以有它,就是因为它的查找时间复杂度是O(1)。梳理一下思路:对于Cache的每个位置,我们设计一个数据结构来储存Cache块的内容,并实现一个双向链表,其中属性next和prev时双向链表的两个指针,key用于存储对象的键值,value用户存储要cache块对象本身,然后用Hash表来查找具体被命中的Cache块。剩下的就是写Code的事了:我们使用一个hashmap作为cache,用hashmap的检索机制来实现cache查找;并用head和last两个属性来记录链表的头和尾。并提供putEntry(),getEntry()方法来操作该cache。 |