Cache LRU en Java avec des génériques et des opérations O(1)
C'est une question qui revient beaucoup dans les entretiens d'embauche. L'idée est de définir une structure de données au lieu d'utiliser LinkedHashMap intégré à Java.
Un cache LRU supprime l'entrée la moins récemment utilisée pour en insérer une nouvelle. Donc, étant donné le scénario suivant:
A - B - C - D - E
Où A est L'élément le moins récemment utilisé, si nous devions insérer F, Nous devons supprimer A.
Cela peut être facilement implémenté si nous gardons un HashMap avec les entrées de cache par (key, value) et un liste séparée qui contient la clé et l'heure d'utilisation des éléments. Cependant, nous devrions interroger la liste pour trouver l'élément le moins récemment utilisé, avec une complexité temporelle potentielle O(n).
Comment cette structure peut-elle être implémentée en Java pour les objets génériques et les opérations O(1)?
Ceci est différent du doublon possible en ce sens qu'il se concentre sur l'efficacité (O (1) ops) et l'implémentation de la structure de données elle-même, sans étendre celle de Java.
13 réponses
De la question elle-même, nous pouvons voir que le problème des opérations O(n) se pose lors de l'interrogation de la liste liée. Par conséquent, nous avons besoin d'une structure de données alternative. Nous devons être en mesure de mettre à jour la dernière heure d'accès des éléments à partir du HashMap sans chercher.
Nous pouvons garder deux structures de données distinctes. Un HashMap avec des paires (clé,pointeur) et une liste doublement liée {[6] } qui fonctionnera comme file d'attente prioritaire pour la suppression et stockera les valeurs. De la HashMap, nous pouvons pointer à un élément de la liste doublement liée et mettre à jour son temps de récupération. Parce que nous allons directement du HashMap à l'élément de la liste, notre complexité temporelle reste à O (1)
Par exemple, notre liste doublement liée peut ressembler à:
least_recently_used -> A <-> B <-> C <-> D <-> E <- most_recently_used
Nous devons garder un pointeur vers les éléments LRU et MRU. Les valeurs des entrées seront stockées dans la liste et lorsque nous interrogerons le HashMap, nous obtiendrons un pointeur vers la liste. Sur get (), nous devons mettre l'élément à droite de la liste. Sur put (clé, valeur), si le cache est plein, nous devons supprimer l'élément sur le côté le plus à gauche de la liste à la fois de la liste et du HashMap.
Voici un exemple d'implémentation en Java:
public class LRUCache<K, V>{
// Define Node with pointers to the previous and next items and a key, value pair
class Node<T, U> {
Node<T, U> previous;
Node<T, U> next;
T key;
U value;
public Node(Node<T, U> previous, Node<T, U> next, T key, U value){
this.previous = previous;
this.next = next;
this.key = key;
this.value = value;
}
}
private HashMap<K, Node<K, V>> cache;
private Node<K, V> leastRecentlyUsed;
private Node<K, V> mostRecentlyUsed;
private int maxSize;
private int currentSize;
public LRUCache(int maxSize){
this.maxSize = maxSize;
this.currentSize = 0;
leastRecentlyUsed = new Node<K, V>(null, null, null, null);
mostRecentlyUsed = leastRecentlyUsed;
cache = new HashMap<K, Node<K, V>>();
}
public V get(K key){
Node<K, V> tempNode = cache.get(key);
if (tempNode == null){
return null;
}
// If MRU leave the list as it is
else if (tempNode.key == mostRecentlyUsed.key){
return mostRecentlyUsed.value;
}
// Get the next and previous nodes
Node<K, V> nextNode = tempNode.next;
Node<K, V> previousNode = tempNode.previous;
// If at the left-most, we update LRU
if (tempNode.key == leastRecentlyUsed.key){
nextNode.previous = null;
leastRecentlyUsed = nextNode;
}
// If we are in the middle, we need to update the items before and after our item
else if (tempNode.key != mostRecentlyUsed.key){
previousNode.next = nextNode;
nextNode.previous = previousNode;
}
// Finally move our item to the MRU
tempNode.previous = mostRecentlyUsed;
mostRecentlyUsed.next = tempNode;
mostRecentlyUsed = tempNode;
mostRecentlyUsed.next = null;
return tempNode.value;
}
public void put(K key, V value){
if (cache.containsKey(key)){
return;
}
// Put the new node at the right-most end of the linked-list
Node<K, V> myNode = new Node<K, V>(mostRecentlyUsed, null, key, value);
mostRecentlyUsed.next = myNode;
cache.put(key, myNode);
mostRecentlyUsed = myNode;
// Delete the left-most entry and update the LRU pointer
if (currentSize == maxSize){
cache.remove(leastRecentlyUsed.key);
leastRecentlyUsed = leastRecentlyUsed.next;
leastRecentlyUsed.previous = null;
}
// Update cache size, for the first added entry update the LRU pointer
else if (currentSize < maxSize){
if (currentSize == 0){
leastRecentlyUsed = myNode;
}
currentSize++;
}
}
}
Implémentation qui passe les tests du Leetcode questiton + des tests unitaires simples suivent.
J'ai fait une demande de tirage avec ceci à: https://github.com/haoel/leetcode/pull/90/files
LRUCache.java
import java.util.LinkedHashMap;
import java.util.Iterator;
public class LRUCache {
private int capacity;
private LinkedHashMap<Integer,Integer> map;
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new LinkedHashMap<>();
}
public int get(int key) {
Integer value = this.map.get(key);
if (value == null) {
value = -1;
} else {
this.set(key, value);
}
return value;
}
public void set(int key, int value) {
if (this.map.containsKey(key)) {
this.map.remove(key);
} else if (this.map.size() == this.capacity) {
Iterator<Integer> it = this.map.keySet().iterator();
it.next();
it.remove();
}
map.put(key, value);
}
}
LRUCacheTest.java :
import java.util.ArrayList;
import org.junit.Test;
import static org.junit.Assert.*;
public class LRUCacheTest {
private LRUCache c;
public LRUCacheTest() {
this.c = new LRUCache(2);
}
@Test
public void testCacheStartsEmpty() {
assertEquals(c.get(1), -1);
}
@Test
public void testSetBelowCapacity() {
c.set(1, 1);
assertEquals(c.get(1), 1);
assertEquals(c.get(2), -1);
c.set(2, 4);
assertEquals(c.get(1), 1);
assertEquals(c.get(2), 4);
}
@Test
public void testCapacityReachedOldestRemoved() {
c.set(1, 1);
c.set(2, 4);
c.set(3, 9);
assertEquals(c.get(1), -1);
assertEquals(c.get(2), 4);
assertEquals(c.get(3), 9);
}
@Test
public void testGetRenewsEntry() {
c.set(1, 1);
c.set(2, 4);
assertEquals(c.get(1), 1);
c.set(3, 9);
assertEquals(c.get(1), 1);
assertEquals(c.get(2), -1);
assertEquals(c.get(3), 9);
}
}
RemoveEldestEntry() variante de mise en œuvre de
Pas sûr que cela en vaut la peine car il faut le même nombre de lignes, mais ici va pour achèvement:
import java.util.LinkedHashMap;
import java.util.Iterator;
import java.util.Map;
class LinkedhashMapWithCapacity<K,V> extends LinkedHashMap<K,V> {
private int capacity;
public LinkedhashMapWithCapacity(int capacity) {
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return this.size() > this.capacity;
}
}
public class LRUCache {
private LinkedhashMapWithCapacity<Integer,Integer> map;
public LRUCache(int capacity) {
this.map = new LinkedhashMapWithCapacity<>(capacity);
}
public int get(int key) {
Integer value = this.map.get(key);
if (value == null) {
value = -1;
} else {
this.set(key, value);
}
return value;
}
public void set(int key, int value) {
if (this.map.containsKey(key))
this.map.remove(key);
this.map.get(key);
}
}
Le LinkedHashMap conçus dans cet esprit
À partir des javadocs:
Un constructeur spécial est fourni pour créer une carte de hachage liée dont l'ordre d'itération est l'ordre dans lequel ses entrées ont été accédées en dernier, du moins récemment accédé au plus récemment (access-order). Ce type de carte est bien adapté à la construction de caches LRU. L'appel des méthodes put, putIfAbsent, get, getOrDefault, compute, computeIfAbsent, computeIfPresent ou merge entraîne un accès à l'entrée correspondante (en supposant qu'elle existe après la fin de l'invocation). Les méthodes replace ne donnent accès à l'entrée que si la valeur est remplacée. La méthode putAll génère un accès d'entrée pour chaque mappage dans la carte spécifiée, dans l'ordre où les mappages clé-valeur sont fournis par l'itérateur de jeu d'entrées de la carte spécifiée. Aucune autre méthode ne génère d'accès d'entrée. En particulier, les opérations sur les vues de collection n'affectent pas l'ordre d'itération de la carte de sauvegarde.
Le removeEldestEntry (carte.Entrée) peut être remplacée pour imposer un stratégie de suppression automatique des mappages périmés lorsque de nouveaux mappages ajouté à la carte.
En utilisant une pile et un HashMap:
import java.util.HashMap;
import java.util.Stack;
public class LRU {
private HashMap<String,Object> lruList;
private Stack<String> stackOrder;
private int capacity;
LRU(int capacity){
this.capacity = capacity;
this.lruList = new HashMap<String, Object>(capacity);
this.stackOrder = new Stack<String>();
}
public void put(String key, Object value){
if(lruList.containsKey(key) || this.capacity < lruList.size() + 1) {
if(lruList.containsKey(key)){
String remove = key;
}else{
String remove = this.stackOrder.firstElement();
}
this.stackOrder.removeElement(remove);
this.lruList.remove(remove);
}
this.lruList.put(key, value);
this.stackOrder.add(key);
}
public Stack<String> get(){
return this.stackOrder;
}
public Object get(String key){
Object value = lruList.get(key);
put(key, value);
return value;
}
}
public static void main(String[] args) {
LRU lru = new LRU(3);
lru.put("k1","v1");
lru.put("k2","v2");
lru.put("k3","v3");
System.out.println(lru.get());
lru.get("k1");
System.out.println(lru.get());
lru.put("k4","v4");
System.out.println(lru.get());
}
Sortie:
[k1, k2, k3]
[k2, k3, k1]
[k3, k1, k4]
/*Java implementation using Deque and HashMap */
interface Cache<K,V> {
V get(K key) ;
void set(K key, V value);
}
class Node <K,V>{
K key;
V value;
public Node (K key, V value) {
this.key = key;
this.value = value;
}
}
public class LRUCache <K,V> implements Cache<K,V>{
Deque<Node<K,V>> dq = new LinkedList<>();
Map<K, Node<K, V>> map = new HashMap<>();
static int SIZE;
public LRUCache(int size) {
this.SIZE = size;
}
public V get(K key) {
Node<K,V> result = map.get(key);
if(result != null) {
dq.remove(result);
dq.addFirst(result);
System.out.println("Get " +key +" : " +result.value);
return result.value;
}
else {
System.out.println("Cache miss");
return null;
}
}
public void set(K key, V value) {
System.out.println("Set " +key +" : " +value);
Node<K,V> result = map.get(key);
if(result != null) {
result.value = value;
map.put(key, result);
dq.remove(result);
dq.addFirst(result);
System.out.println("Updated frame " +key+" as " + value);
}
else {
if(dq.size() == SIZE) {
Node<K,V> toRemove = dq.removeLast();
map.remove(toRemove);
System.out.println("Frame removed " +toRemove.key +" : " +toRemove.value);
}
Node<K,V> newNode = new Node(key, value);
dq.addFirst(newNode);
map.put(key, newNode);
System.out.println("Frame added " + key + " : " +value);
}
}
public static void main(String[] args) {
Cache<Integer, Integer> lru = new LRUCache<>(5);
lru.get(2);
lru.set(1, 11);
lru.set(2, 22);
lru.get(2);
lru.set(3, 33);
lru.set(4, 44);
lru.set(5, 55);
lru.get(2);
lru.get(1);
lru.set(6, 66);
}
}
@templatetypedef
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder)
AccessOrder - le mode de classement - vrai pour l'accès d'ordre, faux pour l'insertion d'ordre
Idée
Le Cache n'est rien d'autre qu'une arrayList circulaire. Cette liste contient L'entrée. lorsque de nouvelles entrées arrivent, ajoutez-les à la fin de la liste. Cela signifie l'élément le moins récemment utilisé au premier. Supposons que si vous réutilisez un élément, dissociez-le de la liste et ajoutez-le à la fin.
Pour obtenir n'importe quel élément, nous devons parcourir la liste qui prend O (N) complexité temporelle. Afin d'éviter cela, je maintiens HashMap>. Ici k est la clé et IndexNode contiendra un pointeur vers le Entrée dans la liste. nous pouvons donc obtenir la complexité temporelle O(1).
Solution de travail
package lrucache;
import java.util.HashMap;
public class LRUCache<K, V> {
private transient Entry<K, V> header = new Entry<K, V>(null, null, null, null);
public HashMap<K,IndexNode<Entry<K,V>>> indexMap = new HashMap<K,IndexNode<Entry<K,V>>>();
private final int CACHE_LIMIT = 3;
private int size;
public LRUCache() {
header.next = header.previous = header;
this.size = 0;
}
public void put(K key,V value){
Entry<K,V> newEntry = new Entry<K,V>(key,value,null,null);
addBefore(newEntry, header);
}
private void addBefore(Entry<K,V> newEntry,Entry<K,V> entry){
if((size+1)<(CACHE_LIMIT+1)){
newEntry.next=entry;
newEntry.previous=entry.previous;
IndexNode<Entry<K,V>> indexNode = new IndexNode<Entry<K,V>>(newEntry);
indexMap.put(newEntry.key, indexNode);
newEntry.previous.next=newEntry;
newEntry.next.previous=newEntry;
size++;
}else{
Entry<K,V> entryRemoved = remove(header.next);
indexMap.remove(entryRemoved.key);
addBefore(newEntry, entry);
}
}
public void get(K key){
if(indexMap.containsKey(key)){
Entry<K,V> newEntry = remove(indexMap.get(key).pointer);
addBefore(newEntry,header);
}else{
System.out.println("No such element was cached. Go and get it from Disk");
}
}
private Entry<K,V> remove(Entry<K,V> entry){
entry.previous.next=entry.next;
entry.next.previous = entry.previous;
size--;
return entry;
}
public void display(){
for(Entry<K,V> curr=header.next;curr!=header;curr=curr.next){
System.out.println("key : "+curr.key+" value : " + curr.value);
}
}
private static class IndexNode<Entry>{
private Entry pointer;
public IndexNode(Entry pointer){
this.pointer = pointer;
}
}
private static class Entry<K, V> {
K key;
V value;
Entry<K, V> previous;
Entry<K, V> next;
Entry(K key, V value, Entry<K, V> next, Entry<K, V> previous) {
this.key = key;
this.value = value;
this.next = next;
this.previous = previous;
}
}
public static void main(String[] args) {
LRUCache<String, Integer> cache = new LRUCache<String, Integer>();
cache.put("abc", 1);
//cache.display();
cache.put("def", 2);
cache.put("ghi", 3);
cache.put("xyz", 4);
cache.put("xab", 5);
cache.put("xbc", 6);
cache.get("xyz");
cache.display();
System.out.println(cache.indexMap);
}
}
Sortie
key : xab value : 5
key : xbc value : 6
key : xyz value : 4
{xab=lrucache.LRUCache$IndexNode@c3d9ac, xbc=lrucache.LRUCache$IndexNode@7d8bb, xyz=lrucache.LRUCache$IndexNode@125ee71}
Nous pouvons utiliser LinkedHashMap .. il a une fonction pour supprimer l'entrée la plus âgée
import java.util.LinkedHashMap;
import java.util.Map;
public LRUCache<K, V> extends LinkedHashMap<K, V> {
private int cacheSize;
public LRUCache(int cacheSize) {
super(16, 0.75, true);
this.cacheSize = cacheSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() >= cacheSize;
}
}
Le seul hic est que par défaut l'ordre de liste lié est l'ordre d'insertion, pas l'accès. Pourtant, le constructeur expose une option utiliser l'ordre d'accès à la place.
J'écris juste cette solution générique très simple bien que celle-ci ne soit pas Thread
sûre.
LRUCacheDemo.java (classe publique)
import java.io.*;
import java.util.*;
/* We can keep two separate data structures. A HashMap with (Key,Pointer) pairs and a doubly linked
list which will work as the priority queue for deletion and store the Values. From the HashMap,
we can point to an element in the doubly linked list and update its' retrieval time. Because we
go directly from the HashMap to the item in the list, our time complexity remains at O(1)
*/
public class LRUCacheDemo {
public static void main(String args[]) {
LRUCache<Integer, Integer> lru = new LRUCache<>(3);
for(int i=1; i<=9; ++i) {
lru.put(i, 100*i+10*i+i); //i iii
}
lru.get(8);
/* for(Map.Entry<Integer, Integer> entry : lru.entrySet()) {
System.out.printf("\n%1$-5s %2$-5s", entry.getKey(), entry.getValue());
} */
System.out.println("LRU : " + lru);
}
}
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
//NOTE : LinkedHashMap have already given implementation for LRU, this class has just used those method
//See http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/LinkedHashMap.java#LinkedHashMap
// LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
// accessOrder - to maintain in order of elements from least-recently accessed to most-recently.
LRUCache(final int sizeIn) {
super(sizeIn, 0.75F, true);
this.CACHE_SIZE = sizeIn;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > this.CACHE_SIZE;
/* Returns true if this map should remove its eldest entry. This method is invoked by put and putAll after
inserting a new entry into the map. It provides the implementor with the opportunity to remove the eldest
entry each time a new one is added. This is useful if the map represents a cache.
*/
}
}
O/P :
LRU : {7=777, 9=999, 8=888}
Voici l'implémentation de java avec Generics
et LinkedHashMap
, et la complexité de O(1) get()
et put()
.
import java.util.LinkedHashMap;
public class LRUCache<K, V> {
private LinkedHashMap<K, V> lruCacheMap;
private final int capacity;
private final boolean SORT_BY_ACCESS = true;
private final float LOAD_FACTOR = 0.75F;
public LRUCache(int capacity){
this.capacity = capacity;
this.lruCacheMap = new LinkedHashMap<>(capacity, LOAD_FACTOR, SORT_BY_ACCESS);
}
public V get(K k){
return lruCacheMap.get(k);
}
public void put(K k, V v){
if(lruCacheMap.containsKey(k)){
lruCacheMap.remove(k);
}else if(lruCacheMap.size() >= capacity){
lruCacheMap.remove(lruCacheMap.keySet().iterator().next());
}
lruCacheMap.put(k, v);
}
public void printSequence(){
System.out.println(lruCacheMap.keySet());
}
}
C'est le code pour le tester:
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class TestLruCache {
static class MyHardDrive {
Map<String, Object> resources = new HashMap<>();
MyHardDrive(){
for(Character i='A'; i<='Z'; i++){
resources.put(i.toString(), new Object());
}
}
Object loadResource(String name){
return resources.get(name);
}
}
public static void main(String[] args) {
MyHardDrive hd = new MyHardDrive();
LRUCache<String,Object> cache = new LRUCache<>(4);
for(String key: Arrays.asList("A","B","C","A","D","E","F","E","F","G","A")){
Object object = cache.get(key);
if(object==null){
object = hd.loadResource(key);
cache.put(key, object);
}
cache.printSequence();
}
}
}
Voici l'implémentation java
import java.util.HashMap;
import java.util.Map;
import com.nadeem.app.dsa.adt.Cache;
// Kind of linkedHashMap
public class LRUCache <K, V> implements Cache<K, V> {
private int capacity;
private Node<K, V> head, tail;
private Map<K, Node<K, V>> map;
private static final int DEFAULT_CAPACITY = 10;
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<K, Node<K,V>>();
}
public LRUCache() {
this(DEFAULT_CAPACITY);
}
@Override
public V get(K key) {
V result = null;
Node<K, V> node = this.map.get(key);
if (node != null) {
result = node.value;
remove(node);
addAsHead(node);
}
return result;
}
@Override
public void set(K key, V value) {
Node<K, V> node = this.map.get(key);
if (node == null) {
Node<K, V> temp = new Node<K, V>(key, value);
if (this.map.size() < this.capacity) {
addAsHead(temp);
} else {
this.map.remove(this.tail.key);
remove(this.tail);
addAsHead(temp);
}
this.map.put(key, temp);
} else {
node.value = value;
remove(node);
addAsHead(node);
}
}
private void remove(Node<K, V> node) {
if (node.pre == null) {
this.head = node.next;
} else {
node.pre.next = node.next;
}
if (node.next == null) {
this.tail = node.pre;
} else {
node.next.pre = node.pre;
}
}
private void addAsHead(Node<K, V> node) {
if (this.head == null) {
this.head = node;
this.tail = node;
} else {
this.head.pre = node;
node.next = this.head;
this.head = node;
}
}
@Override
public void remove(K key) {
Node<K, V> node = this.map.get(key);
if (node != null) {
this.remove(node);
}
}
private static class Node <S, T> {
public S key;
public T value;
Node<S, T> pre;
Node<S, T> next;
Node(S key, T value) {
this.key = key;
this.value = value;
}
}
@Override
public int size() {
return this.map.size();
}
}
Voici le test unitaire
public class LRUCacheTest {
private LRUCache<Integer, Integer> cache;
@Before
public void doBeforeEachTestCase() {
this.cache = new LRUCache<Integer, Integer>(2);
}
@Test
public void setTest() {
this.cache.set(1, 1);
assertThat(this.cache.size(), equalTo(1));
assertThat(this.cache.get(1), equalTo(1));
this.cache.set(2, 2);
assertThat(this.cache.size(), equalTo(2));
assertThat(this.cache.get(2), equalTo(2));
this.cache.set(3, 3);
assertThat(this.cache.size(), equalTo(2));
assertThat(this.cache.get(3), equalTo(3));
this.cache.set(1, 3);
assertThat(this.cache.size(), equalTo(2));
assertThat(this.cache.get(1), equalTo(3));
assertThat(this.cache.get(4), equalTo(null));
}
}
public class LeastRecentlyUsed {
public static <K, V> Map<K, V> leastRecentlyUsedCache(final int maxSize) {
return new LinkedHashMap<K, V>(maxSize , 0.7f, true) {
private static final long serialVersionUID = -3588047435434569014L;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
};
}
public static void main(String[] args) {
Map<Object, Object> leastRecentlyUsed = LeastRecentlyUsed.leastRecentlyUsedCache(3);
leastRecentlyUsed.put("Robert", "Raj");
leastRecentlyUsed.put("Auzi", "Aiz");
leastRecentlyUsed.put("Sandy", "S");
leastRecentlyUsed.put("Tript", "tty");
System.out.println(leastRecentlyUsed);
}
}
Comme K et Autres ont dit cela peut être fait avec la LinkedHashMap
classe. À une pression, vous pouvez obtenir une version minimale en 15 lignes de code.
@Ciro Santilli , Pourquoi ne pas simplement couper la définition de classe supplémentaire? Si les tests utilisés mettent comme d'autres cartes java, nous n'aurions pas à alias cette méthode avec une méthode set, que nous nous attendons à retourner une sorte de vue set de la carte.
import java.utils.LinkedHashMap
import java.util.Map;
public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private int maxSize;
// and other constructors for load factor and hashtable capacity
public LRUCache(int initialCapacity, float loadFactor, int maxSize) {
super(initialCapacity, loadFactor, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > maxSize;
}
// I don't like this. set should return a set put should put an item
public V set(K key, V value) {
put(key, value)
}
}
Voir ici et Dans le Docs pour plus.