LRU algorithm design

Raising questions

Description:

The LRU (least recently used) cache structure is designed. The size of the structure is determined during construction, assuming that the size is k, and has the following two functions

  1. set(key, value): insert a record (key, value) into the structure

  2. get(key): returns the value corresponding to the key

Basic analysis

LRU is a very common page replacement algorithm.

The translation of LRU into colloquialism is: when some data has to be eliminated (usually the capacity is full), select the data that has not been used for the longest time to eliminate.

Let's implement a fixed capacity LRUCache. If it is found that the container is full when inserting data, first eliminate a data according to LRU rules, and then insert new data. Both "insert" and "query" are counted as "use" at one time.

It can be understood through a case. Assuming that we have a capacity of [external chain picture transfer failure, the source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-qbimq466-1637490443898)( https://www.nowcoder.com/equation?tex=2&preview=true )]LRUCache and test key value pairs [1-1,2-2,3-3] of are inserted and queried in order:

  • Insert 1-1, and the latest usage data is 1-1 [(1,1)]
  • Insert 2-2, and the latest usage data becomes 2-2 [(2,2), (1,1)]
  • Query 1-1, and the latest used data is 1-1 [(1,1,), (2,2)]
  • Insert 3-3. Since the container has reached its capacity, you need to eliminate the existing data before inserting. At this time, 2-2 and 3-3 will be eliminated and become the latest usage data * * [(3,3), (1,1)]**

In terms of key value pair storage, we can use the "hash table" to ensure that the complexity of insertion and query is [the external chain picture transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-8rbyeb6j-1637490443901)( https://www.nowcoder.com/equation?tex=O (1)]&preview=true).

In addition, we need to maintain an additional "order of use" sequence.

We expect that when "new data is inserted" or "key value pair query occurs", the current key value pair can be placed at the head of the sequence. In this way, when LRU elimination is triggered, we only need to delete the data from the tail of the sequence.

It is expected that if the [external chain image transfer fails, the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-i6ot4sk7-1637490443904)( https://www.nowcoder.com/equation?tex=O (1) ] & preview = true) to adjust the position of a node in the sequence within the complexity, it is natural to think of a two-way linked list.

Bidirectional linked list

Specifically, we use the hash table to store "Key Value pairs". The Key of the Key Value pair is used as the Key of the hash table, while the Value of the hash table uses our own encapsulated Node class, and the Node is also used as the Node of the two-way linked list.

  • Insert: check whether the current key value pair already exists in the hash table:
    • If it exists, update the key value pair and adjust the Node corresponding to the current key value pair to the head of the linked list (addHead operation)
    • If not, check whether the hash table capacity has reached the capacity:
      • Capacity not reached: insert the hash table and adjust the Node corresponding to the current key value pair to the remove part of the chain header (addHead operation)
      • Reached capacity: first find the element to be deleted from the tail of the linked list for deletion (remove operation), then insert the hash table, and adjust the Node corresponding to the current key value pair to the head of the linked list (addHead operation)
  • Query: if the Key is not found in the hash table, it will directly return [external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-s2mbaizx-1637490443908)( https://www.nowcoder.com/equation?tex= -1&preview=true)]; If the Key exists, return the corresponding value and adjust the Node corresponding to the current Key value pair to the linked list header (addHead operation)

Some details:

  • In order to reduce the "empty judgment" operation of the left and right nodes of the two-way linked list, we create two "sentinel" nodes head and tail in advance.
package com.bugchen.niuke.excirse.list;

import java.util.HashMap;
import java.util.Map;

/**
 * The LRU (least recently used) cache structure is designed. The size of the structure is determined during construction, assuming that the size is k, and has the following two functions
 * 1. set(key, value): Insert the record (key, value) into the structure
 * 2. get(key): Returns the value corresponding to the key
 * <p>
 * Tips:
 * 1.Once the set or get operation of a key occurs, it is considered that the record of the key has become the most commonly used, and then the cache will be refreshed.
 * 2.When the size of the cache exceeds k, the least frequently used records are removed.
 * 3.Enter a two-dimensional array and k. each dimension of the two-dimensional array has 2 or 3 numbers. The first number is opt, and the second and third numbers are key and value
 * If opt=1, the next two integers, key, value, represent set(key, value)
 * If opt=2, the next integer key indicates get(key). If the key does not appear or has been removed, it returns - 1
 * For each opt=2, output an answer
 * 4.In order to distinguish the key and value in the cache, the key in the cache described below is wrapped with "" number
 * <p>
 * Requirement: the complexity of set and get operations is O(1)O(1)
 *
 * @Author:BugChen
 * @Description:hashMap+Two way linked list
 * @Date: 2021-11-21
 * @Method:get() put()
 */
public class LRUCache {
    //A two-way linked list is needed to maintain the order of the most recently updated nodes
    //Establish mapping relationship between hashMap and bidirectional linked list
    //The head of the two-way linked list is inserted and the tail is deleted to change the position of the update node in the two-way linked list
    //There are two sentinel nodes in the two-way linked list, that is, the head node and the tail node, so as to reduce empty judgment
    //1. Bidirectional linked list (maintain the order of nodes, mainly the order after update)
    private class Node {
        int key;
        int value;
        Node pre;//Precursor node
        Node next;//Successor node

        Node(int key, int value) {//Initialize bidirectional linked list
            this.key = key;
            this.value = value;
        }
    }

    //2. Maximum capacity of LRUCache
    private int n;

    //3. Two sentinel nodes
    private Node head;
    private Node tail;

    //4. Hash table
    Map<Integer, Node> lruMap;

    //Initialization of LRUCache (lru cache)
    public LRUCache(int capacity) {
        this.n = capacity;
        this.head = new Node(-1, -1);
        this.tail = new Node(-1, -1);
        this.head.next = this.tail;
        this.tail.pre = this.head;
        this.lruMap = new HashMap<>();
    }

    //5. get and put methods of LRUCache
    public int get(int key) {
        Node node = null;
        //There are two cases: there is a corresponding node in lruMap and there is no corresponding node in lruMap
        if (lruMap.containsKey(key)) {
            //If it exists, skip the position of the node in the bidirectional linked list to the first position
            node = lruMap.get(key);
            addHead(node);
            return node.value;
        }
        return -1;
    }

    public void put(int key, int value) {
        Node node = null;
        //put can be divided into three types: normal join, repeated join and full join
        if (lruMap.containsKey(key)) {//It means to join repeatedly, get the current node, and modify the value value
            node = lruMap.get(key);
            node.value = value;
        } else {
            if (lruMap.size() == n) {//Indicates that it is full. At this time, you need to delete the tail node of the two-way linked list
                Node del = tail.pre;
                lruMap.remove(del.key);//The corresponding mapping also needs to be deleted
                remove(del);
            }
            node = new Node(key, value);
            lruMap.put(key, node);
        }
        addHead(node);
    }

    //addHead() method: put the most recently operated node in the first position of the two-way linked list (header insertion)
    private void addHead(Node node) {
        //It is mainly divided into two steps:
        //The first step is to delete the current node from the two-way linked list
        //Then insert the node into the head node
        remove(node);
        //Note: head and tail are always sentinel nodes; head.next is the head node of the useful node
        node.next = head.next;
        node.pre = head;
        head.next.pre = node;
        head.next = node;
    }

    //remove() method: delete the node in the bidirectional linked list
    private void remove(Node node) {
        //delete: removes the current node from the bidirectional linked list
        //Since we create two sentinels head and tail in advance, if node.pre is not empty, it means that the node itself exists in the two-way linked list (not a new node)
        //The nodes to be deleted need to be in the linked list, otherwise they do not need to be deleted
        if (node.pre != null) {//This indicates that the node exists in the bidirectional linked list
            Node pre = node.pre;//The nodes to be operated need to be saved
            pre.next = node.next;
            node.next.pre = pre;
        }
    }
}

Test:

package com.bugchen.niuke.excirse.list;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class LRUSolution {
    /**
     * lru design
     *
     * @param operators int Integer two-dimensional array the ops
     * @param k         int Integer the k
     * @return int Integer one-dimensional array
     */
    public int[] LRU(int[][] operators, int k) {
        List<Integer> list = new ArrayList<>();//Store results
        LRUCache lru = new LRUCache(k);//Initialize LRU cache
        for (int[] op : operators) {//Traversing a two-dimensional array
            int type = op[0];//Gets the operand, 1 for set and 2 for get
            if (type == 1) {
                // set(k,v) operation
                lru.put(op[1], op[2]);
            } else {
                // get(k) operation
                list.add(lru.get(op[1]));
            }
        }
        int n = list.size();
        int[] ans = new int[n];//Return results
        for (int i = 0; i < n; i++) ans[i] = list.get(i);
        return ans;
    }
    public static void main(String[] args) {
        LRUSolution lruSolution = new LRUSolution();
        int[][] operators = new int[][]{{1,1,1},{1,2,2},{1,3,2},{2,1},{1,4,4},{2,2}};
        System.out.println(Arrays.toString(lruSolution.LRU(operators,3)));
    }
}

Tags: Java Programming

Posted on Sun, 21 Nov 2021 17:01:23 -0500 by Dujo