手写一个LRU工具类
LRU概述
LRU算法,即最近最少使用算法。其使用场景非常广泛,像我们日常用的手机的后台应用展示,软件的复制粘贴板等。
本文将基于算法思想手写一个具有LRU算法功能的Java工具类。
结构设计
在插入数据时,需要能快速判断是否已有相同数据。为实现该目的,可以使用hash表结构。
同时根据LRU的规则,在对已有元素进行查找和修改操作后,该元素应该被置于首位;在增加元素时,如果超过了最大容量,则会淘汰末尾元素。为减少元素移动的时间复杂度,这里采用双端链表结构,使得移动元素到首位和删除末尾元素的时间复杂度都为O(1)。
根据上述数据结构,可以定义元素节点内容,包含hash值,键K,值value,先继节点和后继节点。如下所示:
1 static class Entry<K,V> { 2 final int hash; // 哈希值 3 final K key; // 键 4 V value; // 值 5 Entry<K,V> before; // 先继节点 6 Entry<K,V> after; // 后继节点 7 Entry(int hash, K key, V value, Entry before, Entry after) { 8 this.hash = hash; 9 this.key = key; 10 this.value = value; 11 this.before = before; 12 this.after = after; 13 } 14 }
双端链表则需要存储头节点和尾节点。
其它成员变量如下:
1 int maxSize; // 最大容量 2 Entry<K,V> head; // 头节点 3 Entry<K,V> tail; // 尾节点 4 HashMap<K,V> hashMap; // 哈希表
在实现容器的增删改查方法前,我们先把一些对链表的共用操作抽象出来,包括查找链表节点、将链表节点移动到队首、删除链表中节点。对应方法实现如下:
1 // 根据key从链表中找对应节点 2 Entry<K, V> find(Object key) { 3 // 遍历链表找到该元素 4 Entry<K,V> entry = head; 5 while (entry != null) { 6 if (entry.key.equals(key)) 7 break; 8 entry = entry.after; 9 } 10 return entry; 11 } 12 // 将key对应的元素移至队首 13 Entry<K,V> moveToFront(Object key) { 14 // 遍历链表找到该元素 15 Entry<K,V> entry = find(key); 16 // 如果找到了并且不是队首,则将该节点移动到队列的首部 17 if (entry != null && entry != head) { 18 // 如果该节点是队尾 19 if (entry == tail) 20 tail = entry.before; 21 // 先将该节点从链表中移出 22 Entry<K,V> p = entry.before; 23 Entry<K,V> q = entry.after; 24 p.after = q; 25 if (q != null) 26 q.before = p; 27 // 然后将该节点作为新的head 28 entry.before = null; 29 entry.after = head; 30 head = entry; 31 } 32 return entry; 33 } 34 // 将key对应的元素从双端链表中删除 35 void removeFromLinkedList(Object key) { 36 // 遍历链表找到该元素 37 Entry<K,V> entry = find(key); 38 // 如果没找到则直接返回 39 if (entry == null) return; 40 // 如果是队首元素 41 if (entry == head) { 42 // 只有一个节点 43 if (tail == head) 44 tail = entry.after; 45 head = entry.after; 46 head.before = null; 47 } else if (entry == tail) { 48 // 如果是队尾元素 49 tail = tail.before; 50 tail.after = null; 51 } else { 52 // 如果是中间元素 53 Entry<K,V> before = entry.before; 54 Entry<K,V> after = entry.after; 55 before.after = after; 56 after.before = before; 57 } 58 }
put()方法
put元素时需要判断元素是否已经在容器中存在,如果存在,则修改对应节点的值,并将该节点移动到链表的头部。
如果不存在,则将元素插入到链表的头部。如果此时容量超过预设最大容量,需要将队列尾部元素移除。
注意:上述操作需要判断是否更新头尾节点。
代码如下:
1 // 存入元素/修改元素 2 public void put(K key, V value) { 3 V res = hashMap.put(key,value); 4 // 如果res为null,表示没找到,则存入并放置到队首 5 if (res == null) { 6 Entry<K,V> entry = new Entry<>(key.hashCode(), key, value, null, head); 7 // 如果之前没有头节点 8 if (head == null) { 9 head = entry; 10 tail = entry; 11 } else { 12 // 如果之前有头节点,将头节点before指向entry 13 entry.after = head; 14 head.before = entry; 15 head = entry; 16 } 17 // 判断此时节点数量是否超过最大容量,如果超过,则将队尾元素删除 18 if (hashMap.size() > maxSize) { 19 tail = tail.before; 20 tail.after = null; 21 } 22 } else { 23 // 如果res不为null,表示包含该元素,则将节点放置到队首 24 Entry<K,V> entry = moveToFront(key); 25 // 同时修改节点的V值 26 entry.value = value; 27 } 28 }
remove()方法
从容器中删除元素,需要判断是否在容器中存在。同时也要注意更新头尾节点。
1 // 删除元素 2 public void remove(Object key) { 3 V res = hashMap.remove(key); 4 // 如果删除成功,则将链表中节点一并删除 5 if (res != null) 6 removeFromLinkedList(key); 7 }
get()方法
查找元素如果找到的话需要将对应节点移动到队列头部。
1 // 查询元素 2 public V get(Object key) { 3 V res = hashMap.get(key); 4 // 如果在已有数据中找到,则将该元素放置到队首 5 if (res != null) 6 moveToFront(key); 7 return res; 8 }
完整代码
完整代码以及测试如下:
1 package com.simple.test; 2 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.List; 6 7 public class SimpleLRUCache <K,V>{ 8 int maxSize; // 最大容量 9 Entry<K,V> head; // 头节点 10 Entry<K,V> tail; // 尾节点 11 HashMap<K,V> hashMap; // 哈希表 12 // 构造函数 13 public SimpleLRUCache(int size) { 14 if (size <= 0) 15 throw new RuntimeException("容器大小不能<=0"); 16 this.maxSize = size; 17 this.hashMap = new HashMap<>(); 18 } 19 static class Entry<K,V> { 20 final int hash; // 哈希值 21 final K key; // 键 22 V value; // 值 23 Entry<K,V> before; // 先继节点 24 Entry<K,V> after; // 后继节点 25 Entry(int hash, K key, V value, Entry before, Entry after) { 26 this.hash = hash; 27 this.key = key; 28 this.value = value; 29 this.before = before; 30 this.after = after; 31 } 32 } 33 // 查询元素 34 public V get(Object key) { 35 V res = hashMap.get(key); 36 // 如果在已有数据中找到,则将该元素放置到队首 37 if (res != null) 38 moveToFront(key); 39 return res; 40 } 41 // 存入元素/修改元素 42 public void put(K key, V value) { 43 V res = hashMap.put(key,value); 44 // 如果res为null,表示没找到,则存入并放置到队首 45 if (res == null) { 46 Entry<K,V> entry = new Entry<>(key.hashCode(), key, value, null, head); 47 // 如果之前没有头节点 48 if (head == null) { 49 head = entry; 50 tail = entry; 51 } else { 52 // 如果之前有头节点,将头节点before指向entry 53 entry.after = head; 54 head.before = entry; 55 head = entry; 56 } 57 // 判断此时节点数量是否超过最大容量,如果超过,则将队尾元素删除 58 if (hashMap.size() > maxSize) { 59 tail = tail.before; 60 tail.after = null; 61 } 62 } else { 63 // 如果res不为null,表示包含该元素,则将节点放置到队首 64 Entry<K,V> entry = moveToFront(key); 65 // 同时修改节点的V值 66 entry.value = value; 67 } 68 } 69 // 删除元素 70 public void remove(Object key) { 71 V res = hashMap.remove(key); 72 // 如果删除成功,则将链表中节点一并删除 73 if (res != null) 74 removeFromLinkedList(key); 75 } 76 // 将key对应的元素移至队首 77 Entry<K,V> moveToFront(Object key) { 78 // 遍历链表找到该元素 79 Entry<K,V> entry = find(key); 80 // 如果找到了并且不是队首,则将该节点移动到队列的首部 81 if (entry != null && entry != head) { 82 // 如果该节点是队尾 83 if (entry == tail) 84 tail = entry.before; 85 // 先将该节点从链表中移出 86 Entry<K,V> p = entry.before; 87 Entry<K,V> q = entry.after; 88 p.after = q; 89 if (q != null) 90 q.before = p; 91 // 然后将该节点作为新的head 92 entry.before = null; 93 entry.after = head; 94 head = entry; 95 } 96 return entry; 97 } 98 // 将key对应的元素从双端链表中删除 99 void removeFromLinkedList(Object key) { 100 // 遍历链表找到该元素 101 Entry<K,V> entry = find(key); 102 // 如果没找到则直接返回 103 if (entry == null) return; 104 // 如果是队首元素 105 if (entry == head) { 106 // 只有一个节点 107 if (tail == head) 108 tail = entry.after; 109 head = entry.after; 110 head.before = null; 111 } else if (entry == tail) { 112 // 如果是队尾元素 113 tail = tail.before; 114 tail.after = null; 115 } else { 116 // 如果是中间元素 117 Entry<K,V> before = entry.before; 118 Entry<K,V> after = entry.after; 119 before.after = after; 120 after.before = before; 121 } 122 } 123 // 根据key从链表中找对应节点 124 Entry<K, V> find(Object key) { 125 // 遍历链表找到该元素 126 Entry<K,V> entry = head; 127 while (entry != null) { 128 if (entry.key.equals(key)) 129 break; 130 entry = entry.after; 131 } 132 return entry; 133 } 134 // 顺序返回元素 135 public List<Entry<K,V>> getList() { 136 List<Entry<K,V>> list = new ArrayList<>(); 137 Entry<K,V> p = head; 138 while (p != null) { 139 list.add(p); 140 p = p.after; 141 } 142 return list; 143 } 144 // 顺序输出元素 145 public void print() { 146 Entry<K,V> p = head; 147 while (p != null) { 148 System.out.print(p.key.toString()+":"+p.value.toString()+"\t"); 149 p = p.after; 150 } 151 System.out.println(); 152 } 153 public static void main(String[] args) { 154 SimpleLRUCache<String, String> test = new SimpleLRUCache(4); 155 test.put("a","1"); 156 test.put("b","2"); 157 test.put("c","3"); 158 test.put("d","4"); 159 // 此时顺序为d c b a 160 test.print(); 161 // 获取a,此时顺序为 a d c b 162 test.get("a"); 163 test.print(); 164 // 修改c,此时顺序为 c a d b 165 test.put("c","31"); 166 test.print(); 167 // 增加e,淘汰末尾元素b,此时顺序为e c a d 168 test.put("e","5"); 169 test.print(); 170 } 171 }
后续优化
之前移动链表节点和删除链表节点时还需要遍历链表进行查找,其实完全可以交给hashmap来处理,将hashmap的V定义为Entry<K,V>类型即可。修改后的完整代码如下:
1 package com.simple.rpc.lru; 2 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.List; 6 7 public class SimpleLRUCache2 <K,V>{ 8 int maxSize; // 最大容量 9 Entry<K,V> head; // 头节点 10 Entry<K,V> tail; // 尾节点 11 HashMap<K,Entry<K,V>> hashMap; // 哈希表 12 // 构造函数 13 public SimpleLRUCache2(int size) { 14 if (size <= 0) 15 throw new RuntimeException("容器大小不能<=0"); 16 this.maxSize = size; 17 this.hashMap = new HashMap<>(); 18 } 19 static class Entry<K,V> { 20 K key; // 键 21 V value; // 值 22 Entry<K,V> before; // 先继节点 23 Entry<K,V> after; // 后继节点 24 Entry(K key, V value, Entry before, Entry after) { 25 this.value = value; 26 this.before = before; 27 this.after = after; 28 } 29 } 30 // 查询元素 31 public V get(Object key) { 32 Entry<K,V> res = hashMap.get(key); 33 // 如果在已有数据中找到,则将该元素放置到队首 34 if (res != null) 35 moveToFront(key); 36 return res.value; 37 } 38 // 存入元素/修改元素 39 public void put(K key, V value) { 40 // 如果hashmap中不包含key,则存入并放置到队首 41 if (!hashMap.containsKey(key)) { 42 Entry<K,V> entry = new Entry<>( key, value, null, null); 43 // 如果之前没有头节点 44 if (head == null) { 45 head = entry; 46 tail = entry; 47 } else { 48 // 如果之前有头节点,将头节点before指向entry 49 entry.after = head; 50 head.before = entry; 51 head = entry; 52 } 53 // 判断此时节点数量是否超过最大容量,如果超过,则将队尾元素删除 54 if (hashMap.size() > maxSize) { 55 tail = tail.before; 56 tail.after = null; 57 } 58 } else { 59 // 如果包含该元素,则将节点放置到队首 60 Entry<K,V> entry = moveToFront(key); 61 // 同时修改节点的V值 62 entry.value = value; 63 } 64 } 65 // 删除元素 66 public void remove(Object key) { 67 Entry<K,V> res = hashMap.remove(key); 68 // 如果删除成功,则将链表中节点一并删除 69 if (res != null) 70 removeFromLinkedList(res); 71 } 72 // 将key对应的元素移至队首 73 Entry<K,V> moveToFront(Object key) { 74 // 遍历链表找到该元素 75 Entry<K,V> entry = hashMap.get(key); 76 // 如果找到了并且不是队首,则将该节点移动到队列的首部 77 if (entry != null && entry != head) { 78 // 如果该节点是队尾 79 if (entry == tail) 80 tail = entry.before; 81 // 先将该节点从链表中移出 82 Entry<K,V> p = entry.before; 83 Entry<K,V> q = entry.after; 84 p.after = q; 85 if (q != null) 86 q.before = p; 87 // 然后将该节点作为新的head 88 entry.before = null; 89 entry.after = head; 90 head = entry; 91 } 92 return entry; 93 } 94 // 将节点entry从双端链表中删除 95 void removeFromLinkedList(Entry<K, V> entry) { 96 // 如果没找到则直接返回 97 if (entry == null) return; 98 // 如果是队首元素 99 if (entry == head) { 100 // 只有一个节点 101 if (tail == head) 102 tail = entry.after; 103 head = entry.after; 104 head.before = null; 105 } else if (entry == tail) { 106 // 如果是队尾元素 107 tail = tail.before; 108 tail.after = null; 109 } else { 110 // 如果是中间元素 111 Entry<K,V> before = entry.before; 112 Entry<K,V> after = entry.after; 113 before.after = after; 114 after.before = before; 115 } 116 } 117 // 顺序返回元素 118 public List<Entry<K,V>> getList() { 119 List<Entry<K,V>> list = new ArrayList<>(); 120 Entry<K,V> p = head; 121 while (p != null) { 122 list.add(p); 123 p = p.after; 124 } 125 return list; 126 } 127 // 顺序输出元素 128 public void print() { 129 Entry<K,V> p = head; 130 while (p != null) { 131 System.out.print(p.key.toString()+":"+p.value.toString()+"\t"); 132 p = p.after; 133 } 134 System.out.println(); 135 } 136 public static void main(String[] args) { 137 SimpleLRUCache<String, String> test = new SimpleLRUCache(4); 138 test.put("a","1"); 139 test.put("b","2"); 140 test.put("c","3"); 141 test.put("d","4"); 142 // 此时顺序为d c b a 143 test.print(); 144 // 获取a,此时顺序为 a d c b 145 test.get("a"); 146 test.print(); 147 // 修改c,此时顺序为 c a d b 148 test.put("c","31"); 149 test.print(); 150 // 增加e,淘汰末尾元素b,此时顺序为e c a d 151 test.put("e","5"); 152 test.print(); 153 } 154 }