これは就職の面接でよく出てくる質問です。アイデアは、Javaに組み込まれたLinkedHashMapを使用する代わりに、データ構造を定義することです。
LRUキャッシュは、新しいエントリを挿入するために最低使用頻度エントリを削除します。したがって、次のシナリオを考えます:
A - B - C - D - E
Aが最も最近使用されていないアイテムで、Fを挿入する場合、Aを削除する必要があります。
これは、(key、value)によるキャッシュエントリと、要素のキーと使用時間を含む別のリストでHashMapを保持する場合、簡単に実装できます。ただし、潜在的なO(n)時間の複雑さを伴う)を使用して、最も最近使用されていない項目を見つけるためにリストを照会する必要があります。
この構造は、どのようにJavaジェネリックオブジェクトとO(1)操作の場合に実装できますか?
これは、Javaを拡張するのではなく、効率(O(1)ops)とデータ構造自体の実装に焦点を当てているという点で、可能な重複とは異なります。
質問自体から、O(n)操作の問題がリンクリストを照会するときに発生することがわかります。したがって、代替のデータ構造が必要です。検索せずにHashMapからアイテムの最終アクセス時刻。
2つの別個のデータ構造を保持できます。 HashMap with(Key、Pointer) pairとdoubledly linked listは、削除の優先キューとして機能し、値を保存します。 HashMapから、二重にリンクされたリスト内の要素をポイントし、その取得時間を更新できます。 HashMapからリスト内のアイテムに直接移動するため、時間の複雑さはO(1)のままです。
たとえば、二重リンクリストは次のようになります。
least_recently_used -> A <-> B <-> C <-> D <-> E <- most_recently_used
LRUおよびMRUアイテムへのポインターを保持する必要があります。エントリの値はリストに保存され、HashMapをクエリすると、リストへのポインターが取得されます。 get()で、アイテムをリストの右端に配置する必要があります。 put(key、value)で、キャッシュがいっぱいの場合、リストとHashMapの両方からリストの左端のアイテムを削除する必要があります。
以下は、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++;
}
}
}
leetcode questiton+単純な単体テストのテストに合格する実装が続きます。
私はこれでプルリクエストをしました: 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()代替実装
同じ数の行を使用するので、それが価値があるかどうかはわかりませんが、ここでは完了します。
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);
}
}
LinkedHashMap はそれを念頭に置いて設計されています
Javadocsから:
リンクされたハッシュマップを作成するための特別なコンストラクターが提供されます。このマップの反復順序は、エントリが最後にアクセスされた順序で、アクセスが最も遅いものから最も新しいもの(access-order)です。この種類のマップは、LRUキャッシュの構築に適しています。 put、putIfAbsent、get、getOrDefault、compute、computeIfAbsent、computeIfPresent、またはmergeメソッドを呼び出すと、対応するエントリにアクセスできます(呼び出しが完了した後に存在すると仮定します)。置換メソッドは、値が置換された場合にのみエントリにアクセスします。 putAllメソッドは、指定されたマップのエントリセット反復子によってキーと値のマッピングが提供される順序で、指定されたマップの各マッピングに対して1つのエントリアクセスを生成します。他のメソッドはエントリアクセスを生成しません。特に、コレクションビューの操作は、バッキングマップの反復の順序に影響しません。
RemoveEldestEntry(Map.Entry)メソッドをオーバーライドして、新しいマッピングがマップに追加されたときに古いマッピングを自動的に削除するポリシーを課すことができます。
スタックと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());
}
出力:
[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-順序モード-アクセス順序の場合はtrue、挿入順序の場合はfalse
以下にJava Generics
とLinkedHashMap
を使用した実装と、O(1) on get()
および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());
}
}
これはそれをテストするためのコードです:
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();
}
}
}
これはThread
安全ではありませんが、私はこの非常に単純な汎用ソリューションを書いています。
LRUCacheDemo.Java(パブリッククラス)
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}
キャッシュは循環arrayListに他なりません。このリストにはエントリが含まれています。新しいエントリが来るときは、リストの最後に追加します。これは、最初に最も使用頻度の低い要素を意味します。要素を再利用する場合は、リストからリンクを解除して最後に追加するとします。
要素を取得するには、O(n)時間の複雑さを要するリストを走査する必要があります。これを回避するために、HashMap>を維持しています。リスト内のエントリへのポインタ。したがって、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}
linkedHashMapを使用できます。最も古いエントリを削除する機能があります
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;
}
}
唯一の問題は、デフォルトでは、リンクリストの順序はアクセスではなく挿入の順序であるということです。ただし、コンストラクターの1つは、代わりにアクセス順序を使用するオプションを公開します。
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);
}
}
[〜#〜] k [〜#〜] および Others は、これはLinkedHashMap
クラスで実行できると述べています。簡単に言えば、15行のコードで最小バージョンを入手できます。
@ Ciro Santilli 、余分なクラス定義を切り捨ててみませんか?使用されるテストが他のJavaマップのように配置されている場合、実際にreturnマップからのある種のセットビュー。
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)
}
}
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();
}
}
これがユニットテストです
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));
}
}