C'est une question qui revient souvent dans les entretiens d'embauche. L'idée est de définir une structure de données au lieu d'utiliser l'interface LinkedHashMap de Java.
Un cache LRU supprime l'entrée la moins récemment utilisée pour en insérer une nouvelle . Ainsi, dans le scénario suivant:
A - B - C - D - E
Où A est l'élément le moins récemment utilisé, si nous insérons F, nous devons supprimer A.
Cela peut être facilement implémenté si nous gardons un HashMap avec les entrées de cache par (clé, valeur) et une liste séparée qui contient la clé et le moment d'utilisation des éléments. Cependant, nous aurions besoin d'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 dans 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 l'extension de celle de Java.
La question elle-même montre 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 pouvoir mettre à jour la dernière heure d'accès des éléments à partir de HashMap sans chercher.
Nous pouvons conserver deux structures de données distinctes. Un HashMap avec (clé, pointeur) paires et une liste doublement chaînée qui fonctionnera comme file d'attente prioritaire pour la suppression et stockera les valeurs À partir de HashMap, nous pouvons pointer sur un élément de la liste doublement chaînée et mettre à jour son heure de récupération. Étant donné que nous passons directement de la carte de hachage à l'élément de la liste, notre complexité temporelle reste de 0 (1).
Par exemple, notre liste doublement chaînée peut ressembler à:
least_recently_used -> A <-> B <-> C <-> D <-> E <- most_recently_used
Nous devons garder un pointeur sur 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 sur la liste. Sur get (), nous devons placer l'élément à l'extrême droite de la liste. Au moment de la mise en place (clé, valeur), si le cache est plein, nous devons supprimer l'élément situé à l'extrême gauche de la liste, de la liste et du tableau de hachage.
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++;
}
}
}
Une implémentation qui réussit les tests du leetcode questiton + suit des tests unitaires simples.
J'ai fait une demande de retrait 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 () implémentation alternative
Je ne suis pas sûr que cela en vaille la peine car il faut le même nombre de lignes, mais voici pour l'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çu dans cet esprit
Des javadocs:
Un constructeur spécial est fourni pour créer une mappe de hachage liée dont l'ordre d'itération est l'ordre dans lequel les entrées ont été consultées en dernier, du plus récent accès au plus récent (ordre d'accès). 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 fusion entraîne un accès à l'entrée correspondante (en supposant qu'elle existe une fois l'appel terminé). Les méthodes de remplacement n'entraînent un 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 mappe spécifiée, dans l'ordre dans lequel les mappages clé-valeur sont fournis par l'itérateur du jeu d'entrées de la mappe spécifiée. Aucune autre méthode ne génère d’accès aux entrées. En particulier, les opérations sur les vues de collection n'affectent pas l'ordre d'itération de la carte de sauvegarde.
La méthode removeEldestEntry (Map.Entry) peut être remplacée pour imposer un stratégie permettant de supprimer automatiquement les mappages périmés lorsque de nouveaux mappages sont ajouté à la carte.
Utiliser une pile et une carte de hachage:
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 commande - true pour l'ordre d'accès, false pour l'ordre d'insertion
Je viens d’écrire 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}
le cache n'est rien d'autre que array arrayList circulaire. Cette liste contient Entry . Quand de nouvelles entrées arrivent, ajoutez à la fin de la liste . Cela signifie le dernier élément utilisé au premier . Supposons que vous réutilisiez un élément, supprimez le lien de la liste et ajouter à la fin.
Pour obtenir n'importe quel élément, nous devons parcourir la liste qui prend O(n) complexité en temps. Afin d'éviter cela, je maintiens HashMap>. Ici k est la clé et IndexNode contiendra un pointeur sur l'entrée dans la liste. afin que nous puissions avoir la complexité temporelle O(1).
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);
}
}
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 dispose d'une fonctionnalité pour supprimer l'entrée la plus ancienne
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 problème est que, par défaut, l'ordre de la liste liée est l'ordre d'insertion, pas l'accès. Cependant, l'un des constructeurs expose une option, utilisez plutôt la commande d'accès.
Voici l'implémentation Java avec Generics
et LinkedHashMap
et la complexité de O(1) sur 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();
}
}
}
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 que cela peut être fait avec la classe LinkedHashMap
. À la compression, vous pouvez obtenir une version minimale en 15 lignes de code.
@Ciro Santilli , pourquoi ne pas simplement supprimer 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, ce qui nous ferions en sorte de renvoyer une sorte de vue définie 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 les documents pour plus d'informations.
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));
}
}