Leetcode146. Mécanisme de mise en cache LRU
Faire des roues
Dans une interview, les intervieweurs s'attendent généralement à ce que les lecteurs mettent en œuvre eux-mêmes une simple liste à double chaînage, au lieu d'utiliser la structure de données encapsulée de la langue. Par conséquent, il est nécessaire de fabriquer des roues.
L'algorithme d'élimination du cache LRU est une stratégie courante. Le nom complet de LRU est le moins récemment utilisé, ce qui se traduit par le moins récemment utilisé.
Leikou Question 146 "LRU Caching Mechanism" est à nous de concevoir une telle structure de données:
Recevoir un premier capacity
paramètre comme capacité maximale du cache, puis implémenter l'API deux, est une put(key, val)
méthode de paires clé-valeur stockée, l'autre get(key)
méthode d'acquisition key
du correspondant val
, s'il key
n'est pas présent ou -1.
La signature de la fonction est la suivante:
class LRUCache {
public LRUCache(int capacity) {
}
public int get(int key) {
}
public void put(int key, int value) {
}
}
Remarque: get
et les put
méthodes doivent à la O(1)
fois être complexes dans le temps, donnons un exemple concret pour voir comment fonctionne l'algorithme LRU.
LRUCache cache = new LRUCache(2); // 缓存容量为 2
// 可以把 cache 理解成一个队列
// 假设左边是队头,右边是队尾
// 那么最近使用的排在队头,最久未使用的排在队尾
// 圆括号表示键值对 (key, val)
cache.put(1, 1); // cache = [(1, 1)]
cache.put(2, 2); // 新来的添加到队头(左边),此时 cache = [(2, 2), (1, 1)]
cache.get(1); // 访问一次 key=1,返回 1。
// 因为(1,1)被访问,被提到队头,此时 cache = [(1, 1), (2, 2)]
cache.put(3, 3); // 新添加一个元素
// 但是此时已经满了,需要将队尾(最久未使用)元素删除,然后将新元素添加到队头
// 此时 cache = [(3, 3), (1, 1)]
cache.get(2); // 返回 -1 (未找到)
cache.put(1, 4); // key=1 已存在,把value覆盖为 4,同时将(1,4)提到队头
// 此时cache = [(1, 4), (3, 3)]
L'analyse de l'opération ci-dessus, de la fabrication put
et de la get
complexité temporelle de la méthode est O (1), nous pouvons conclure que cache
les conditions nécessaires de cette structure de données:
1, il est évident que cache
l'élément temporel doit faire la distinction entre les données récemment utilisées et celles récemment utilisées, lorsque la capacité totale de cet élément doit être supprimée pour faire de la place pour les moins récemment utilisées.
2, nous voulons en cache
trouver rapidement un dans key
existe déjà et a été correspondant val
;
3, chaque visite cache
est un key
besoin d'utiliser cet élément devient récemment, cette opération peut être divisée en trois étapes: ① trouver rapidement cet élément (c'est-à-dire au-dessus de 2); ② les éléments qui ont été supprimés; ③ l'élément est réajouté à le chef de file. Cela permet cache
de prendre en charge la suppression rapide et l'insertion d'éléments dans n'importe quelle position.
Alors, quelle structure de données remplit les conditions ci-dessus en même temps? La recherche de table de hachage est rapide, mais les données n'ont pas d'ordre fixe; la liste liée a un ordre, l'insertion et la suppression sont rapides, mais la recherche est lente. Alors combinez-le pour former une nouvelle structure de données: une liste chaînée de hachage LinkedHashMap
.
La structure de données de base de l'algorithme de cache LRU est une combinaison de liste chaînée de hachage, de liste doublement liée et de table de hachage. Cette structure de données ressemble à ceci:
Deux problèmes peuvent survenir à la lecture de ceci:
- Pourquoi une liste à double lien, une liste à lien unique?
- La table de hachage a été enregistrée
key
, pourquoi la liste devrait-elle existerkey
etval
quoi?
Ces deux questions ne sont pas faciles à expliquer à partir de rien, puis regardez en bas pour trouver les réponses dans le code.
Conception d'architecture :
Notre objectif ultime est d'implémenter LRUCache
une telle structure de données. Avant cela, dessinons d'abord le diagramme d'architecture:
Nous devons mettre en œuvre l' 底层、抽象层、实现层
:.architecture à
Notre couche inférieure a besoin de trois catégories: Node
、DoubleList
、HashMap
La structure générale est la suivante:
Code**:
class Node{
public int key, val;
public Node next, prev;
public Node(int k, int v){
this.key = k;
this.val = v;
}
}
Ensuite, comptez sur notre Node
type pour construire une liste doublement chaînée, plusieurs algorithmes LRU doivent réaliser une API (rien de plus qu'un certain nombre d'ajouts et de suppressions à la méthode):
class DoubleList {
// 头尾虚节点
private Node head, tail;
// 链表元素数
private int size;
public DoubleList() {
// 初始化双向链表的数据
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 在链表尾部添加节点 x,时间 O(1)
public void addLast(Node x) {
x.prev = tail.prev;
x.next = tail;
tail.prev.next = x;
tail.prev = x;
size++;
}
// 删除链表中的 x 节点(x 一定存在)
// 由于是双链表且给的是目标 Node 节点,时间 O(1)
public void remove(Node node) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除链表中第一个节点,并返回该节点,时间 O(1)
public Node removeFirst() {
if (head.next == tail)
return null;
Node first = head.next;
remove(first);
return first;
}
// 返回链表长度,时间 O(1)
public int size() {
return size; }
}
À ce stade, nous pouvons répondre à la question "Pourquoi devons-nous utiliser une liste à double chaînage" tout à l'heure, car nous devons supprimer. La suppression d'un nœud nécessite non seulement le pointeur du nœud lui-même, mais également le pointeur de son nœud prédécesseur. La liste doublement liée peut prendre en charge la recherche directe du prédécesseur et la complexité temporelle de l'opération est O (1).
Parmi eux: addLast(Node x)、remove(Node node)、removeFirst()
le schéma de principe des trois méthodes est le suivant:
![](https://img-blog.csdnimg.cn/20201116142337360.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center)
Notez que l'API de liste à double lien que nous avons implémentée ne peut être insérée qu'à partir de la queue, ce qui signifie que les données sur la queue sont les plus récemment utilisées, et les données sur la tête sont les plus anciennes.
Avec la réalisation de la liste doublement chaînée, il suffit de la combiner avec la table de hachage dans l'algorithme LRU, et d'abord construire le framework de code:
class LRUCache {
// key -> Node(key, val)
private HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)...
private DoubleList cache;
// 最大容量
private int cap;
public LRUCache(int capacity) {
this.cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
/* 将某个 key 提升为最近使用的 */
private void makeRecently(int key) {
Node x = map.get(key);
// 先从链表中删除这个节点
cache.remove(x);
// 重新插到队尾
cache.addLast(x);
}
/* 添加最近使用的元素 */
private void addRecently(int key, int val) {
Node x = new Node(key, val);
// 链表尾部就是最近使用的元素
cache.addLast(x);
// 别忘了在 map 中添加 key 的映射
map.put(key, x);
}
/* 删除某一个 key */
private void deleteKey(int key) {
Node x = map.get(key);
// 从链表中删除
cache.remove(x);
// 从 map 中删除
map.remove(key);
}
/* 删除最久未使用的元素 */
private void removeLeastRecently() {
// 链表头部的第一个元素就是最久未使用的
Node deletedNode = cache.removeFirst();
// 同时别忘了从 map 中删除它的 key
int deletedKey = deletedNode.key;
map.remove(deletedKey);
}
}
Ici, avant de pouvoir répondre aux questions du quiz, "Pourquoi devrait stocker à la fois la clé et la valeur dans la liste, plutôt que de simplement stocker la valeur", faites attention à la removeLeastRecently
fonction, nous devons deletedNode
obtenir deletedKey
.
Autrement dit, lorsque la mémoire tampon est pleine, nous devons non seulement supprimer le dernier Node
nœud, mais également map
mapper vers le nœud key
supprimé, et cela key
ne peut être rendu Node
disponible. Si la Node
structure est uniquement stockée val
, nous ne pouvons pas savoir de key
quoi il s'agit, vous ne pouvez pas supprimer map
les clés, ce qui entraîne des erreurs.
La méthode ci-dessus est un package d'opération simple, pour éviter d'appeler ces fonctions manipuler directement les cache
listes liées et map
les tables de hachage, maintenant je commence à réaliser la get
méthode de l' algorithme LRU :
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
// 将该数据提升为最近使用的
makeRecently(key);
return map.get(key).val;
}
put
La méthode est un peu plus compliquée, dessinons une image pour comprendre sa logique:
![](https://img-blog.csdnimg.cn/20201116142400362.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center)
Nous pouvons donc facilement écrire le put
code de la méthode:
public void put(int key, int val) {
if (map.containsKey(key)) {
// 删除旧的数据
deleteKey(key);
// 新插入的数据为最近使用的数据
addRecently(key, val);
return;
}
if (cap == cache.size()) {
// 删除最久未使用的元素
removeLeastRecently();
}
// 添加为最近使用的元素
addRecently(key, val);
}
À ce stade, vous devriez avoir pleinement saisi le principe et l'implémentation de l'algorithme LRU.
Résolution de problème
Enfin, nous utilisons les types intégrés de Java LinkedHashMap
pour implémenter l'algorithme LRU, et exactement la même logique avant:
class LRUCache {
int capacity;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if(!cache.containsKey(key)){
return -1;
}
makeRecently(key);
return cache.get(key);
}
public void put(int key, int value) {
if(cache.containsKey(key)){
cache.put(key, value); // 修改 key 的值
makeRecently(key);
return;
}
if(cache.size() >= this.capacity){
// 链表头部就是最久未使用的 key
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// 将新的 key 添加链表尾部
cache.put(key, value);
}
private void makeRecently(int key){
int val = cache.get(key);
// 删除 key,重新插入到队尾
cache.remove(key);
cache.put(key, val);
}
}
- Complexité temporelle: à la fois pour mettre et obtenir
O(1)
- Complexité spatiale:
O(capacity)
parce que les tables de hachage et les listes doublement liées stockentcapacity+1
jusqu'à des éléments.