借助散列表,实现一个高效的LRU缓存淘汰算法
首先,我们先回顾一下只使用链表是怎么实现一个LRU缓存淘汰算法的。
我们需要维护一个按照访问时间从小到大有序排列的链表结构,当我们需要缓存一个数据时,首先我们会在链表中查找是否已经存在该数据,如果存在,则将数据移到链表的末尾。如果没有找到,我们会先判断缓存大小是否已经满了, 如果还没有满,则将数据插入到链表的末尾。而当缓存满时,我们需要删掉一个长时间未使用的数据,也就是删掉链表头部的节点,然后再将要缓存的数据插入到链表尾部。因为查找数据需要遍历链表,所以单纯的使用链表来实现LRU缓存淘汰算法的时间复杂度是O(n),这个时间复杂度是非常高的。
总结一下,一个缓存系统主要包括以下几个操作:
- 往缓存中添加一个数据
- 删除缓存中的一个数据
- 查找缓存中的一个数据
接下来我们结合链表和散列表这两种数据结构来完成一个高效的LRU缓存淘汰算法,它可以将原先的时间复杂度将为O(1)。 先看下两者结合的结构是怎样的。
注:图片来自极客时间–《数据结构与算法之美》这篇专栏
如图,我们缓存的数据是储存在双向链表中的,这里我们的散列表是通过链表法来解决散列冲突的,所以每个节点会在两条链中,其中深黑色箭头维持 的链表是散列表中的拉链,浅色的箭头代表双向链表。注意前驱和后继指针是为了将结点串在双向链表中,hnext指针是为了将结点串在散列表的拉链中。
如何查找数据?
我们知道在散列表中查找数据的时间复杂度是O(1),所以通过散列表,我们可以很快地在缓存中找到一个数据。当找到数据之后我们就将它移到双向链表的末尾。
如何删除数据?
我们需要找到数据所在节点,再将它删除。借助散列表,我们可以在O(1)的时间复杂度下找到我们要删除的数据,然后通过前驱指针,我们可以在O(1)的时间复杂度下删除掉数据。
如何添加一个数据?
其实就是先按照刚才的查找操作执行一遍,如果找到了,就将数据移到双向链表的末尾。如果没有找到,先判断缓存是否满了,如果满了,就删除双向链表的头节点,再将数据插入到链表的末尾,如果没有满,就直接插入。这整个过程涉及的查找操作都可以通过散列表来完成。其他的操作,比如删除头结点、链表尾部插入数据等,都可以在O(1)的时间复杂度内完成。所以,这三个操作的时间复杂度都是 O(1)。
到这里,我们已经通过散列表和双向链表的组合实现一个高效的LRU缓存淘汰算法。