算法就像搭乐高:带你手撸 LRU 算法
Info
已完成网站教程、网站习题、配套插件中所有多语言代码的校准,解决了之前 chatGPT 翻译可能出错的问题~
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
LeetCode | Difficulty |
---|---|
146. LRU Cache | 🟠 |
Prerequisites
Before reading this article, you need to learn:
The LRU algorithm is a cache eviction strategy. While its principle is not difficult, writing bug-free code in an interview requires some技巧. It involves layering and breaking down data structures. This article will guide you to write elegant code.
Computer caches have limited capacity. When the cache is full, some content must be deleted to make room for new data. But which content should be deleted? We want to remove less useful cache data and keep useful data for future use. So, what data do we consider "useful"?
The LRU cache eviction algorithm is a common strategy. LRU stands for Least Recently Used, meaning we consider recently used data as "useful" and data that hasn't been used for a long time as "useless." When memory is full, we prioritize deleting the least recently used data.
For a simple example, Android phones can run apps in the background. Suppose I open "Settings," "Phone Manager," and "Calendar" in sequence. Their order in the background would be:
But if I access the "Settings" screen, "Settings" will move to the front, like this:
Assume my phone only allows three apps to run simultaneously, and it's already full. If I open a new app, "Clock," I must close one app to make room. Which one should I close?
According to the LRU strategy, I would close "Phone Manager" at the bottom because it's the least recently used, and then place the new app at the top:
Now you should understand the LRU (Least Recently Used) strategy. There are other cache eviction strategies, such as evicting based on access frequency (LFU strategy) rather than access order, each with its own use cases. This article explains the LRU algorithm strategy. I will cover the LFU algorithm in LFU Algorithm Explained.
I. LRU Algorithm Description
LeetCode problem 146, "LRU Cache," asks you to design a data structure:
First, you need to accept a capacity
parameter as the maximum size of the cache. Then, implement two APIs: the put(key, val)
method to store key-value pairs, and the get(key)
method to retrieve the val
corresponding to key
. If the key
does not exist, return -1.
Note that both the get
and put
methods must have an O(1)
time complexity. Let's look at a specific example to see how the LRU algorithm works.
// the cache capacity is 2
LRUCache cache = new LRUCache(2);
// you can understand the cache as a queue
// assume the left side is the head of the queue and the right side is the tail
// the most recently used is at the head of the queue, and the least recently used is at the tail
// parentheses represent the key-value pair (key, val)
cache.put(1, 1);
// cache = [(1, 1)]
cache.put(2, 2);
// cache = [(2, 2), (1, 1)]
// return 1
cache.get(1);
// cache = [(1, 1), (2, 2)]
// explanation: because key 1 was recently accessed, it is moved to the head of the queue
// return the value corresponding to key 1, which is 1
cache.put(3, 3);
// cache = [(3, 3), (1, 1)]
// explanation: the cache is full, need to delete content to make space
// prefer to delete the least recently used data, which is the data at the tail
// then insert the new data at the head of the queue
// return -1 (not found)
cache.get(2);
// cache = [(3, 3), (1, 1)]
// explanation: there is no data with key 2 in the cache
cache.put(1, 4);
// cache = [(1, 4), (3, 3)]
// explanation: key 1 already exists, overwrite the original value 1 with 4
// don't forget to also move the key-value pair to the head of the queue
// the cache capacity is 2
LRUCache* cache = new LRUCache(2);
// you can understand cache as a queue
// assume the left is the head of the queue and the right is the tail
// the most recently used is at the head, and the least recently used is at the tail
// parentheses represent the key-value pair (key, val)
cache->put(1, 1);
// cache = [(1, 1)]
cache->put(2, 2);
// cache = [(2, 2), (1, 1)]
// return 1
cache->get(1);
// cache = [(1, 1), (2, 2)]
// explanation: because key 1 was recently accessed, it is moved to the head
// return the value corresponding to key 1
cache->put(3, 3);
// cache = [(3, 3), (1, 1)]
// explanation: the cache is full, need to delete content to make space
// preferentially delete the least recently used data, which is the data at the tail
// then insert the new data at the head
// return -1 (not found)
cache->get(2);
// cache = [(3, 3), (1, 1)]
// explanation: there is no data with key 2 in the cache
cache->put(1, 4);
// cache = [(1, 4), (3, 3)]
// explanation: key 1 already exists, overwrite the original value 1 with 4
// don't forget to also move the key-value pair to the head
# the cache capacity is 2
cache = LRUCache(2)
# you can understand cache as a queue
# assume the left is the head of the queue and the right is the tail
# the most recently used is at the head of the queue, and the least recently used is at the tail
# parentheses represent key-value pairs (key, val)
cache.put(1, 1)
# cache = [(1, 1)]
cache.put(2, 2)
# cache = [(2, 2), (1, 1)]
# return 1
cache.get(1)
# explanation: because key 1 was recently accessed, it is moved to the head of the queue
# return the value corresponding to key 1
# cache = [(1, 1), (2, 2)]
cache.put(3, 3)
# cache = [(3, 3), (1, 1)]
# explanation: the cache is full, need to delete content to make space
# priority is given to deleting the least recently used data, which is the data at the tail of the queue
# then insert the new data at the head of the queue
# return -1 (not found)
cache.get(2)
# explanation: there is no data with key 2 in the cache
# cache = [(3, 3), (1, 1)]
cache.put(1, 4)
# cache = [(1, 4), (3, 3)]
# explanation: key 1 already exists, overwrite the original value 1 with 4
# don't forget to also move the key-value pair to the head of the queue
// Create an LRU Cache with a capacity of 2
cache := Constructor(2)
// You can think of the cache as a queue
// Assuming the left is the head of the queue and the right is the tail
// The most recently used is at the head of the queue, and the least recently used is at the tail
// Parentheses represent key-value pairs (key, val)
cache.Put(1, 1)
// cache = [(1, 1)]
cache.Put(2, 2)
// cache = [(2, 2), (1, 1)]
// Return 1
cache.Get(1)
// cache = [(1, 1), (2, 2)]
// Explanation: Because key 1 was recently accessed, it is moved to the head of the queue
// Return the value corresponding to key 1
cache.Put(3, 3)
// cache = [(3, 3), (1, 1)]
// Explanation: The cache capacity is full, need to remove the least recently used element to make room
// Prioritize deleting the least recently used data, which is the data at the tail of the queue
// Then insert the new data at the head of the queue
// Return -1 (not found)
cache.Get(2)
// cache = [(3, 3), (1, 1)]
// Explanation: The cache does not contain the data with key 2
cache.Put(1, 4)
// cache = [(1, 4), (3, 3)]
// Explanation: Key 1 already exists, overwrite the original value 1 with 4
// Don't forget to also move the key-value pair to the head of the queue
// the cache capacity is 2
var cache = new LRUCache(2);
// you can understand the cache as a queue
// assuming the left is the head of the queue and the right is the tail
// the most recently used is at the head of the queue, and the least recently used is at the tail
// parentheses represent key-value pairs (key, val)
cache.put(1, 1);
// cache = [(1, 1)]
cache.put(2, 2);
// cache = [(2, 2), (1, 1)]
// return 1
console.log(cache.get(1));
// cache = [(1, 1), (2, 2)]
// explanation: because key 1 was recently accessed, it is moved to the head of the queue
// return the value corresponding to key 1
cache.put(3, 3);
// cache = [(3, 3), (1, 1)]
// explanation: the cache is full, need to remove content to make space
// prioritize removing the least recently used data, which is the data at the tail of the queue
// then insert the new data at the head of the queue
// return -1 (not found)
console.log(cache.get(2));
// cache = [(3, 3), (1, 1)]
// explanation: there is no data with key 2 in the cache
cache.put(1, 4);
// cache = [(1, 4), (3, 3)]
// explanation: key 1 already exists, overwrite the original value 1 with 4
// also, don't forget to move the key-value pair to the head of the queue
II. LRU Algorithm Design
Analyzing the above operation process, to ensure that the put
and get
methods have a time complexity of O(1), we can summarize the necessary conditions for the cache
data structure:
Obviously, the elements in the
cache
must have an order to distinguish between recently used and least recently used data. When the capacity is full, the least recently used element must be removed to make space.We need to quickly find if a
key
already exists in thecache
and get the correspondingval
.Each time a
key
in thecache
is accessed, this element needs to be marked as the most recently used. This means thecache
must support quick insertion and deletion of elements at any position.
So, what data structure meets all these conditions? A hash table has fast lookup but no fixed order; a linked list has order and fast insertion/deletion but slow lookup. Combining these, we form a new data structure: a hash-linked list, LinkedHashMap
.
The core data structure of the LRU cache algorithm is the hash-linked list, a combination of a doubly linked list and a hash table. This data structure looks like this:
With this structure, let's analyze the three conditions mentioned above one by one:
If we always add elements to the end of the list by default, then obviously, the elements closer to the tail are the most recently used, and those closer to the head are the least recently used.
For a specific
key
, we can quickly locate the corresponding node in the list through the hash table, thus obtaining the correspondingval
.Linked lists inherently support quick insertion and deletion at any position by simply adjusting pointers. However, traditional linked lists cannot quickly access an element at a specific index. Here, with the help of the hash table, we can quickly map a
key
to any node in the linked list and then perform insertion or deletion.
Readers might wonder, why do we need a doubly linked list? Wouldn't a singly linked list suffice? Also, since the hash table already stores the key
, why do we need to store both key
and val
in the linked list? Wouldn't storing just val
be enough?
Questions arise in thought, but answers come through action. The reasons for this design will only become clear once we implement the LRU algorithm ourselves. So, let's dive into the code~
III. Code Implementation
Many programming languages have built-in hash-linked lists or library functions similar to LRU functionality. However, to help everyone understand the details of the algorithm, we will first implement the LRU algorithm from scratch and then use Java's built-in LinkedHashMap
for another implementation.
First, let's define the node class for the doubly linked list. For simplicity, we'll consider both key
and val
as int types:
class Node {
public int key, val;
public Node next, prev;
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
class Node {
public:
int key, val;
Node* next;
Node* prev;
Node(int k, int v){
this->key = k;
this->val = v;
}
};
class Node:
def __init__(self, k: int, v: int):
self.key = k
self.val = v
self.next = None
self.prev = None
type Node struct {
key, val int
next, prev *Node
}
func NewNode(k, v int) *Node {
return &Node{key: k, val: v}
}
var Node = function(k, v) {
this.key = k;
this.val = v;
this.next = null;
this.prev = null;
};
Next, we use our Node
type to build a doubly linked list and implement several essential APIs for the LRU algorithm:
class DoubleList {
// virtual head and tail nodes
private Node head, tail;
// number of elements in the linked list
private int size;
public DoubleList() {
// initialize the data of the doubly linked list
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
// add node x to the end of the list, time O(1)
public void addLast(Node x) {
x.prev = tail.prev;
x.next = tail;
tail.prev.next = x;
tail.prev = x;
size++;
}
// remove node x from the list (x is guaranteed to exist)
// since it's a doubly linked list and the target Node is given, time O(1)
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// remove the first node from the list and return it, time O(1)
public Node removeFirst() {
if (head.next == tail)
return null;
Node first = head.next;
remove(first);
return first;
}
// return the length of the list, time O(1)
public int size() { return size; }
}
class DoubleList {
private:
// virtual head and tail nodes
Node* head;
Node* tail;
// number of elements in the list
int size;
public:
DoubleList() {
// initialize the data of the doubly linked list
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->prev = head;
size = 0;
}
// add node x to the end of the list, time complexity O(1)
void addLast(Node* x) {
x->prev = tail->prev;
x->next = tail;
tail->prev->next = x;
tail->prev = x;
size++;
}
// remove node x from the list (x is guaranteed to exist)
// since it is a doubly linked list and the target Node is given, time complexity O(1)
void remove(Node* x) {
x->prev->next = x->next;
x->next->prev = x->prev;
size--;
}
// remove the first node of the list and return it, time complexity O(1)
Node* removeFirst() {
if (head->next == tail)
return nullptr;
Node* first = head->next;
remove(first);
return first;
}
// return the length of the list, time complexity O(1)
int getSize() { return size; }
};
class DoubleList:
# virtual head and tail nodes
def __init__(self):
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
# add node x to the end of the list, time complexity O(1)
def addLast(self, x: Node):
x.prev = self.tail.prev
x.next = self.tail
self.tail.prev.next = x
self.tail.prev = x
self.size += 1
# remove node x from the list (x is guaranteed to exist)
# since it's a doubly linked list and the target Node is given, time complexity O(1)
def remove(self, x: Node):
x.prev.next = x.next
x.next.prev = x.prev
self.size -= 1
# remove the first node of the list and return it, time complexity O(1)
def removeFirst(self) -> Node:
if self.head.next == self.tail:
return None
first = self.head.next
self.remove(first)
return first
# return the size of the list, time complexity O(1)
def size(self) -> int:
return self.size
type DoubleList struct {
head, tail *Node
size int
}
func NewDoubleList() *DoubleList {
// initialize the data of the doubly linked list
head := &Node{key: 0, val: 0}
tail := &Node{key: 0, val: 0}
head.next, tail.prev = tail, head
return &DoubleList{head: head, tail: tail, size: 0}
}
// add node x to the end of the list, time O(1)
func (this *DoubleList) AddLast(x *Node) {
x.prev = this.tail.prev
x.next = this.tail
this.tail.prev.next = x
this.tail.prev = x
this.size++
}
// remove node x from the list (x is guaranteed to exist)
// since it is a doubly linked list and the target Node is given, time O(1)
func (this *DoubleList) Remove(x *Node) {
x.prev.next = x.next
x.next.prev = x.prev
this.size--
}
// remove the first node of the list and return it, time O(1)
func (this *DoubleList) RemoveFirst() *Node {
if this.head.next == this.tail {
return nil
}
first := this.head.next
this.Remove(first)
return first
}
// return the length of the list, time O(1)
func (this *DoubleList) Size() int {
return this.size
}
var DoubleList = function() {
// virtual head and tail nodes
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
// number of elements in the list
this.size = 0;
// initialize the data of the doubly linked list
this.head.next = this.tail;
this.tail.prev = this.head;
};
// add node x to the end of the list, time complexity O(1)
DoubleList.prototype.addLast = function(x) {
x.prev = this.tail.prev;
x.next = this.tail;
this.tail.prev.next = x;
this.tail.prev = x;
this.size++;
};
// remove node x from the list (x is guaranteed to exist)
// since it's a doubly linked list and the target Node is given, time complexity O(1)
DoubleList.prototype.remove = function(x) {
x.prev.next = x.next;
x.next.prev = x.prev;
this.size--;
};
// remove the first node of the list and return it, time complexity O(1)
DoubleList.prototype.removeFirst = function() {
if (this.head.next == this.tail)
return null;
var first = this.head.next;
this.remove(first);
return first;
};
// return the length of the list, time complexity O(1)
DoubleList.prototype.size = function() {
return this.size;
};
If you are not familiar with linked list operations, you can refer to the previous article Step-by-Step Guide to Implementing a Doubly Linked List.
Now we can answer the earlier question, "Why must we use a doubly linked list?" It's because we need deletion operations. To delete a node, we not only need a pointer to the node itself but also need to manipulate the pointer of its predecessor node. Only a doubly linked list allows direct access to the predecessor, ensuring the operation has a time complexity of O(1).
重要
Note that the doubly linked list API we implemented only allows insertion from the tail. This means the data near the tail is the most recently used, and the data near the head is the least recently used.
With the implementation of the doubly linked list, we just need to combine it with a hash table in the LRU algorithm. Let's start by setting up the code framework:
class LRUCache {
// key -> Node(key, val)
private HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)...
private DoubleList cache;
// maximum capacity
private int cap;
public LRUCache(int capacity) {
this.cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
}
class LRUCache {
private:
// key -> Node(key, val)
unordered_map<int, Node*> map;
// Node(k1, v1) <-> Node(k2, v2)...
DoubleList cache;
// maximum capacity
int cap;
public:
LRUCache(int capacity) {
this->cap = capacity;
}
};
class LRUCache:
def __init__(self, capacity: int):
# key -> Node(key, val)
self.map = {}
# Node(k1, v1) <-> Node(k2, v2)...
self.cache = DoubleList()
# maximum capacity
self.cap = capacity
type LRUCache struct {
// key -> Node(key, val)
_map map[int]*Node
// Node(k1, v1) <-> Node(k2, v2)...
cache *DoubleList
// maximum capacity
cap int
}
func Constructor(capacity int) LRUCache {
return LRUCache{
map_: make(map[int]*Node),
cache: NewDoubleList(),
cap: capacity,
}
}
var LRUCache = function(capacity) {
// key -> Node(key, val)
this.map = new Map();
// Node(k1, v1) <-> Node(k2, v2)...
this.cache = new DoubleList();
// maximum capacity
this.cap = capacity;
};
Let's not rush into implementing the get
and put
methods for the LRU algorithm. Since we need to maintain both a doubly linked list cache
and a hash table map
, it's easy to miss some operations. For example, when deleting a key
, you might remove the corresponding Node
in cache
but forget to delete the key
in map
.
An effective way to solve this problem is to provide an abstract API layer on top of these two data structures.
This might sound a bit mystical, but it's actually quite simple. The goal is to make the main LRU methods get
and put
avoid directly handling the details of map
and cache
. We can start by implementing the following functions:
class LRUCache {
// To save space, the previous code part is omitted...
// promote a key to the most recently used
private void makeRecently(int key) {
Node x = map.get(key);
// first remove this node from the linked list
cache.remove(x);
// reinsert it at the end of the queue
cache.addLast(x);
}
// add the most recently used element
private void addRecently(int key, int val) {
Node x = new Node(key, val);
// the tail of the linked list is the most recently used element
cache.addLast(x);
// don't forget to add the key mapping in the map
map.put(key, x);
}
// delete a certain key
private void deleteKey(int key) {
Node x = map.get(key);
// remove from the linked list
cache.remove(x);
// delete from the map
map.remove(key);
}
// remove the least recently used element
private void removeLeastRecently() {
// the first element in the linked list is the least recently used
Node deletedNode = cache.removeFirst();
// also, don't forget to remove its key from the map
int deletedKey = deletedNode.key;
map.remove(deletedKey);
}
}
class LRUCache {
private:
// For the sake of brevity, the previous given code part is omitted...
// promote a certain key to be recently used
void makeRecently(int key) {
Node* x = map[key];
// first remove this node from the list
cache.remove(x);
// reinsert it at the end of the queue
cache.addLast(x);
}
// add the recently used element
void addRecently(int key, int val) {
Node* x = new Node(key, val);
// the tail of the list is the most recently used element
cache.addLast(x);
// don't forget to add the mapping of the key in the map
map[key] = x;
}
// delete a certain key
void deleteKey(int key) {
Node* x = map[key];
// remove it from the list
cache.remove(x);
// delete from the map
map.erase(key);
}
// remove the least recently used element
void removeLeastRecently() {
// the first element at the head of the list is the least recently used
Node* deletedNode = cache.removeFirst();
// also, don't forget to delete its key from the map
int deletedKey = deletedNode->key;
map.erase(deletedKey);
}
};
class LRUCache:
# For the sake of brevity, the previous part of the code is omitted...
# Promote a certain key to recently used
def makeRecently(self, key: int):
x = self.map[key]
# First, remove this node from the linked list
self.cache.remove(x)
# Reinsert it at the end of the queue
self.cache.addLast(x)
# Add the most recently used element
def addRecently(self, key: int, val: int):
x = Node(key, val)
# The tail of the linked list is the most recently used element
self.cache.addLast(x)
# Don't forget to add the key mapping in the map
self.map[key] = x
# Delete a certain key
def deleteKey(self, key: int):
x = self.map[key]
# Remove it from the linked list
self.cache.remove(x)
# Delete it from the map
self.map.pop(key)
# Remove the least recently used element
def removeLeastRecently(self):
# The first element at the head of the linked list is the least recently used
deletedNode = self.cache.removeFirst()
# Also, don't forget to delete its key from the map
deletedKey = deletedNode.key
self.map.pop(deletedKey)
// For the sake of brevity, the previous code is omitted...
// Promote a certain key to the most recently used
func (this *LRUCache) makeRecently(key int) {
x := this._map[key]
// First, remove this node from the list
this.cache.Remove(x)
// Re-insert it at the end of the queue
this.cache.AddLast(x)
}
// Add the most recently used element
func (this *LRUCache) addRecently(key, val int) {
x := NewNode(key, val)
// The tail of the list is the most recently used element
this.cache.AddLast(x)
// Don't forget to add the mapping of the key in the map
this._map[key] = x
}
// Delete a certain key
func (this *LRUCache) deleteKey(key int) {
x := this._map[key]
// Remove it from the list
this.cache.Remove(x)
// Delete from the map
delete(this._map, key)
}
// Remove the least recently used element
func (this *LRUCache) removeLeastRecently() {
// The first element in the list is the least recently used
deletedNode := this.cache.RemoveFirst()
// Also, don't forget to delete its key from the map
deletedKey := deletedNode.key
delete(this._map, deletedKey)
}
// For the sake of brevity, the previous code section is omitted...
// Promote a certain key to the most recently used
LRUCache.prototype.makeRecently = function(key) {
var x = this.map.get(key);
// First, remove this node from the linked list
this.cache.remove(x);
// Re-insert it at the end of the queue
this.cache.addLast(x);
};
// Add the most recently used element
LRUCache.prototype.addRecently = function(key, val) {
var x = new Node(key, val);
// The tail of the linked list is the most recently used element
this.cache.addLast(x);
// Don't forget to add the key mapping in the map
this.map.set(key, x);
};
// Delete a certain key
LRUCache.prototype.deleteKey = function(key) {
var x = this.map.get(key);
// Remove from the linked list
this.cache.remove(x);
// Delete from the map
this.map.delete(key);
};
// Remove the least recently used element
LRUCache.prototype.removeLeastRecently = function() {
// The first element of the linked list is the least recently used
var deletedNode = this.cache.removeFirst();
// Also, don't forget to delete its key from the map
var deletedKey = deletedNode.key;
this.map.delete(deletedKey);
};
Here we can answer the previous question: "Why do we need to store both key
and val
in a linked list instead of just val
?" Note in the removeLeastRecently
function, we need to use deletedNode
to get deletedKey
.
In other words, when the cache capacity is full, we not only need to delete the last Node
in the list, but also need to remove the corresponding key
from the map
. This key
can only be obtained from the Node
. If the Node
structure only stores val
, we would not know what the key
is, and we would not be able to remove the key from the map
, leading to errors.
The above method is a simple operation encapsulation. Calling these functions avoids direct manipulation of the cache
linked list and map
hash table. Next, I will implement the get
method for the LRU algorithm:
class LRUCache {
// To save space, the previous code is omitted...
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
// promote the data to the most recently used
makeRecently(key);
return map.get(key).val;
}
}
class LRUCache {
private:
// For brevity, the previous given code is omitted...
public:
int get(int key) {
if (!map.count(key)) {
return -1;
}
// promote the data to the most recently used
makeRecently(key);
return map[key]->val;
}
};
class LRUCache:
# To save space, the previous code part is omitted...
def get(self, key: int) -> int:
if key not in self.map:
return -1
# promote this data to most recently used
self.makeRecently(key)
return self.map[key].val
// For the sake of brevity, the previous code is omitted...
func (this *LRUCache) Get(key int) int {
if _, ok := this._map[key]; !ok {
return -1
}
// Promote the data to most recently used
this.makeRecently(key)
return this._map[key].val
}
// To save space, the previous code is omitted...
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) {
return -1;
}
// promote the data to most recently used
this.makeRecently(key);
return this.map.get(key).val;
};
The put
method is a bit more complex, so let's start by drawing a diagram to clarify its logic:
With this, we can easily write the code for the put
method:
class LRUCache {
// To save space, the previous given code part is omitted...
public void put(int key, int val) {
if (map.containsKey(key)) {
// delete the old data
deleteKey(key);
// the newly inserted data is the most recently used data
addRecently(key, val);
return;
}
if (cap == cache.size()) {
// remove the least recently used element
removeLeastRecently();
}
// add as the most recently used element
addRecently(key, val);
}
}
class LRUCache {
private:
// To save space, the previous code is omitted...
public:
void put(int key, int val) {
if (map.count(key)) {
// delete the old data
deleteKey(key);
// the newly inserted data is the most recently used data
addRecently(key, val);
return;
}
if (cap == cache.getSize()) {
// delete the least recently used element
removeLeastRecently();
}
// add as the most recently used element
addRecently(key, val);
}
};
class LRUCache:
# To save space, the previous code part is omitted...
def put(self, key: int, val: int) -> None:
if key in self.map:
# delete the old data
self.deleteKey(key)
# the newly inserted data is the most recently used data
self.addRecently(key, val)
return
if self.cap == self.cache.size():
# delete the least recently used element
self.removeLeastRecently()
# add as the most recently used element
self.addRecently(key, val)
// To save space, the previous code part is omitted...
func (this *LRUCache) Put(key, val int) {
if _, ok := this._map[key]; ok {
// delete the old data
this.deleteKey(key)
// the newly inserted data is the most recently used data
this.addRecently(key, val)
return
}
if this.cap == this.cache.Size() {
// delete the least recently used element
this.removeLeastRecently()
}
// add as the most recently used element
this.addRecently(key, val)
}
// To save space, the previous code part is omitted...
LRUCache.prototype.put = function(key, val) {
if (this.map.has(key)) {
// delete the old data
this.deleteKey(key);
// the newly inserted data is the most recently used data
this.addRecently(key, val);
return;
}
if (this.cap === this.cache.size()) {
// delete the least recently used element
this.removeLeastRecently();
}
// add as the most recently used element
this.addRecently(key, val);
};
至此,你应该已经完全掌握 LRU 算法的原理和实现了。看下完整的实现:
我们最后用 Java 的内置类型 LinkedHashMap
来实现 LRU 算法,逻辑和之前完全一致:
class LRUCache {
int cap;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
// make the key recently used
makeRecently(key);
return cache.get(key);
}
public void put(int key, int val) {
if (cache.containsKey(key)) {
// modify the value of the key
cache.put(key, val);
// make the key recently used
makeRecently(key);
return;
}
if (cache.size() >= this.cap) {
// the head of the list is the least recently used key
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// add the new key to the end of the list
cache.put(key, val);
}
private void makeRecently(int key) {
int val = cache.get(key);
// remove the key and re-insert it at the end
cache.remove(key);
cache.put(key, val);
}
}
For other languages, you can search to see if there is a data structure like LinkedHashMap
. Generally, in interviews, you don't need to write a doubly linked list from scratch; you can directly use built-in data structures to implement the LRU algorithm.
With this, the LRU algorithm is no longer mysterious. For more problems related to data structure design, see Classic Exercises on Data Structure Design.