https://leetcode.com/problems/lru-cache/submissions/1221683044/?envType=study-plan-v2&envId=top-interview-150

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

class LRUCache:

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.dic = {}
        self.head = ListNode(-1, -1)
        self.tail = ListNode(-1, -1)
        self.head.next = self.tail
        self.tail.prev = self.head

    def get(self, key: int) -> int:
        if key not in self.dic:
            return -1
        
        node = self.dic[key]
        # remove the node from it's existing place
        # and add it to the end of the linked list
        self.remove(node)
        self.add(node)
        return node.val

    def put(self, key: int, value: int) -> None:
        # if key already exists
        if key in self.dic:
            # get the old node and remove it from the linked list
            old_node = self.dic[key]
            self.remove(old_node)
        
        # create new node, add the key:key node pairing to dictionary,
        # and add node to end of linked list
        node = ListNode(key, value)
        self.dic[key] = node
        self.add(node)

        # if adding new node exceeds capacity
        if len(self.dic) > self.capacity:
            # get the node at the front of the linked list
            node_to_delete = self.head.next
            # delete it from the list
            self.remove(node_to_delete)
            # delete it from the dictionary
            del self.dic[node_to_delete.key]

    def add(self, node):
        # get the node at the end of the linked list
        previous_end = self.tail.prev
        # insert new node after the previous end node 
        previous_end.next = node
        # set the prev and next of new end node
        node.prev = previous_end
        node.next = self.tail
        # update the prev of the tail to be the new end node
        self.tail.prev = node

    def remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)
struct Node {
    int key;
    int val;
    Node *next;
    Node *prev;
    Node(int key, int val) : key(key), val(val), next(nullptr), prev(nullptr) {}
};

class LRUCache {
public:
    int capacity;
    unordered_map<int, Node*> dic;
    Node *head = new Node(-1, -1);
    Node *tail = new Node(-1, -1);
    LRUCache(int capacity) {
        this->capacity = capacity;
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if (dic.find(key) == dic.end()){
            return -1;
        }

        Node *node = dic[key];
        remove(node);
        add(node);
        return node->val;
    }
    
    void put(int key, int value) {
        if (dic.find(key) != dic.end()){
            Node *oldNode = dic[key];
            remove(oldNode);
        }

        Node *node = new Node(key, value);
        dic[key] = node;
        add(node);

        if (dic.size() > capacity) {
            Node *nodeToDelete = head->next;
            remove(nodeToDelete);
            dic.erase(nodeToDelete->key);
        }
    }

    void add(Node *node) {
        Node *previousEnd = tail->prev;
        previousEnd->next = node;
        node->prev = previousEnd;
        node->next = tail;
        tail->prev = node;
    }

    void remove(Node *node){
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
};

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

The LRU Cache implementation has the following time and space complexities:

Time Complexity:

Space Complexity: O(capacity)

This efficient performance is achieved by using: