目录
1.什么是LRU算法
2.LRU算法原题描述
3.LRU算法设计
4.LRU算法细节分析
5.代码实现
1.什么是LRU算法
就是一种缓存淘汰策略。
计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢?
LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。
2.LRU算法原题描述
举一个例子:
假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢?
按照 LRU 的策略,就关最底下的,因为那是最久未使用的,然后把新开的应用放到最上面:
现在你应该理解 LRU(Least Recently Used)策略了。
我们先从一道LRU设计算法题开始
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。支持以下操作:
获取数据 get 和 写入数据 put 。获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
O(1) 时间复杂度内完成这两种操作
3.LRU算法设计
分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。
因为必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。
LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
在双向链表中特意增加两个节点,不用来存储任何数据。使用节点,增加/删除节点的时候就可以不用考虑边界节点不存在情况,简化编程难度,降低代码复杂度。
思想听起来很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。回想刚才的例子,这种数据结构是不是完美解决了 LRU 缓存的需求?
也许大家会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了?
这样设计的原因,必须等我们亲自实现 LRU 算法之后才能理解,所以我们开始一步步实现代码吧
4.LRU算法细节分析
新插入的元素或者最新查询的元素要放到链表的头部,对于长时间未访问的元素要放到链表尾部,所以每次插入或者查询都需要维护链表中元素的顺序。
使用哈希表的原因是查询时间复杂度为O(1),使用双向链表的原因是对于删除和插入操作时间复杂度为O(1)。
其中哈希表中存储的 key 为 K,value 为 Node<K,V> 的引用,双向链表存储的元素为Node<K,V>的引用.
对于put操作:
①首先判断缓存中 元素 K 是否存在,如果存在,则把链表中的元素Node<K, V>删除,map中的数据<K, Node<K, V> >不用删除,再在链表头部插入元素,并更新map,直接返回即可 ;
②缓存不存在,并且缓存没有满的话,直接把元素插入链表的表头,缓存满了的话移除表尾元素(最旧未访问元素),将元素K插入表头,增加map中的<K, Node<K, V>>, 更新map。
对于get操作:
首先要判断 缓存中(map)是否存在,如果存在则把该节点删除并在链表头部插入该元素并更新map 返回当前元素即可,如果map不存在 则直接返回-1;
5.代码实现
public class LRUCache {Entry head, tail;int capacity;int size;Map<Integer, Entry> cache;public LRUCache(int capacity) {this.capacity = capacity;// 初始化链表initLinkedList();size = 0;cache = new HashMap<>(capacity 2);}/*** 如果节点不存在,返回 -1.如果存在,将节点移动到头结点,并返回节点的数据。** @param key* @return*/public int get(int key) {Entry node = cache.get(key);if (node == null) {return -1;}// 存在移动节点moveToHead(node);return node.value;}/*** 将节点加入到头结点,如果容量已满,将会删除尾结点** @param key* @param value*/public void put(int key, int value) {Entry node = cache.get(key);if (node != null) {node.value = value;moveToHead(node);return;}// 不存在。先加进去,再移除尾结点// 此时容量已满 删除尾结点if (size == capacity) {Entry lastNode = tail.pre;deleteNode(lastNode);cache.remove(lastNode.key);size--;}// 加入头结点Entry newNode = new Entry();newNode.key = key;newNode.value = value;addNode(newNode);cache.put(key, newNode);size ;}private void moveToHead(Entry node) {// 首先删除原来节点的关系deleteNode(node);addNode(node);}private void addNode(Entry node) {head.next.pre = node;node.next = head.next;node.pre = head;head.next = node;}private void deleteNode(Entry node) {node.pre.next = node.next;node.next.pre = node.pre;}public static class Entry {public Entry pre;public Entry next;public int key;public int value;public Entry(int key, int value) {this.key = key;this.value = value;}public Entry() {}}private void initLinkedList() {head = new Entry();tail = new Entry();head.next = tail;tail.pre = head;}public static void main(String[] args) {LRUCache cache = new LRUCache(2);cache.put(1, 1);cache.put(2, 2);System.out.println(cache.get(1));cache.put(3, 3);System.out.println(cache.get(2));}
}
如果大家完全理解了LRU算法,那么练习结果这个相信大家一眼就可以看出:
分析:
1.首先我们给出的容量是2,先push 1 后push 2,那么现在数据存储顺序应该是2,1
2.我们接下来对于1这个值进行了查询,因为1这个值存在,所以1这个值被引用了一次,1这个值放到最上边,返回1,现在的顺序是1,2
3.现在push数据3,因为容量只有2,所以淘汰掉末尾数据2,现在顺序变为3,1
4.最后查询数据2,因为没有数据2,所以返回-1;顺序不变