Java: Simple understanding and use of the LRU cache mechanism of data structure notes

Simple understanding and use of Java LRU cache mechanism

LRU cache mechanism

1. Topic

Original title link
Use the data structure you have mastered to design and implement one LRU (最近最少使用) 缓存机制. It should support the following operations: 获取数据 getand 写入数据 put.

  • 获取数据 get(key)- If the key (key) exists in the cache, get the value of the key (always a positive number), otherwise return -1.
  • 写入数据 put(key, value)- If the keyword already exists, change its data value; if the keyword does not exist, insert the set of "keyword/value". When the cache capacity reaches its upper limit, it should remove the oldest unused data values ​​before writing new data, thus making room for new data values.
  • Can you O(1) 时间复杂度内do both in ?

Example:

/* 缓存容量为 2 */
LRUCache cache = new LRUCache(2);
// 你可以把 cache 理解成一个队列
// 假设左边是队头,右边是队尾
// 最近使用的排在队头,久未使用的排在队尾
// 圆括号表示键值对 (key, val)

cache.put(1, 1);
// cache = [(1, 1)]
cache.put(2, 2);
// cache = [(2, 2), (1, 1)]
cache.get(1);       // 返回 1
// cache = [(1, 1), (2, 2)]
// 解释:因为最近访问了键 1,所以提前至队头
// 返回键 1 对应的值 1
cache.put(3, 3);
// cache = [(3, 3), (1, 1)]
// 解释:缓存容量已满,需要删除内容空出位置
// 优先删除久未使用的数据,也就是队尾的数据
// 然后把新的数据插入队头
cache.get(2);       // 返回 -1 (未找到)
// cache = [(3, 3), (1, 1)]
// 解释:cache 中不存在键为 2 的数据
cache.put(1, 4);    
// cache = [(1, 4), (3, 3)]
// 解释:键 1 已存在,把原始值 1 覆盖为 4
// 不要忘了也要将键值对提前到队头

2. Ideas

What is the LRU algorithm : It is a cache elimination strategy.

  • The cache capacity of the computer is limited. If the cache is full, some content must be deleted to make room for new content. But the question is, what to delete? We definitely want to delete those useless caches, and keep the useful data in the cache for future use. So, what kind of data do we judge as "useful" data?
  • The LRU cache elimination algorithm is a common strategy. LRU 的全称是 Least Recently Used, that is to say, we think that the data that has been used recently should be "useful", and the data that has not been used for a long time should be useless. When the memory is full, the data that has not been used for a long time should be deleted first.

The LRU algorithm actually lets you design the data structure:

  • First, you need to receive one capacity 参数as the maximum capacity of the cache, and then implement two APIs,
  • One is put(key, val)the method to store key-value pairs,
  • The other is get(key)the method to get the val corresponding to the key, and return -1 if the key does not exist.

Note that both the get and put methods must have a time complexity of O(1)

Analysis:
Hash table lookup is fast, but the data has no fixed order; linked list has order, insertion and deletion are fast, but search is slow. So combine them to form a new data structure: hash linked list.

  • The core data structure of the LRU cache algorithm is 哈希链表,a combination of a doubly linked list and a hash table. This data structure looks like this:

insert image description here

"Why do you have to use a doubly linked list"

  • Because we need delete operation. Deleting a node not only needs to obtain the pointer of the node itself, but also needs to operate the pointer of its predecessor node, and the doubly linked list can support direct search of the predecessor, ensuring the time complexity of the operation O(1)
    .

Since the key is already stored in the hash table, why do we need to store key-value pairs in the linked list? Isn't it enough to just store the value?

  • When the cache capacity is full, we not only need to delete the last Node node, but also delete the key mapped to the node in the map at the same time, and this key can only be obtained by Node. If only val is stored in the Node structure, then we cannot know what the key is, and cannot delete the key in the map, resulting in an error.

3. Solution

1. get() operation: two cases

  • The accessed key does not exist, return -1;
  • The key exists, 删除原有(key, value)the position in the cache, the new position (k, v) 换到队头of , 更新mapthe position corresponding to the key
  public int get(int key) {
    
    
        Node node = cache.get(key);
        if (node == null) {
    
    
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

1. Push (k, v) operation: two cases

  • 1. key 已经存在In the cache, 删除原有(key, value)the position in the cache, the position corresponding to the key in 添加(k,v)the header更新map
  • 2, key 不存在, judging cachewhether is full can be subdivided into two situations
    • The cache is not full, the position corresponding to the key in cache添加(k,v)the header更新map
    • The cache is full, 删除mapthe element at the last position corresponding to the cache in the cache, 删除listthe key-value pair cache添加(k,v)at the end , 更新mapand the position corresponding to the key in the head
public void put(int key, int value) {
    
    
        Node oldNode = cache.get(key);
        if (oldNode == null) {
    
    
            Node newNode = new Node(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;
            if (size > capacity) {
    
    
                Node res = removeTail();
                cache.remove(res.key);
                size--;
            }
        } else {
    
    
            oldNode.value = value;
            moveToHead(oldNode);
        }
    }

Combining the above, we get:

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;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
    
    
        DLinkedNode node = cache.get(key);
        if (node == null) {
    
    
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
    
    
        DLinkedNode node = cache.get(key);
        if (node == null) {
    
    
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
    
    
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
    
    
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }
	//节点添加到头部
    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;
    }
}

Guess you like

Origin blog.csdn.net/JMW1407/article/details/122575999