https://leetcode.com/problems/lru-cache/
코드설명
Class(레퍼런스 객체) + 해시맵(HashMap) + LinkedHashMap 을 활용합니다.
LRU Cache는 Least Recently Used Cache를 제거하는 캐싱 정책입니다.
이를 구현할때, 기본적으로 HashMap을 사용해야한다는 점은 O(1) 안에 처리하라는 조건이 있어 알 수 있습니다.
HashMap을 사용하면서, LRU Cache를 구현하기 위해서는 Cache가 사용된 데이터들의 사용빈도를 기반으로 하여 관리하면서, 그 위치를 알아야 하기에 ArrayList를 사용해야 한다고 생각했습니다. 하지만, 이 방식은 순서는 보장하지만, 시간복잡도 O(1)안에 처리하지 못합니다. ( 그안에서 삭제될경우 안의 요소들이 이동되기 때문 )
그렇기에 HashMap<Integer, Node> 를 통해서 처리해야만 시간복잡도 O(1) 안에 처리할 수 있습니다.
이러한 구현을 LinkedHashMap에서 구현하여 자바 라이브러리 API 로 제공하는데, 해당 방식을 사용하여도 됩니다.
특이사항은, LinkedHashMap 객체를 인스턴스화할때, capacity = 용량, 0.75f는 해시테이블이 얼마나 차면 리사이징할지, true는 accessOrder, 즉 들어온 순서대로 처리할지에 대하여 입니다. 이 코드 자체가 LRU를 보장합니다.
LinkedHashMap은 Key가 들어온 순서대로 저장하는데, 이때 true로 설정함으로써 접근순서에 따른 순서로 바꾸어줍니다.
추가로, 제거할시에는 removeEldestEntry 를 Override하여 구현함으로써 초과될시 자동으로 가장 오래전에 사용된 객체를 삭제해줍니다.
this.cache = new LinkedHashMap<>(capacity, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { return size() > capacity; // 용량 초과 시 가장 오래된 항목 제거 } }; }
코드
Node 래퍼런스 클래스를 활용하여 cache HashMap에서 가리키는 위치를 포인터시킵니다.
아래 오답코드를 보면 알 수 있듯이, 가장 문제점은 위치를 갱신시키는 방법에 대한 것 이었습니다.
Node Class를 활용하고, Dummy Class인 Oldest와 Latest를 통해 위치를 갱신시키는 로직이 사라지고 주소값 기반으로 처리할 수 있게 됩니다. 처음에 드는 생각은, put 과정에서 기존의 Node를 그대로 사용하면 더 좋을 것 이라고 생각했지만, Cache의 Key값이 바뀔 수 있으므로 그 과정에서 새로 해당 cache값의 value값을 변경시켜주어야 합니다.
class LRUCache { private int capacity; private Map<Integer, Node> cache; private Node oldest; private Node latest; public LRUCache(int capacity) { this.capacity = capacity; this.cache = new HashMap<>(); this.oldest = new Node(-1, -1); this.latest = new Node(-1, -1); this.oldest.next = this.latest; this.latest.prev = this.oldest; } public int get(int key) { if(cache.containsKey(key)){ Node node = cache.get(key); remove(node); insert(node); return node.val; } return -1; } private void remove(Node node){ Node prev = node.prev; Node next = node.next; prev.next = next; next.prev = prev; } private void insert(Node node){ Node prev = latest.prev; Node next = latest; prev.next = next.prev = node; node.next = next; node.prev = prev; } public void put(int key, int value) { if(cache.containsKey(key)){ remove(cache.get(key)); } Node newNode = new Node(key, value); cache.put(key, newNode); insert(newNode); if(cache.size() > capacity){ Node lru = oldest.next; remove(lru); cache.remove(lru.key); } } } class Node{ int key; int val; Node prev; Node next; public Node(int key, int val){ this.key = key; this.val = val; this.prev = prev; this.next = null; } } /** * Your LRUCache object will be instantiated and called as such: * LRUCache obj = new LRUCache(capacity); * int param_1 = obj.get(key); * obj.put(key,value); */
위의 구현이 제공되는 LinkedHashMap 라이브러리입니다.
import java.util.*; class LRUCache { private final int capacity; private final LinkedHashMap<Integer, Integer> cache; public LRUCache(int capacity) { this.capacity = capacity; this.cache = new LinkedHashMap<>(capacity, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { return size() > capacity; // 용량 초과 시 가장 오래된 항목 제거 } }; } public int get(int key) { return cache.getOrDefault(key, -1); } public void put(int key, int value) { cache.put(key, value); } }
처음에 시도한 오답코드입니다.
틀린 이유는, Cache가 꽉 찬 상태에서 새로운 <key, value>가 들어왔을 때 LRU Cache 정책에 의해 맨 앞의 값을 제거하거나 맨 뒤의 값을 제거해야하는데, 이렇게 유지하기 위해서는 다른 값들의 Index들이 PUT, GET 하는 과정에서 모두 이동시켜줘야 되면서 시간복잡도가 평균 O(1) 이 되도록 할 수 없기 때문입니다.
즉, 코드의 구조자체가 시간복잡도의 조건을 통과하지 못합니다.
class LRUCache { HashMap<Integer, Integer> hashmap; ArrayList<int[]> arr; int capacity = 0; public LRUCache(int capacity) { hashmap = new HashMap<Integer, Integer>(); arr = new ArrayList<int[]>(); this.capacity = capacity; } //가장 최근에 사용된걸 맨뒤로 바꿔주자. public int get(int key) { int arrIdx = hashmap.get(key); return arr.get(hashmap.get(key)); } public void put(int key, int value) { if(capacity <= 0){ //swap first and last int[] lastArr = arr.get(arr.size() - 1); int lastKey = lastArr[0]; int lastValue = lastArr[1]; int[] firstArr = arr.get(0); int firstKey = fisrtArr[0]; int firstValue = firstArr[1]; hashmap.put(lastKey, 0); arr.get(0) = lastArr; hashmap.remove(firstKey); capacity++; } hashmap.put(key, arr.size()); arr.add(new int[] {key, value}); capacity -= 1; } } /** * Your LRUCache object will be instantiated and called as such: * LRUCache obj = new LRUCache(capacity); * int param_1 = obj.get(key); * obj.put(key,value); */