首页 > 技术文章 > 手写一个LRU工具类

ningbing 2021-05-11 22:13 原文

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 }

 

 
 

推荐阅读