前言:

什么是LRU算法:LRU是Least Recently Used的缩写,即最近最久未使用,是一种操作系统中常用的页面置换算法。

应用场景:

知道了什么是LRU后,我们再来聊下它的使用场景;在工作中,对于Redis我们一定是比较熟悉的,它是一个内存数据库;因为它是内存数据库,并且内存的空间是有限的,如果Redis中数据量很大的话,内存就可能被占满,但是此时如果还有数据存入Redis的话,那该怎么办呢?这就是由Redis的的内存淘汰策略所决定的。

LRU最近最久未使用算法就是Redis的内存淘汰策略之一。 

示例:

// 当前缓存的容量为2
LRUCache cache = new LRUCache( 2 );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

设计LRU算法的数据结构:

1、要求:
①、首先支持查询数据get和写入数据put;
②、满足时间复杂度为O(1);

2、思路:
由题目中要求的O(1)时间复杂度想到缓存可以想到用一个map来存储key、value结点,最近最久未使用到的(缓存数据)放到最后,最新访问的(缓存数据)放到最前面,可以考虑用双向链表来实现,这样,这个map的key对应的是缓存的Key, value对应的是双向链表的一个节点,即链表的节点同时存在map的value中。

这样,当新插入一个节点时,它应该在这个双向链表的头结点处,同时把这个节点的key和这个节点put到map中保留下来。当LRU缓存链表容量达到最大又要插入新节点时,把链表的尾节点删除掉,同时在map中移除该节点对应的key。

双向链表中节点的数据结构:

public class DoubleLinkedListNode {
	
	String key;
	Object value;
	// 头指针
	DoubleLinkedListNode pre;
	// 尾指针
	DoubleLinkedListNode next;
	
	
	public DoubleLinkedListNode(String key, Object value) {
        this.key = key;
        this.value = value;
    }

}

由此可以抽象出LRU缓存算法的数据结构:双向链表+HashMap。

数据结构逻辑图如下所示:

java 从缓存取出 LIst java lru缓存_缓存

代码奉上:

import java.util.HashMap;


public class LRUCache {

	private HashMap<String, DoubleLinkedListNode> map = new HashMap<String, DoubleLinkedListNode>();
	// 头结点
	private DoubleLinkedListNode head;
	// 尾节点
	private DoubleLinkedListNode tail;
	// 双向链表的容量
	private int capacity;
	// 双向链表中节点的数量
	private int size;

	public LRUCache(int capacity) {
		this.capacity = capacity;
		size = 0;
	}

	/**
	 * @Description: 将节点设置为头结点
	 * @param node
	 */
	public void setHead(DoubleLinkedListNode node) {
		// 节点的尾指针执行头结点
		node.next = head;
		// 节点的头指针置为空
		node.pre = null;
		if (head != null) {
			// 将头结点的头指针执行节点
			head.pre = node;
		}
		head = node;
		if (tail == null) {
			// 如果双向链表中还没有节点时,头结点和尾节点都是当前节点
			tail = node;
		}
	}

	/**
	 * @Description:将双向链表中的节点移除
	 * @param node
	 */
	public void removeNode(DoubleLinkedListNode node) {
		DoubleLinkedListNode cur = node;
		DoubleLinkedListNode pre = cur.pre;
		DoubleLinkedListNode post = cur.next;
		// 如果当前节点没有头指针的话,说明它是链表的头结点
		if (pre != null) {
			pre.next = post;
		} else {
			head = post;
		}
		// 如果当前节点没有尾指针的话,说明当前节点是尾节点
		if (post != null) {
			post.pre = pre;
		} else {
			tail = pre;
		}
	}

	/**
	 * @Description:从缓存Cache中get
	 * @param key
	 * @return
	 */
	public Object get(String key) {
		// 使用hashmap进行查询,时间复杂度为O(1),如果进行链表查询,需要遍历链表,时间复杂度为O(n)
		if (map.containsKey(key)) {
			DoubleLinkedListNode node = map.get(key);
			// 将查询出的节点从链表中移除
			removeNode(node);
			// 将查询出的节点设置为头结点
			setHead(node);
			return node.value;
		}
		// 缓存中没有要查询的内容
		return null;
	}

	/**
	 * @Description:将key-value存储set到缓存Cache中
	 * @param key
	 * @param value
	 */
	public void set(String key, Object value) {
		if (map.containsKey(key)) {
			DoubleLinkedListNode node = map.get(key);
			node.value = value;
			removeNode(node);
			setHead(node);
		} else {
			// 如果缓存中没有词key-value
			// 创建一个新的节点
			DoubleLinkedListNode newNode = new DoubleLinkedListNode(key, value);
			// 如果链表中的节点数小于链表的初始容量(还不需要进行数据置换)则直接将新节点设置为头结点
			if (size < capacity) {
				setHead(newNode);
				// 将新节点放入hashmap中,用于提高查找速度
				map.put(key, newNode);
				size++;
			} else {
				// 缓存(双向链表)满了需要将"最近醉酒未使用"的节点(尾节点)删除,腾出新空间存放新节点
				// 首先将map中的尾节点删除
				map.remove(tail.key);
				// 移除尾节点并重新置顶尾节点的头指针指向的节点为新尾节点
				removeNode(tail);
				// 将新节点设置为头节点
				setHead(newNode);
				// 将新节点放入到map中
				map.put(key, newNode);
			}
		}
	}

	/**
	 * @Description: 遍历双向链表
	 * @param head
	 *            双向链表的 头结点
	 */
	public void traverse(DoubleLinkedListNode head) {
		DoubleLinkedListNode node = head;
		while (node != null) {
			System.out.print(node.key + "  ");
			node = node.next;
		}
		System.out.println();
	}

	// test
	public static void main(String[] args) {
		System.out.println("双向链表容量为6");
		LRUCache lc = new LRUCache(6);

		// 向缓存中插入set数据
		for (int i = 0; i < 6; i++) {
			lc.set("test" + i, "test" + i);
		}

		// 遍历缓存中的数据,从左到右,数据越不经常使用
		System.out.println("第一次遍历双向链表:(从头结点遍历到尾节点)");
		lc.traverse(lc.head);

		// 使用get查询缓存中数据
		lc.get("test2");

		// 再次遍历缓存中的数据,从左到右,数据越不经常使用,并且此次发现刚刚操作的数据节点位于链表的头结点了。
		System.out.println();
		System.out.println("get查询 test2节点后 ,第二次遍历双向链表:");
		lc.traverse(lc.head);

		// 再次向缓存中插入数据,发现缓存链表已经满了,需要将尾节点移除
		lc.set("sucess", "sucess");

		/**
		 * 再次遍历缓存中的数据,从左到右,数据越不经常使用,并且此次发现刚刚set操作时由于链表满了, 就将尾节点test0
		 * 移除了,并且将新节点置为链表的头结点。
		 */
		System.out.println();
		System.out.println("put插入sucess节点后,第三次遍历双向链表:");
		lc.traverse(lc.head);
	}

}

运行结果展示:

双向链表容量为6
第一次遍历双向链表:(从头结点遍历到尾节点)
test5  test4  test3  test2  test1  test0  

get查询 test2节点后 ,第二次遍历双向链表:
test2  test5  test4  test3  test1  test0  

put插入sucess节点后,第三次遍历双向链表:
sucess  test2  test5  test4  test3  test1

参考资料:

1、记一次阿里面试,我挂在了 最熟悉不过的LRU 缓存算法设计上。。。。。

2、【LeetCode】146. LRU缓存机制

3、LRU Cache leetcode java