Hand tearing algorithm -- LRU cache elimination strategy, asked so often

Posted by Topper on Wed, 02 Mar 2022 15:59:16 +0100

As we all know, the size of cache is limited! When the cache is full, it requires a cache elimination strategy to determine which data should be cleaned out.
There are three common strategies: First In, First Out (FIFO), Least Frequently Used (LFU) and Least Recently Used (LRU).

Take a look at this LRU design question on LeetCode:

1.Please design and implement a satisfaction  LRU (Least recently used) Cache constrained data structures.

realization LRUCache Class:

LRUCache(int capacity) Take positive integer as capacity capacity initialization LRU cache
int get(int key) If keyword key If it exists in the cache, the value of the keyword is returned; otherwise, it is returned -1 . 
void put(int key, int value) If keyword key If it already exists, change its data value value ;
If it does not exist, the group is inserted into the cache key-value . If the insert operation causes the number of keywords to exceed capacity ,
The longest unused keyword should be expelled.

Note: function get and put Must be O(1) Average run time complexity.

As can be seen from the title, the average time complexity required is O(1). When I was a sophomore, the data structure teacher said that once keys and values appear, the hash table will bear the brunt! Because it can find keys and values in O(1) time.

For the requirements of access order, the first data structures to be considered are stack, linked list and queue. However, after LRUCache updates the data with get() and put(), it needs to set the data to the latest accessed data, which means that the data needs to be randomly accessed and inserted into the head or tail.

Linked list can quickly move the location of nodes, but to realize random access, we can consider that the time complexity of hash table is O(1).
If the value of the hash table contains the location information of the linked list, you can access the linked list in O(1) time! Because the linked list can record the time sequence of access (the earlier the access is at the end of the linked list), the elements in the linked list must store the key, so as to find the key in the hash table and realize the requirement of deleting the key in the hash table!

1. First of all, when we design the get() method, there are only two cases: getting the value and not getting it!

  • To get the value, in addition to returning the value, in order to meet the principle of least recent use, you should also pay attention to moving the node to the head of the linked list (because the closer it is to the tail, the earlier it is accessed), and delete the original node.
  • If the value cannot be obtained, return - 1.
 public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // If the key exists, first locate it through the hash table and then move it to the head
        moveToHead(node);
        return node.value;
    }

2. When designing the put() method, it is necessary to consider the two situations that the key does not exist in the cache and the key already exists!

  • When the key does not exist in the cache, a new node is created. In order to meet the principle of least recent use, the new node is placed at the head of the linked list. Most importantly, remember to judge whether the current cache size exceeds the maximum capacity! If it exceeds the limit, let's delete one of the tail nodes!
  • When the key already exists in the cache, we only need to modify the value value and move the node to the head of the linked list. Remember to delete the original node.
public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // If the key does not exist, create a new node
            DLinkedNode newNode = new DLinkedNode(key, value);
            // Add to hash table
            cache.put(key, newNode);
            // Add to the header of a two-way linked list
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // If the capacity is exceeded, delete the tail node of the two-way linked list
                DLinkedNode tail = removeTail();
                // Delete the corresponding entry in the hash table
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // If the key exists, first locate it through the hash table, then modify the value and move it to the header
            node.value = value;
            moveToHead(node);
        }
    }
public class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // Using pseudo header and pseudo tail nodes
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    
	private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
 }

Topics: Java Algorithm data structure Cache