Java 徹底手撕LRU設計(使用JDK容器與不使用JDK容器)
JDK 1.8
本文講述了兩種利用JDK容器實現LRU的方法,以及最後不使用JDK容器,自己定義HashMap和鏈表來純手工打造時間複雜度爲O(1)的LRU算法。
§ 使用JDK容器
1、使用LinkedHashMap
這種方式比較簡單,註釋都在代碼裏
public class LRUCache<K,V>{ // 使用泛型
private LinkedHashMap<K, V> cache;
private int capacity; // 容量
public LRUCache(int capacity) {
cache = new LinkedHashMap<>();
this.capacity = capacity;
}
public V get(K key) {
if (cache.containsKey(key)) {
V val = cache.get(key);
cache.remove(key); // 刪除key
cache.put(key, val); // 重新put,實現了生命週期的更新
return val;
} else {
return null;
}
}
public void put(K key, V val) {
if (cache.containsKey(key)) {
cache.remove(key);
} else if (cache.size() >= capacity) {
Iterator itr = cache.keySet().iterator();
cache.remove(itr.next()); // 刪除頭
}
cache.put(key, val);
}
}
2、使用HashMap + LinkedList
- HashMap存儲K-V元素
- LinkedList按序存儲key
import java.util.HashMap;
import java.util.LinkedList;
/**
* @description:
* @Author: JachinDo
* @Date: 2020/04/24 22:04
*/
public class LRUCache2<K, V> {
private HashMap<K, V> map;
private LinkedList<K> list;
private int capacity;
public LRUCache2(int capacity) {
map = new HashMap<>();
list = new LinkedList<K>();
this.capacity = capacity;
}
public V get(K key) {
if (map.containsKey(key)) {
V val = map.get(key);
list.remove(key); // 更新
list.addLast(key);
return val;
} else {
return null;
}
}
public void put(K key, V value) {
if (map.containsKey(key)) {
map.remove(key);
list.remove(key);
} else if (map.size() >= capacity) {
K oldKey = list.removeFirst(); // 刪除頭
map.remove(oldKey);
}
list.addLast(key);
map.put(key, value);
}
}
§ 不使用JDK容器實現O(1)的LRU – 純手工打造LRU
- 自定義HashMap:
JachinHashMap<K, Node>
- 核心:HashMap的value值封裝爲Node節點(包含k,v,以及前後指針,形成鏈表),這樣即以O(1)的時間複雜度維持順序,避免了LinkedList的O(n)。
1、自定義HashMap
代碼結構圖:
註釋很詳細,結合代碼結構圖更清晰
/**
* @description: 實現簡易HashMap
* @Author: JachinDo
* @Date: 2020/04/25 00:17
*/
public class JachinHashMap<K, V>{
private Node<K, V>[] table; // 內部元素封裝爲Node類,存儲在一個數組中
private int length; // 初始化時指定數組長度
private int size; // HashMap實際元素個數
public JachinHashMap(int length) {
this.length = length;
table = new Node[length];
}
/**
* 返回當前map中元素個數
*/
public int size() {
return size;
}
/**
* 添加元素
*/
public V put(K key, V value) {
// 根據key的hash值獲取索引下標
int index = key.hashCode() % (length - 1);
// 拿到槽點元素
Node<K, V> node = table[index];
if (node == null) {
// 槽點爲空,則直接設置元素
table[index] = new Node(key, value);
} else {
// 如已有元素,則插入鏈表(包含覆蓋舊值或新增節點)
while (node != null) {
if (node.getKey().equals(key)) {
node.setVallue(value); // 覆蓋舊值
break;
} else {
node = node.next;
}
}
if (node == null) {
// 需要新增節點
Node newNode = new Node(key, value);
newNode.next = table[index];
table[index].pre = newNode;
table[index] = newNode;
}
}
size++;
return table[index].getValue();
}
/**
* 獲取元素
*/
public V get(K key) {
int index = key.hashCode() % (length - 1);
// 拿到槽點元素
Node<K,V> node = table[index];
if (node == null) {
// 若爲空,說明不存在,返回null
return null;
} else if (node.getKey().equals(key)) {
return node.getValue(); // 槽點元素即滿足則直接返回
} else {
while (node != null) { // 遍歷鏈表
if (node.getKey().equals(key)) {
return node.getValue(); // 找到滿足key的元素,返回。
} else {
node = node.next;
}
}
return null; // 遍歷完還是沒找到,返回null
}
}
/**
* 刪除指定key對應元素
*/
public V remove(K key) {
int index = key.hashCode() % (length - 1);
// 拿到槽點元素
Node<K,V> node = table[index];
if (node == null) {
// 所刪元素不存在。
return null;
} else if (node.getKey().equals(key)) {
V value = node.getValue();
// 刪除槽點元素(頭節點),設置新頭
Node next = node.next;
node.next = null;
if (next != null) {
next.pre = null;
}
table[index] = next;
size--;
return value;
} else {
while (node != null) {
// 遍歷鏈表,找到滿足條件key,刪除。
if (node.getKey().equals(key)) {
V value = node.getValue();
// 刪除節點,並重新組建鏈表連接
Node pre = node.pre;
Node next = node.next;
node.pre = null;
node.next = null;
if (pre != null) {
pre.next = next;
}
if (next != null) {
next.pre = pre;
}
size--;
return value;
} else {
node = node.next;
}
}
return null; // 沒有滿足條件的元素
}
}
/**
* JachinHashMap中的元素封裝類型
*/
class Node<K, V> {
K key;
V value;
Node<K, V> next;
Node<K, V> pre;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public void setKey(K key) {
this.key = key;
}
public void setVallue(V value) {
this.value = value;
}
}
}
2、LRU的實現
代碼結構圖:
註釋很詳細,結合代碼結構圖更清晰
/**
* @description:
* @Author: JachinDo
* @Date: 2020/04/24 22:57
*/
public class JachinLRUCache<K, V> {
private JachinHashMap<K, JachinLRUNode> map; // 將value封裝爲Node類,便於在鏈表中進行索引
private int capacity; // 緩存大小
// 維護鏈表頭尾,便於刪頭,插尾
private JachinLRUNode head;
private JachinLRUNode tail;
public JachinLRUCache(int capacity) {
this.capacity = capacity;
map = new JachinHashMap<>(capacity);
}
/**
* 添加元素
*/
public void put(K key, V value) {
JachinLRUNode node = map.get(key);
if (node != null) {
// 若存在,更新值,並將節點移至末尾
node.value = value;
updateNode(node);
} else { // 若不存在,則需判斷是否需要執行lru淘汰,並插入新節點
JachinLRUNode newNode = new JachinLRUNode(key, value); // 創建節點
if (map.size() == capacity) { // 若容量滿,需要刪除頭節點(最近最少訪問節點)
JachinLRUNode<K, V> oldHead = removeHead(); // 鏈表中刪除
map.remove(oldHead.key); // map中刪除
}
addTail(newNode); // 將節點插入尾部
map.put(key, newNode); // 將節點加入map
}
}
/**
* 獲取元素
*/
public V get(K key) {
JachinLRUNode<K, V> node = map.get(key);
if (node != null) {
// 若存在,則更新節點位置到尾部
updateNode(node);
return node.value;
}
return null;
}
/**
* 更新節點位置,將剛訪問過的節點移至鏈表尾部
*/
public void updateNode(JachinLRUNode node) {
if (tail == node) { // 如果當前node就是末尾,則不做操作
return;
}
if (head == node) { // 如果當前node是頭節點,則將其下一節點作爲頭節點
head = node.next;
head.pre = null;
} else {
// 調整雙向鏈表指針,將當前節點刪除
node.pre.next = node.next;
node.next.pre = node.pre;
}
// 將當前節點接到尾部,成爲新的尾節點
node.pre = tail;
node.next = null;
tail.next = node;
tail = node;
}
/**
* 刪除鏈表頭節點
*/
public JachinLRUNode removeHead() {
if (head == null) {
return null;
}
JachinLRUNode oldHead = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = oldHead.next; // 設置新頭
oldHead.next = null; // 斷開連接
head.pre = null;
}
return oldHead; // 返回舊頭,以便map中刪除
}
/**
* 添加節點到鏈表尾部
*/
public void addTail(JachinLRUNode newNode) {
if (newNode == null) {
return;
}
if (head == null) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode; // 插入新節點
newNode.pre = tail;
tail = newNode; // 更新尾節點爲新節點
}
}
}
/**
* LRUCache中value元素封裝類型
* 用JachinLRUNode實現雙向鏈表,實現O(1)的刪除節點(刪除最近最少使用節點)
*/
class JachinLRUNode<K, V> {
K key;
V value;
JachinLRUNode pre;
JachinLRUNode next;
public JachinLRUNode(K key, V value) {
this.key = key;
this.value = value;
}
}