LRU Cache Strategy in Detail
Last updated
Last updated
Translator: youyun
Author: labuladong
It is just a cache clean-up strategy.
A computer has limited memory cache. If the cache is full, some contents need to be removed from cache to provide space for new content. However, which part of the cache should be removed? We hope to remove not so useful contents, while leaving useful contents untouched for future usage. So the question is, what are the criteria to determine if the data is useful or not?
LRU (Least Recently Used) cache clean-up algorithm is a common strategy. According to the name, the latest used data should be useful. Hence, when the memory cache is full, we should prioritize removing those data that haven't been used for long are not useful.
For example, an Android phone can run apps in the background. If I opened in sequence: Settings, Phone Manager, and Calendar, their order in the background will be shown as following:
If I switch to Settings now, Settings will be brought to the first:
Assume that my phone only allows me to open 3 apps simultaneously, then the cache is already full by now. If I open another app, Clock, then I have to close another app to free up space for Clock. Which one should be closed?
According to LRU strategy, the lowest app, Phone Manager, should be closed, because it is the longest unused app. Afterwards, the newly opened app will be on the top:
Now you should understand LRU (Least Recently Used) strategy. There are some other strategies available, for example, LFU (Least Frequently Used) strategy, etc. Different strategies can be applied in different use cases. We'll focus on LRU in this article.
LRU algorithm is actually about data structure design: 1. Take a parameter, capacity
, as the maximum size; then 2. Implement two APIs:
put(key, val)
: to store key-value pair
get(key)
: return the value associated with the key; return -1 if the key doesn't exist.
The time complexity for both get
and put
should be O(1).
Let's use an example to understand how the LRU algorithm works.
Through analysis of the above steps, if time complexity for put
and get
are both O(1), we can summarize features of this cache data structure: fast search, fast insertion, fast deletion, and ordered.
Ordered: Obviously, the data has to be ordered to distinguish recently used and longest unused.
Fast Search: We also need to be able to find if a key exists in the cache.
Fast Deletion: If the cache is full, we need to delete the last element.
Fast Insertion: We need to insert the data to the head upon each visit.
Which data structure can fulfill the above requirements? Hash table can search fast, but the data is unordered. Data in linked list is ordered, and can be inserted or deleted fast, but is hard to search. Combining these two, we can come up with a new data structure: hash linked list.
The core data structure of LRU cache algorithm is hash linked list, a combination of doubly linked list and hash table. Here is how the data structure looks:
The idea is simple - using a hash table to provide the ability of fast search to linked list. Think again about the previous example, isn't this data structure the perfect solution for LRU cache data structure?
Some audience may wonder, why doubly linked list? Can't single linked list work? Since key exists in hash table, why do we have to store the key-value pairs in linked list instead of values only?
The answers only afloat when we actually do it. We can only understand the rationale behind the design after we implement the LRU algorithm ourselves. Let's look at the code.
A lot of programming languages have built-in hash linked list, or LRU-alike functions. To help understand the details of the LRU algorithm, let's use Java to reinvent the wheel.
First, define the Node
class of doubly linked list. Assuming both key
and val
are of type int
.
Using our Node
class, implement a doubly linked list with the necessary APIs (the time complexity of these functions are all O(1)):
P.S. This is the typical interface of a doubly linked list. In order to focus on the LRU algorithm, we'll skip the detailed implementation of functions in this class.
Now we can answer the question, why we have to use a doubly linked list. In order to delete a node, we not only need to get the pointer of the node itself, but also need to update the node before and the node after. Only using a doubly linked list, we can guarantee the time complexity is O(1).
With the doubly linked list, we just need to use it in with a hash table in the LRU algorithm. Let's sort out the logic with pseudo code:
If you can understand the logic above, it's easy to translate to code:
This can answer the previous question, why we need to store key-value pair in the linked list, instead of value only. Pay attention to the block of code below:
If the cache is full, we not only need to delete the last node, but also need to delete the key in the map, where we can only get the key through the node. If we only store value in a node, we can't get the key, and hence, can't delete the key from the map.
Till now, you should have understood the idea and implementation of LRU algorithm. One common mistake is to update associated entries in the hash table when you deal with nodes in the linked list.