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);
 */

+ Recent posts