1. 什麼是LRU cache?
就是一種緩存,算法思想是:將最近訪問的數據挪動到頭部,如果下次還是訪問這個數據,那麼就能在靠前的位置訪問到(緩存大小固定,需要淘汰最近最少訪問數據)
這點其實是運用了時間局部性原理:最近一次訪問的位置,下一次也很可能訪問
2. 實現思路:
- 使用雙向鏈表
- 使用哨兵節點,pre 指向尾節點,next 指向頭結點
- 插入操作:push(1,先查找,如果有,挪動到頭部2,如果沒有,檢查cache 是否到達大容量,如果達到了,刪除尾節點,將新節點插入到頭結點)
- 查找操作:如果沒找到,返回null,如果找到,先將目標節點調整到鏈表頭,然後返回該節點
3. 數據舉例說明:
假設輸入序列是 1,3,2,3,4
輸入完1,鏈表效果是
1->哨兵->1
輸入完3,效果是:(使用頭插法,新的元素總是在頭結點)
1 ->哨兵->3->1
輸入2,鏈表效果:
1 ->哨兵->2->3->1
輸入3:(注意cache 中已經存在3,只需要將該節點調整到頭結點即可)
1 ->哨兵->3->2->1
輸入4:注意,cache 大小已經達到最大緩存限制,而且 cache 不存在4,那麼需要線刪除尾節點(也就是1,然後將4插入到頭部)
2->哨兵->4->3->2
4. java 代碼實現:
import java.util.Scanner;
/**
* @author wangwei
* @date 2019/3/28 21:30
* @classDescription 最近最少使用, 放在鏈表末尾
* 使用雙向鏈表:
* 1,pop (查找元素,將目標元素放到鏈表頭部,然後返回數據)
* 2,push 添加元素,檢查size,size如果超過最大,移除最後一個節點,將新節點插入鏈表頭部
*/
public class LRUCache<T> {
private int MAX_SIZE;// cache 最大容量
private int size;
private DoNode sentryNode;// 虛擬節點,pre 指向頭節點,next 指向尾節點
public LRUCache(int MAX_SIZE) {
this.MAX_SIZE = MAX_SIZE;
sentryNode = new DoNode(Integer.MIN_VALUE);
sentryNode.next = sentryNode;
sentryNode.pre = sentryNode;
}
// 查找數據,並將節點返回
public DoNode<T> pop(T data) {
DoNode target = search(data);
if (null == target) {
return null;
}
//將target 調整到頭部
moveExistsNodeToHead(target);
return target;
}
public void push(T data) {
DoNode lookResult = search(data);
if (lookResult != null) {
moveExistsNodeToHead(lookResult);
return;
}
if (size >= MAX_SIZE) {
removeLast();
}
// 插入新節點
insertBeforeHead(data);
}
private DoNode<T> search(T data) {
DoNode result = sentryNode.next;
// 不要繞圈查找,如果到了哨兵還沒查找到,說明緩存不存在
while (result != null && (!result.equals(sentryNode))) {
if (result.data.equals(data)) {
break;
}
result=result.next;
}
return sentryNode.equals(result) ? null : result;
}
//將node 調整緩存中存在的節點到頭部
private void moveExistsNodeToHead(DoNode target) {
//將target 調整到頭部
target.pre.next = target.next;
target.next=target.pre;
target.pre = sentryNode;
sentryNode.next.pre=target;
target.next=sentryNode.next;
sentryNode.next = target;
}
//刪除尾節點
private void removeLast() {
if (size <= 0) {
throw new RuntimeException(" 不能繼續刪除尾節點");
}
sentryNode.pre = sentryNode.pre.pre;
sentryNode.pre.next = sentryNode;
size--;
}
// 頭插入節點
private void insertBeforeHead(T data) {
DoNode node = new DoNode(data);
node.pre = sentryNode;
node.next = sentryNode.next;
node.next.pre=node;
sentryNode.next = node;
size++;
}
public static void main(String[] args) {
LRUCache<Integer> cache = new LRUCache<>(3);
Scanner in = new Scanner(System.in);
int [] input={1,2,3,2,3};
for(int data:input){
cache.push(data);
// System.out.println("當前cache:" + cache.toString());
}
}
class DoNode<T> {
T data;
DoNode pre;
DoNode next;
public DoNode(T data) {
this.data = data;
}
}
}
5. 分析:
- 爲什麼使用雙向鏈表?
方便刪除節點,以及插入節點 - 爲什麼使用哨兵
方便快速定位頭結點與尾節點(插入是在頭結點,刪除是在尾節點)
6另一種更通用的版本實現:
將節點定義由單獨的data改爲 K+DATA的
package top.forethought.linklist;
import java.util.Scanner;
/**
* @author wangwei
* @date 2019/3/28 21:30
* @classDescription 最近最少使用, 放在鏈表末尾
* 使用雙向鏈表:
* 1,pop (查找元素,將目標元素放到鏈表頭部,然後返回數據)
* 2,push 添加元素,檢查size,size如果超過最大,移除最後一個節點,將新節點插入鏈表頭部
*/
public class LRUCache<K,V> {
private int MAX_SIZE;// cache 最大容量
private int size;
private DoNode sentryNode;// 虛擬節點,pre 指向頭節點,next 指向尾節點
public LRUCache(int MAX_SIZE) {
this.MAX_SIZE = MAX_SIZE;
sentryNode = new DoNode();
sentryNode.next = sentryNode;
sentryNode.pre = sentryNode;
}
// 查找數據,並將節點返回
public DoNode<K,V> pop(K key) {
DoNode target = search(key);
if (null == target) {
return null;
}
//將target 調整到頭部
moveExistsNodeToHead(target);
return target;
}
public void push(K key,V data) {
DoNode lookResult = search(key);
if (lookResult != null) {
moveExistsNodeToHead(lookResult);
return;
}
if (size >= MAX_SIZE) {
removeLast();
}
// 插入新節點
insertBeforeHead(key,data);
}
private DoNode search(K key) {
DoNode result = sentryNode.next;
// 不要繞圈查找,如果到了哨兵還沒查找到,說明緩存不存在
while (result != null && (!result.equals(sentryNode))) {
if (key.equals(result.key)) {
break;
}
result = result.next;
}
return sentryNode.equals(result) ? null : result;
}
//將node 調整緩存中存在的節點到頭部
private void moveExistsNodeToHead(DoNode target) {
//將target 調整到頭部
target.pre.next = target.next;
target.next = target.pre;
target.pre = sentryNode;
sentryNode.next.pre = target;
target.next = sentryNode.next;
sentryNode.next = target;
}
//刪除尾節點
private void removeLast() {
if (size <= 0) {
throw new RuntimeException(" 不能繼續刪除尾節點");
}
sentryNode.pre = sentryNode.pre.pre;
sentryNode.pre.next = sentryNode;
size--;
}
// 頭插入節點
private void insertBeforeHead(K key,V data) {
DoNode node = new DoNode(key,data);
node.pre = sentryNode;
node.next = sentryNode.next;
node.next.pre = node;
sentryNode.next = node;
size++;
}
public static void main(String[] args) {
LRUCache<Integer,Integer> cache = new LRUCache<>(3);
Scanner in = new Scanner(System.in);
Integer[] input = {1, 2, 3, 2, 3};
for (Integer data : input) {
Integer key=data-1;
cache.push(key,data);
}
}
class DoNode<K,V> {
K key;
V data;
DoNode pre;
DoNode next;
public DoNode(K key,V data) {
this.key=key;
this.data = data;
}
public DoNode() {
}
}
}