Algorithm-LRU Cache principle and implementation


LRU is Least Recently Used an acronym for least recently used. Its theoretical basis is temporal locality : "Recently used data will still be used for a period of time in the future, and data that has not been used for a long time will most likely not be used for a long time in the future."

topic introduction

146. LRU cache - LeetCode
uses the data structure you have mastered to design and implement an LRU (least recently used) cache mechanism. Implement the LRUCache class:

  • LRUCache(int capacity) Initialize the LRU cache with a positive integer as the capacity capacity
  • int get(int key) Returns the value of the key if the key key exists in the cache, otherwise returns -1.
  • void put(int key, int value) If the key already exists, change its data value; if the key does not exist, insert the set of "key-value". When the cache capacity reaches its upper limit, it should delete the oldest unused data values ​​before writing new data, thus making room for new data values.
    The functions get and put must run with an average time complexity of O(1).

topic analysis

An ideal LRU should be able to O(1)read a piece of data or update a piece of data within 20 minutes, that is, the functions get and put must run with an average time complexity of O(1). It's easy to think of a hash table . According to the key of the data, the reading and writing speed can be easily o(1)achieved within the time complexity. In addition, if only the hash table is used, to determine which piece of data has the earliest access time, it needs to traverse all the tables to find it. Therefore, it is necessary to maintain a sort by access time and access the most recently used data within the time complexityo(1) . element (insert most recently accessed) , last used element (remove least frequently accessed) , delete an element (fast delete node) . Based on this, consider using a hash table + doubly linked list implementation . Its logical structure diagram is as follows:
insert image description here

Here is a brief explanation. Cache represents a hash table (HashMap), which is mapped to its position in the doubly linked list through the key of the cached data. The doubly linked list stores these key-value pairs in the order they are used, the key-value pairs near the head are the most recently used, and the key-value pairs near the tail are the oldest.
Note: In the implementation of the doubly linked list, a dummy head (dummy head) and a dummy tail (dummy tail) are used to mark the boundaries, so that there is no need to check whether adjacent nodes exist when adding nodes and deleting nodes.

Based on this structure, its data structure is implemented as follows:

public class LRUCache {
    
     
	// 双向链表的节点元素  
	class DLinkedNode {
    
     
		int key; // 关键字  
		int value; // 对应的值  
		DLinkedNode prev; // 双向链表的前驱指针  
		DLinkedNode next; // 双向链表的后继指针  
	}  
	
	// 使用java自带的 HashMap 来模拟 Cache表  
	private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();  
	private int size; // 目前缓存中有效数据的个数  
	private int capacity; // LRU缓存容量大小  
	private DLinkedNode head, tail; // 双向链表的头结点和尾结点  
}

Explanation, here is the structure designed for 146. LRU cache - Lituo , if you want to make it more widely used, you can consider using the paradigm. And implement the hash table of Cache by yourself (array + red-black tree: resolve conflicts through the chain address method, and optimize the linked list with red-black tree to o(log(n))find elements in time complexity).

Graphic example

Based on the above data structure, now simulate an LRU cache with a capacity of 4. According to different situations, the analysis is as follows:

For the Cache table is not full and misses after inserting data

At the beginning, add four elements (k1,value1), (k2,value2), (k3,value3), (k4,value4), because these four insertions Cache表are not full, and there is no conflict when calculating the hash value. Therefore, the situation is the same, here is put(k4, value4)an example illustrate: first judge that the valid data in the current cache is not full, then calculate k4the hash value of the key Hash(k4)and find that there is no conflict, so create a doubly linked list node and load the data into (key, value), then find the head pointer of the doubly linked list head, add the element at the head, and Hash(k4)point the corresponding pointer field in the Cache to the element node.

After inserting these four elements, the logical structure corresponding to the data in the structure is as follows:

insert image description here

For the Cache table is full and misses after inserting data

After adding k1, k2, k3, k4four elements, at this time, by put(k5, value5)adding elements, it is found that Cachethe capacity of the table is exceeded, but after calculating the hash value, it is found that there is no conflict, so a doubly linked list node is still created, and the k5data of the node is input, and in the two-way The element node is added to the head of the linked list. Since the capacity is exceeded after adding k5a node, the predecessor node of the tail pointer of the doubly linked list is deleted to ensure that the longest unused element leaves the cache. (There is a point here, that is, for Cachethe processing of the table, since it is a hash table, and the conflict is handled through the chain address method, the elements in the hash table can be deleted. If it is other methods, how to delete the elements in the hash table element). Its processing flow is as follows, where the labels indicate the execution order.

insert image description here

For the case of a miss after inserting data

After adding k1, k2, k3, k4four elements, continue to put(k3, value3)add elements (here does not necessarily need to be Cachefull): find that the calculation Hash(k3)can be found k3in Cachethe table. So Cacheno modification is required for the table. Instead, the corresponding Cachenode in k3the doubly linked list in the table is moved to the head.

insert image description here

Based on the analysis of the above three situations, it can be concluded that :

  • Every time an element is added to the doubly linked list, it is added from the head
  • Every time an element is deleted, it is deleted from the end
  • When deleting, the corresponding key needs to be deleted from the hash table at the same time
  • The element to be accessed again needs to be moved to the head of the linked list

Algorithm implementation

Mainly used to implement three methods:

  • LRUCache(int capacity)capacity: Initialize a buffer with a capacity of .
  • int get(int key): keyReturns the keyword's if the keyword exists in the cache, valueotherwise -1.
  • void put(int key, int value): It will keybe stored in the cache and maintain LRUthe characteristics of .

get operation

For getthe operation, first determine keywhether exists:

  • If keydoes not exist, returns-1
  • If keyexists, then keythe corresponding node is the most recently used node. Locate the position of the node in the doubly linked list through the hash table, move it to the head of the doubly linked list, and finally return the value of the node.
    insert image description here
public int get(int key) {
    
    
	DLinkedNode node = cache.get(key); //从Cache中获取对应双向链表中的结点地址
	
	// key不存在
	if (node == null) {
    
    
		return -1;
	}
	
	// 如果 key 存在,先通过哈希表定位,再移到头部,先删除:
	node.prev.next = node.next;
	node.next.prev = node.prev;
	
	// 再插入到头部  
	node.next = head.next;  
	head.next.prev = node;  
	node.prev = head;  
	head.next = node;
	
	return node.value;
}

put operation

  • If keydoes not exist, use keyand valueto create a new doubly linked list node, and add the node at the head of the doubly linked list, and add keyand the address of the node into the hash table. Then judge whether the number of nodes of the doubly linked list exceeds the capacity, if it exceeds the capacity, delete the tail node of the doubly linked list, and delete the corresponding item in the hash table;
  • If keyexists, it getis similar to the operation, first locate through the hash table, then update the value of the corresponding node to value, and move the node to the head of the doubly linked list.
public void put(int key, int value) {
    
      
	DLinkedNode node = cache.get(key); //从Cache中获取对应双向链表中的结点地址  
	  
	// 如果 key 不存在  
	if (node == null) {
    
      
		// 创建一个新的双向链表节点  
		DLinkedNode newNode = new DLinkedNode(key, value);  
		// 添加进哈希表  
		cache.put(key, newNode);  
		// 添加至双向链表的头部  
		newNode.next = head.next;  
		head.next.prev = newNode;  
		newNode.prev = head;  
		head.next = newNode;  
		
		++size;//容量 + 1  
		
		// 如果超出容量,删除双向链表的尾部节点  
		if (size > capacity) {
    
      
			DLinkedNode temp = tail.prev;  
			// 删除结点  
			temp.prev.next = tail;  
			tail.prev = temp.prev;  
			  
			// 删除哈希表中对应的项  
			cache.remove(temp.key);  
			--size;  
		}  
	} else {
    
      
		// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部  
		node.value = value;  
		  
		node.prev.next = node.next;  
		node.next.prev = node.prev;  
		  
		// 再插入到头部  
		node.next = head.next;  
		head.next.prev = node;  
		node.prev = head;  
		head.next = node;  
	}  
}

rebuild code

According to the official answer, you will find that it encapsulates some simple operations, that is, during the operation of putand get, the processing of the doubly linked list mainly includes 删除结点, 从头结点插入, 从尾结点删除, 移动结点到头结点中:

//删除结点
private void removeNode(DLinkedNode node) {
    
    
	node.prev.next = node.next;
	node.next.prev = node.prev;
}

//从头结点插入
private void addToHead(DLinkedNode node) {
    
    
	node.prev = head;
	node.next = head.next;
	head.next.prev = node;
	head.next = node;
}

//从尾结点删除
private DLinkedNode removeTail() {
    
    
	DLinkedNode res = tail.prev;
	removeNode(res);
	return res;
}

//移动结点到头结点中
private void moveToHead(DLinkedNode node) {
    
    
	removeNode(node);
	addToHead(node);
}

Its operation logic for the doubly linked list is shown in the figure:
delete a node:

Insert from the beginning node:
insert image description here

Delete from tail node:
insert image description here

Move the node to the head node:
insert image description here

HashMap extension

In the above content, it is realized by using the javabuilt-in collection. HashMapTherefore, a certain explanation is given to it: after the hash value is calculated, there is a certain probability that a conflict will occur. Therefore, the conflict can be resolved through the chain address method: for the conflict element, add it to the correspondingly maintained singly linked list. Yes, as shown in the picture:
insert image description here

But this is easy to cause the linked list to be too long and cause its search efficiency to be low. Therefore, the singly linked list can be maintained as a balanced binary tree ( javared-black tree is maintained in it), so that o(lg(n))the element can be found under the condition of time complexity . Right now:

insert image description here

Guess you like

Origin blog.csdn.net/weixin_41012765/article/details/131639122