Leetcode 146. LRU Cache

📅2023-07-18🧐65

https://leetcode.com/problems/lru-cache/

Let's think about the Least Recently Used (LRU) cache, to run get() and put() in O(1) time complexity, we must have sequence in our solution. What kind of data structure has sequence?

The answer is Linked-list, And move the node or item in a linked-list just using O(1) time.

In this question, we could use Doubly linked-list, it has ability bidirectionally access it looks like this:

algo-double-linked-list

Since the removal just need to change the next and previous pointer, so we could fast remove an item.

Another data structure will be used in our solution is hashmap, so we could fast query if our node is existing.

In the solution we need to build the Node object(class) first, then add logic of doubly linked-list directly to make our code concise. And don't forget head and tail just special nodes, but they still are Node.

class Node:
    def __init__(self, key=None, val=None):
        self.prev = None
        self.next = None
        self.key = key
        self.val = val

class LRUCache:
    def __init__(self, capacity: int):
        self.cache = {} # to save key and node, so we could access the node by O(1)
        self.size = 0
        self.capacity = capacity

        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _delete_node(self, node):
        _prev = node.prev
        _next = node.next
        _prev.next = _next
        _next.prev = _prev
        
    def _add_to_head(self, node):
        _next = self.head.next
        _prev = _next.prev
        node.next = _next
        node.prev = _prev
        _next.prev = node
        self.head.next = node

    def _move_to_head(self, node):
        self._delete_node(node)
        self._add_to_head(node)

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        
        self._move_to_head(self.cache[key])
        return self.cache[key].val

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.cache[key].val = value
            self._move_to_head(self.cache[key])
            return self.cache[key].val
        
        if self.size >= self.capacity:
            tailkey = self.tail.prev.key
            self._delete_node(self.tail.prev)
            del self.cache[tailkey]
            self.size -= 1

        node = Node(key, value)
        self._add_to_head(node)
        self.cache[key] = node
        self.size += 1

If we use Java, we could implement with builtin LinkedHashMap

class LRUCache {
    int capacity;
    LinkedHashMap<Integer, Integer> cache;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        cache = new LinkedHashMap<>(1, 0.75f, true){
            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> node){
                return size() > capacity;
            }
        };
    }
    
    public int get(int key) {
        return cache.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        cache.put(key, value);
    }
}

In Python we also have builtin OrderedDict to make life easier

from collections import OrderedDict

class LRUCache:

    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache = OrderedDict()
        

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key, True)
        return self.cache[key]
        
    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.cache.move_to_end(key, True)

        self.cache[key] = value
        if len(self.cache) > self.cap:
            self.cache.popitem(False)

In the end let's take a look on the source code of Python's OrderedDict in this link:

https://github.com/python/cpython/blob/aecf6aca515a203a823a87c711f15cbb82097c8b/Lib/collections/__init__.py#L83

LRU cache also can be used to cache overlapping subproblems, so we could use it to solve top-down dynamic programming (DP) problems.

Once we are very familiar with LRU, we can move forward to LFU problem:

https://leetcode.com/problems/lfu-cache/

And please check my solution here if you like:

https://freeyeti.net/leetcode-460-lfu-cache/

LRU is a very useful data structure, for example, when you swipe up on your iPhone screen, you will find LRU. Also the frontend of my blog built by Nextjs, which used LRU to cache server side rendering data.