pythondoubly-linked-list

How is the LRU deleted in this LRUCache implementation?


I'm going over the optimal Leetcode solution for LRU Cache LeetCode Medium question. The original problem can be found here: https://leetcode.com/problems/lru-cache/description/

I'm confused by a specific portion of the solution code.

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


class LRUCache:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache = {}  # map key to node

        self.left, self.right = Node(0, 0), Node(0, 0)
        self.left.next, self.right.prev = self.right, self.left

    # remove node from list
    def remove(self, node):
        prev, nxt = node.prev, node.next
        prev.next, nxt.prev = nxt, prev

    # insert node at right
    def insert(self, node):
        prev, nxt = self.right.prev, self.right
        prev.next = nxt.prev = node
        node.next, node.prev = nxt, prev

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

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.remove(self.cache[key])
        self.cache[key] = Node(key, value)
        self.insert(self.cache[key])

        if len(self.cache) > self.cap:
            # remove from the list and delete the LRU from hashmap
            **lru = self.left.next
            self.remove(lru)**
            del self.cache[lru.key]

Why am I deleting the lru? In other words why is self.left.next represent the least recently used key?

To illustrate this let's say your capacity param is 2. And you currently have two nodes comprising your doubly linked list: [1,1] and [2,2]

Now you put a 3rd node [3,3] so the linked list looks like this: [2,2] [1,1] [3,3]. But you have exceeded the capacity constraint of two so you need to remove the LRU (least recently used) node which is [2,2]. For lru = self.left.next ... why does it equal [2,2] and not [1,1]?


Solution

  • code is simple, it's just a double linked list with self.head and self.tail (two pointer)define initially , and node info is stored in dictionary

     Node(0, 0)) <->(DO node insertion delete here) <-> (Node(0,0)
      ^                                                     ^     
      head                                                tail
    

    all deletion and updation happenining in above space between these two node.

    in simple terms:

    for addition -> element is added at tail (as second last element)

    for deletion -> element after head is deleted.

    addition and deletion is same as what happen at double linked list.No need to explain.

    accessing of node is done via dictionary by mapping key to node.

    work flow:

    when you read a node having a key:

    1. if key present -> delete the key from dictionary and node correspoinding to it from LruCache and add that node at tail part(second last)
    2. if key not present -> add the key and node created at last(tail part). check if size of dict exceed the self.capacity or not. if yes then remove the first element (After head) from lrucache double linked list.

    in your example [[2,2], [1,1]]

    here is double linked list in diagram

    start ->

    head (node(0, 0)) <-> tail (node(0,0)) # lru cache created

    [2,2] added put operation

    head (node(0, 0)) <-> Node(2,2) <-> tail (node(0,0))

    [1,1] added put operation

    head (node(0, 0)) <-> Node(2,2) <-> Node(1, 1) <-> tail (node(0,0))

    [3,3] added put operation

    head (node(0, 0)) <-> Node(2,2) <-> Node(1, 1) <-> Node(3,3) <->tail <->(node(0,0))

    here size increase. since Node(2,2) used once and no get operation happen on it , it will be removed and cache become

    head (node(0, 0)) <-> Node(1, 1) <-> Node(3,3) <->tail(node(0,0))

    now suppose [1,3] put operation, having key present happen then this is how list will be changed

    head (node(0, 0)) <-> Node(3,3) <-> Node(1,3) <->tail (node(0,0))

    you can see for each operation if element is accessed ie get used and is found then element is added at right and remove from his present location

    in case of put it is added at right, if capacity increases then first element after head is removed.

    Hope this help