Implement an LFU Cache
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Introduction
In this tutorial, we'll discuss implementing an LFU (Least Frequently Used) cache. Here's what we'll cover:
What is an LFU Cache?
A brief overview of how an LFU cache works and why it's useful.
How is it Different from LRU Cache?
We'll explain the difference between LFU and LRU caching strategies.
Required Operations
The key operations we need to implement for an LFU cache.
Implementation #1: HashMap
A first pass implementation using a HashMap.
Implementation #2: Singly Linked List
An improved version using a linked list for tracking order.
Implementation #3: Doubly Linked List
Final implementation with a doubly linked list structure.
Alright, let's get started!
LFU Cache Overview
LFU Cache
Least Frequently Used or LFU cache is a cache algorithm that manages the replacement of blocks in a cache. The algorithm keeps track of the number of times a block of memory is referenced, and replaces the block with the least frequency once the cache is full.
How LFU Cache is different from LRU Cache?
How LFU Cache is Different From LRU Cache?
The primary difference between Least Frequently and Least Recently Used cache is that the former evicts the memory block with the lowest frequency, even if the block is just recently accessed. Whereas the latter removes the memory block that has not been referenced for the longest time.
Also, check out, Design an LRU Cache.
Design an LFU Cache
Design an LFU Cache
The problem statement is to design a data structure for an LFU cache. Our LFUCache class should perform the following operations:
LFUCache(int n)
The constructor initializes the cache object with the required capacity.
int get(int key)
This method gets the value of the key if the key exists in the cache. If not, it returns -1.
void put(int key, int value)
This method does the following:
- If the key is present, it updates the value of the key.
- If the key is not present, it inserts the key along with the value.
- If the cache becomes full, it removes the least frequently used key, and then inserts a new element.
- If the cache becomes full, and there's a tie between the least frequently used key, then the least recently used key would be invalidated.
Multiple Choice
LFU cache evicts the memory block that has been least recently accessed.
Using HashMap
Implement an LFU Cache Using HashMap
Before moving on to designing the cache using HashMap, let's look at a straightforward approach to implementing an LFU Cache:
- First, initialize an array with the required capacity.
- Each element in an array should contain fields to store key, value, frequency, and timestamp.
- In order to get an element with a particular key, traverse the array, if the element exists, return it, otherwise return -1. The worst-case complexity, in this case, is O(n).
- To insert an element into the array, there can be two possibilities.
- If the array is not full, and the key doesn't exists already, insert the element with frequency 1 and timestamp 0. Update the timestamp of the remaining elements in the array.
- If the array is full, traverse the entire array to find the element with the least frequency to remove it. If there's a tie, then remove the element with the highest timestamp.
- Now insert the new element.
Now, let's discuss a better approach using HashMap.
Here, we'll use maps. One will be to store key-value pairs, and the other to store the count of the times a block is referenced from the cache. In other words, a value is accessed from the key-value map:
java
private Map<Integer, Integer> valueMap = new HashMap<>();
private Map<Integer, Integer> countMap = new HashMap<>();
javascript
let valueMap = new Map();
let countMap = new Map();
python
valueMap = dict()
countMap = dict()
cpp
std::map<int, int> valueMap;
std::map<int, int> countMap;
csharp
private Dictionary<int, int> valueMap = new Dictionary<int, int>();
private Dictionary<int, int> countMap = new Dictionary<int, int>();
go
valueMap := make(map[int]int)
countMap := make(map[int]int)
LFU cache constructor will look like the following:
```java
public LFUCache(int n) {
size = n;
}
javascript
LFUCache = function(n) {
this.size = n;
}
python
def init(self, n):
self.size = n
cpp
LFUCache::LFUCache(int n) {
size = n;
}
csharp
public LFUCache(int n) {
size = n;
}
go
type LFUCache struct {
size int
}
func Constructor(n int) LFUCache {
return LFUCache{n}
}
To get an element from the valueMap, the implementation is straightforward:
java
public int get(int key) {
if (!valueMap.containsKey(key) || size==0) {
return -1;
}
countMap.put(key, countMap.get(key) + 1);
return valueMap.get(key);
}
javascript
get(key) {
if (!this.valueMap.has(key) || this.size == 0) {
return -1;
}
this.countMap.set(key, this.countMap.get(key) + 1);
return this.valueMap.get(key);
}
python
def get(self, key):
if key not in self.value_map or self.size == 0:
return -1
self.count_map[key] = self.count_map[key] + 1
return self.value_map[key]
```cpp
int get(int key) {
if (!valueMap.count(key) || size == 0) {
return -1;
}
countMap[key] = countMap[key] + 1;
return valueMap[key];
}
public int Get(int key) {
if (!valueMap.ContainsKey(key) || size == 0) {
return -1;
}
countMap[key] = countMap[key] + 1;
return valueMap[key];
}
func (c *counter) get(key int) int {
if _, ok := c.valueMap[key]; !ok || c.size == 0 {
return -1
}
c.countMap[key] = c.countMap[key] + 1
return c.valueMap[key]
}
*If you want to access an element, it will only take O(1) time. *
Now, it's time to insert an element in the cache. We've multiple things to consider here:
- If the key already exists, just update the value in valueMap and increment the count in countMap. This will take O(1) time.
- If the key doesn't exists, and the size of the countMap hasn't reached it's maximum capacity, then insert the key and value in valueMap, and set the count to 1 in countMap. This will take O(1) time.
- Now,there can be a case when the cache is full. To handle this, iterate through the countMap to find the key with the lowest count, then remove that entry from both the countMap and the valueMap. This will take O(n) time.
public void put(int key, int value) {
if (!valueMap.containsKey(key) && size>0) {
if (valueMap.size() == size) {
// find the key with the lowest count
Entry<Integer, Integer> lowestCountEntry = countMap.entrySet().iterator().next();
int lowestCountKey = lowestCountEntry.getKey();
for (Entry<Integer, Integer> entry : countMap.entrySet()) {
if (entry.getValue() < lowestCountEntry.getValue()) {
lowestCountKey = entry.getKey();
}
}
countMap.remove(lowestCountKey);
valueMap.remove(lowestCountKey);
}
valueMap.put(key, value);
countMap.put(key, 1);
} else if(size>0){
valueMap.put(key, value);
countMap.put(key, countMap.get(key) + 1);
}
}
put(key, value) {
if (!this.valueMap.has(key) && this.size > 0) {
if (this.valueMap.size === this.size) {
let lowestCountEntry = this.countMap.entries().next().value;
let lowestCountKey = lowestCountEntry[0];
for (let entry of this.countMap.entries()) {
if (entry[1] < lowestCountEntry[1]) {
lowestCountKey = entry[0];
}
}
this.countMap.delete(lowestCountKey);
this.valueMap.delete(lowestCountKey);
}
this.valueMap.set(key, value);
this.countMap.set(key, 1);
} else if (this.size > 0) {
this.valueMap.set(key, value);
this.countMap.set(key, this.countMap.get(key) + 1);
}
}
def put(self, key, value):
if key not in self.value_map and self.size > 0:
if len(self.value_map) == self.size:
lowest_count_key = min(self.count_map.items(), key=lambda x: x[1])[0]
del self.count_map[lowest_count_key]
del self.value_map[lowest_count_key]
self.value_map[key] = value
self.count_map[key] = 1
elif self.size > 0:
self.value_map[key] = value
self.count_map[key] += 1
void put(int key, int value) {
if (valueMap.find(key) == valueMap.end() && size > 0) {
if (valueMap.size() == size) {
auto lowestCountKey = min_element(countMap.begin(), countMap.end(), [](const auto& l, const auto& r) { return l.second < r.second; })->first;
countMap.erase(lowestCountKey);
valueMap.erase(lowestCountKey);
}
valueMap[key] = value;
countMap[key] = 1;
} else if (size > 0) {
valueMap[key] = value;
countMap[key]++;
}
}
public void Put(int key, int value) {
if (!valueMap.ContainsKey(key) && size > 0) {
if (valueMap.Count == size) {
var lowestCountKey = countMap.OrderBy(kvp => kvp.Value).First().Key;
countMap.Remove(lowestCountKey);
valueMap.Remove(lowestCountKey);
}
valueMap[key] = value;
countMap[key] = 1;
} else if (size > 0) {
valueMap[key] = value;
countMap[key] = countMap[key] + 1;
}
}
func (this *objName) Put(key int, value int) {
if _, ok := this.valueMap[key]; !ok && this.size > 0 {
if len(this.valueMap) == this.size {
lowestCountKey := getMinKey(this.countMap)
delete(this.countMap, lowestCountKey)
delete(this.valueMap, lowestCountKey)
}
this.valueMap[key] = value
this.countMap[key] = 1
} else if this.size > 0 {
this.valueMap[key] = value
this.countMap[key]++
}
}
func getMinKey(m map[int]int) (minkey int) {
minval := int(^uint(0) >> 1)
for k, v := range m {
if v < minval {
minkey, minval = k, v
}
}
return minkey
}
Note that there's a problem here. If there exists multiple keys with the same count, then this solution will not work as HashMap doesn't keep track of the insertion order.
With this, we'll move to our next implementation in which we'll be using a Singly Linked List to handle this issue.
Multiple Choice
HashMap keeps the track of insertion order of elements.
Using Singly Linked List
Implement an LFU Cache Using Singly Linked List
Here, we'll introduce a new data structure in which we'll insert the key as the count, and value as the list of elements with the same count. For this we'll be using TreeMap as it will store the count in sorted order:
java
private TreeMap<Integer, List<Integer>> sortedCountMap = new TreeMap<>();
csharp
private SortedDictionary<int, List<int>> sortedCountMap = new SortedDictionary<int, List<int>>();
javascript
let sortedCountMap = new Map();
python
sorted_count_map = {}
cpp
std::map<int, std::list<int>> sortedCountMap;
go
sortedCountMap := make(map[int][]int)
Now accessing the element that has been least recently accessed along with the lowest count will become straightforward. The first key in sortedCountMap will be the lowest frequency. Now to find the element that has been least recently accessed with that frequency, all you have to do is remove the first element from that list. This will take O(1) time.

Now, let's discuss the steps that are different from our previous implementation to get an element:
- First, we'll find the count from the countMap.
- Next, we'll remove the key from the sortedCountMap, and insert it again with a new count value.
- Note that we'll remove the entry of the previous count from the sortedCountMap if that element was the only element in the list.
private void removeEntryFromSortedCountMapIfListIsEmpty(int count) {
if (sortedCountMap.get(count).size() == 0) {
sortedCountMap.remove(count);
}
}
private removeEntryFromSortedCountMapIfListIsEmpty(count) {
if (this.sortedCountMap.get(count).size() === 0) {
this.sortedCountMap.remove(count);
}
}
def remove_entry_from_sorted_count_map_if_list_is_empty(self, count):
if len(self.sorted_count_map.get(count)) == 0:
self.sorted_count_map.remove(count)
private:
void removeEntryFromSortedCountMapIfListIsEmpty(int count) {
if (sortedCountMap[count].size() == 0) {
sortedCountMap.erase(count);
}
}
private void RemoveEntryFromSortedCountMapIfListIsEmpty(int count)
{
if (SortedCountMap[count].Count == 0)
{
SortedCountMap.Remove(count);
}
}
func (m *MyStruct) removeEntryFromSortedCountMapIfListIsEmpty(count int) {
if len(m.sortedCountMap[count]) == 0 {
delete(m.sortedCountMap, count)
}
}
public int get(int key) {
if (!valueMap.containsKey(key) || size == 0) {
return -1;
}
int count = countMap.get(key);
sortedCountMap.get(count).remove(new Integer(key));
removeEntryFromSortedCountMapIfListIsEmpty(count);
sortedCountMap.computeIfAbsent(count + 1, k -> new LinkedList<>()).add(key);
countMap.put(key, countMap.get(key) + 1);
return valueMap.get(key);
}
get(key) {
if (!this.valueMap.has(key) || this.size === 0) {
return -1;
}
let count = this.countMap.get(key);
this.sortedCountMap.get(count).delete(key);
this.removeEntryFromSortedCountMapIfListIsEmpty(count);
if (!this.sortedCountMap.has(count + 1)) {
this.sortedCountMap.set(count + 1, new LinkedList());
}
this.sortedCountMap.get(count + 1).add(key);
this.countMap.set(key, this.countMap.get(key) + 1);
return this.valueMap.get(key);
}
def get(self, key):
if not key in self.valueMap or self.size == 0:
return -1
count = self.countMap[key]
self.sortedCountMap[count].remove(key)
self.removeEntryFromSortedCountMapIfListIsEmpty(count)
if not count + 1 in self.sortedCountMap:
self.sortedCountMap[count + 1] = LinkedList()
self.sortedCountMap[count + 1].add(key)
self.countMap[key] = self.countMap[key] + 1
return self.valueMap[key]
public int get(int key) {
if (valueMap.find(key) == valueMap.end() || size == 0) {
return -1;
}
int count = countMap[key];
sortedCountMap[count].remove(key);
removeEntryFromSortedCountMapIfListIsEmpty(count);
if (sortedCountMap.find(count + 1) == sortedCountMap.end()) {
sortedCountMap[count + 1] = LinkedList<int>();
}
sortedCountMap[count + 1].push_back(key);
countMap[key] = countMap[key] + 1;
return valueMap[key];
}
public int Get(int key) {
if (!valueMap.ContainsKey(key) || size == 0) {
return -1;
}
int count = countMap[key];
sortedCountMap[count].Remove(key);
RemoveEntryFromSortedCountMapIfListIsEmpty(count);
if (!sortedCountMap.ContainsKey(count + 1)) {
sortedCountMap[count + 1] = new LinkedList<int>();
}
sortedCountMap[count + 1].AddLast(key);
countMap[key] = countMap[key] + 1;
return valueMap[key];
}
func (cache *LRUCache) Get(key int) int {
if _, ok := cache.valueMap[key]; !ok || cache.size == 0 {
return -1
}
count := cache.countMap[key]
val := cache.sortedCountMap[count].Remove(key)
cache.sortedCountMap[count].PushBack(val)
if _, ok := cache.sortedCountMap[count+1]; !ok {
cache.sortedCountMap[count+1] = list.New()
}
cache.sortedCountMap[count+1].PushBack(val)
cache.countMap[key] = cache.countMap[key] + 1
return cache.valueMap[key]
}
Let's discuss the steps that are different from our previous implementation to insert an element:
If the key doesn't exist, and the cache is full, the eviction strategy will be different now:
- We'll first remove the least frequently used key from the sortedCountMap.
- Next, we'll also remove that entry from the sortedCountMap if that key was the only element in the list.
- After that, we'll remove the key/value pair from both the countMap and the valueMap.
- Finally, we'll update the countMap, valueMap, and sortedCountMap.
Let's discuss the case, when the key already exists:
- Now, we'll update the position of the key in sortedCountMap.
- We'll also remove the entry if the key was the only element in the list with that previous count value.
public void put(int key, int value) {
if (!valueMap.containsKey(key) && size > 0) {
if (valueMap.size() == size) {
int lowestFrequency = sortedCountMap.firstKey();
int evictKey = sortedCountMap.get(lowestFrequency).remove(0);
removeEntryFromSortedCountMapIfListIsEmpty(lowestFrequency);
valueMap.remove(evictKey);
countMap.remove(evictKey);
}
valueMap.put(key, value);
countMap.put(key, 1);
sortedCountMap.computeIfAbsent(1, k -> new LinkedList<>()).add(key);
} else if (valueMap.containsKey(key) && size > 0) {
int count = countMap.get(key);
valueMap.put(key, value);
countMap.put(key, count + 1);
sortedCountMap.get(count).remove(new Integer(key));
removeEntryFromSortedCountMapIfListIsEmpty(count);
sortedCountMap.computeIfAbsent(count + 1, k -> new LinkedList<>()).add(key);
}
}
put(key, value) {
if (!this.valueMap.has(key) && this.size > 0) {
if (this.valueMap.size === this.size) {
let lowestFrequency = [...this.sortedCountMap.keys()][0];
let evictKey = this.sortedCountMap.get(lowestFrequency).shift();
if (!this.sortedCountMap.get(lowestFrequency).length) {
this.sortedCountMap.delete(lowestFrequency);
}
this.valueMap.delete(evictKey);
this.countMap.delete(evictKey);
}
this.valueMap.set(key, value);
this.countMap.set(key, 1);
if (!this.sortedCountMap.has(1)) {
this.sortedCountMap.set(1, []);
}
this.sortedCountMap.get(1).push(key);
} else if (this.valueMap.has(key) && this.size > 0) {
let count = this.countMap.get(key);
this.valueMap.set(key, value);
this.countMap.set(key, count + 1);
this.sortedCountMap.get(count).remove(key);
if (!this.sortedCountMap.get(count).length) {
this.sortedCountMap.delete(count);
}
if (!this.sortedCountMap.has(count + 1)) {
this.sortedCountMap.set(count + 1, []);
}
this.sortedCountMap.get(count + 1).push(key);
}
}
def put(self, key, value):
if not key in self.value_map and self.size > 0:
if len(self.value_map) == self.size:
lowest_frequency = min(self.sorted_count_map.keys())
evict_key = self.sorted_count_map[lowest_frequency].pop(0)
if len(self.sorted_count_map[lowest_frequency]) == 0:
del self.sorted_count_map[lowest_frequency]
del self.value_map[evict_key]
del self.count_map[evict_key]
self.value_map[key] = value
self.count_map[key] = 1
if not 1 in self.sorted_count_map:
self.sorted_count_map[1] = []
self.sorted_count_map[1].append(key)
elif key in self.value_map and self.size > 0:
count = self.count_map[key]
self.value_map[key] = value
self.count_map[key] = count + 1
self.sorted_count_map[count].remove(key)
if len(self.sorted_count_map[count]) == 0:
del self.sorted_count_map[count]
if not count+1 in self.sorted_count_map:
self.sorted_count_map[count+1] = []
self.sorted_count_map[count+1].append(key)
void put(int key, int value) {
if (valueMap.count(key) == 0 && size > 0) {
if (valueMap.size() == size) {
int lowestFrequency = sortedCountMap.begin()->first;
std::list<int>& evictKeys = sortedCountMap[lowestFrequency];
int evictKey = evictKeys.front();
evictKeys.pop_front();
if (evictKeys.empty())
sortedCountMap.erase(lowestFrequency);
valueMap.erase(evictKey);
countMap.erase(evictKey);
}
valueMap[key] = value;
countMap[key] = 1;
sortedCountMap[1].push_back(key);
} else if (valueMap.count(key) == 1 && size > 0) {
int count = countMap[key];
valueMap[key] = value;
countMap[key] = count + 1;
sortedCountMap[count].remove(key);
if (sortedCountMap[count].empty())
sortedCountMap.erase(count);
sortedCountMap[count + 1].push_back(key);
}
}
public void Put(int key, int value)
{
if (!ValueMap.ContainsKey(key) && Size > 0)
{
if (ValueMap.Count == Size)
{
var lowestFrequency = SortedCountMap.Keys.First();
var evictKey = SortedCountMap[lowestFrequency].First();
SortedCountMap[lowestFrequency].RemoveAt(0);
if (SortedCountMap[lowestFrequency].Count == 0)
{
SortedCountMap.Remove(lowestFrequency);
}
ValueMap.Remove(evictKey);
CountMap.Remove(evictKey);
}
ValueMap.Add(key, value);
CountMap.Add(key, 1);
if (!SortedCountMap.ContainsKey(1))
{
SortedCountMap.Add(1, new List<int>());
}
SortedCountMap[1].Add(key);
}
else if (ValueMap.ContainsKey(key) && Size > 0)
{
int count = CountMap[key];
ValueMap[key] = value;
CountMap[key] = ++count;
SortedCountMap[count-1].Remove(key);
if (!SortedCountMap[count-1].Any())
{
SortedCountMap.Remove(count-1);
}
if (!SortedCountMap.ContainsKey(count))
{
SortedCountMap.Add(count, new List<int>());
}
SortedCountMap[count].Add(key);
}
}
func (cache *LFUCache) Put(key int, value int) {
if _, exist := cache.values[key]; exist && cache.cap > 0 {
count := cache.counts[key]
cache.values[key] = value
cache.counts[key] = count + 1
cache.lists[count] = remove(cache.lists[count], key)
if len(cache.lists[count]) == 0 {
delete(cache.lists, count)
}
if _, exist := cache.lists[count+1]; !exist {
cache.lists[count+1] = make(queue, 0)
}
cache.lists[count+1] = append(cache.lists[count+1], key)
} else if !exist && cache.cap > 0 {
if len(cache.values) == cache.cap {
min := cache.min
evictKey := cache.lists[min][0]
cache.lists[min] = cache.lists[min][1:]
if len(cache.lists[min]) == 0 {
delete(cache.lists, min)
}
delete(cache.values, evictKey)
delete(cache.counts, evictKey)
}
cache.values[key] = value
cache.counts[key] = 1
if _, exist := cache.lists[1]; !exist {
cache.lists[1] = make(queue, 0)
}
cache.lists[1] = append(cache.lists[1], key)
cache.min = 1
}
}
There exists a problem in this implementation.
The keys having the same count are in a list. Now to access a key, inorder to update it's entry in the sortedCountMap, we'll have to iterate through the entire list. The worst complexity in this case will be O(n).
To solve this problem, we'll move to our next implementation.
Using Doubly Linked List
Implement an LFU Cache Using Doubly Linked List
To avoid iterating through the whole list, remove the element directly, and insert it against other count value sortedCountMap, we'll be needing a Doubly Linked List. Note that this will now take O(1) time.
private TreeMap<Integer, DoubleLinkedList> sortedCountMap = new TreeMap<>();
let sortedCountMap = new Map();
sorted_count_map = {}
std::map<int, DoubleLinkedList> sortedCountMap;
private SortedDictionary<int, DoubleLinkedList> sortedCountMap = new SortedDictionary<int, DoubleLinkedList>();
var sortedCountMap = make(map[int]*DoubleLinkedList)

Here, we'll store Node object as the value in valueMap. It will have key, value, and the position of the node in the linked list stored as the value in sortedCountMap:
java
private Map<Integer, Node> valueMap = new HashMap<>();
csharp
private Dictionary<int, Node> valueMap = new Dictionary<int, Node>();
go
valueMap := make(map[int]Node)
javascript
var valueMap = {};
python
value_map = {}
cpp
std::map<int, Node> valueMap;
public class Node {
private int key;
private int value;
Node next;
Node previous;
public Node(int key, int value) {
super();
this.key = key;
this.value = value;
}
class Node {
constructor(key, value) {
super();
this.key = key;
this.value = value;
}
}
class Node:
def __init__(self, key, value):
super().__init__()
self.key = key
self.value = value
self.next = None
self.previous = None
class Node {
private:
int key;
int value;
Node* next;
Node* previous;
public:
Node(int key, int value) {
this->key = key;
this->value = value;
}
};
public class Node {
private int key;
private int value;
public Node next;
public Node previous;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
type Node struct {
key int
value int
next *Node
previous *Node
}
func NewNode(key int, value int) *Node {
return &Node{
key: key,
value: value,
}
}
Let's have a look at the DoubleLinkedList class:
public class DoubleLinkedList {
private int n;
Node head;
Node tail;
public void add(Node node) {
if (head == null) {
head = node;
}
else {
tail.next = node;
node.previous = tail;
}
tail = node;
n++;
}
public void remove(Node node) {
if (node.next == null) {
tail = node.previous;
} else {
node.next.previous = node.previous;
}
if (node.previous == null) {
head = node.next;
} else {
node.previous.next = node.next;
}
n--;
}
public int size() {
return n;
}
public Node getHead() {
return head;
}
}
class DoubleLinkedList {
constructor(){
this.n = 0;
this.head = null;
this.tail = null;
}
add(node) {
if (this.head == null) {
this.head = node;
}
else {
this.tail.next = node;
node.previous = this.tail;
}
this.tail = node;
this.n++;
}
remove(node) {
if (node.next == null) {
this.tail = node.previous;
} else {
node.next.previous = node.previous;
}
if (node.previous == null) {
this.head = node.next;
} else {
node.previous.next = node.next;
}
this.n--;
}
size() {
return this.n;
}
getHead() {
return this.head;
}
}
class DoubleLinkedList:
def __init__(self):
self.n = 0
self.head = None
self.tail = None
def add(self, node):
if self.head is None:
self.head = node
else:
self.tail.next = node
node.previous = self.tail
self.tail = node
self.n += 1
def remove(self, node):
if node.next is None:
self.tail = node.previous
else:
node.next.previous = node.previous
if node.previous is None:
self.head = node.next
else:
node.previous.next = node.next
self.n -= 1
def size(self):
return self.n
def get_head(self):
return self.head
class DoubleLinkedList {
private:
int n;
Node* head;
Node* tail;
public:
DoubleLinkedList() : n(0), head(nullptr), tail(nullptr) {}
void add(Node* node) {
if (head == nullptr) {
head = node;
}
else {
tail->next = node;
node->previous = tail;
}
tail = node;
n++;
}
void remove(Node* node) {
if (node->next == nullptr) {
tail = node->previous;
} else {
node->next->previous = node->previous;
}
if (node->previous == nullptr) {
head = node->next;
} else {
node->previous->next = node->next;
}
n--;
}
int size() {
return n;
}
Node* getHead() {
return head;
}
};
public class DoubleLinkedList {
private int n;
Node head;
Node tail;
public void Add(Node node) {
if (head == null) {
head = node;
}
else {
tail.next = node;
node.previous = tail;
}
tail = node;
n++;
}
public void Remove(Node node) {
if (node.next == null) {
tail = node.previous;
} else {
node.next.previous = node.previous;
}
if (node.previous == null) {
head = node.next;
} else {
node.previous.next = node.next;
}
n--;
}
public int Size() {
return n;
}
public Node GetHead() {
return head;
}
}
package main
type Node struct {
next *Node
previous *Node
}
type DoubleLinkedList struct {
n int
head *Node
tail *Node
}
func (dll *DoubleLinkedList) Add(node *Node) {
if dll.head == nil {
dll.head = node
} else {
dll.tail.next = node
node.previous = dll.tail
}
dll.tail = node
dll.n++
}
func (dll *DoubleLinkedList) Remove(node *Node) {
if node.next == nil {
dll.tail = node.previous
} else {
node.next.previous = node.previous
}
if node.previous == nil {
dll.head = node.next
} else {
node.previous.next = node.next
}
dll.n--
}
func (dll *DoubleLinkedList) Size() int {
return dll.n
}
func (dll *DoubleLinkedList) GetHead() *Node {
return dll.head
}
To get an element, and update the value in sortedCountMap takes O(1) time now:
java
public int get(int key) {
if (!valueMap.containsKey(key) || size == 0) {
return -1;
}
Node nodeToRemove = valueMap.get(key);
Node node = new Node(key, nodeToRemove.getValue());
int count = countMap.get(key);
sortedCountMap.get(count).remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(count);
valueMap.remove(key);
countMap.remove(key);
valueMap.put(key, node);
countMap.put(key, count + 1);
sortedCountMap.computeIfAbsent(count + 1, k -> new DoubleLinkedList()).add(node);
return valueMap.get(key).getValue();
}
javascript
get(key) {
if (!this.valueMap.has(key) || this.size === 0) {
return -1;
}
let nodeToRemove = this.valueMap.get(key);
let node = new Node(key, nodeToRemove.value);
let count = this.countMap.get(key);
this.sortedCountMap.get(count).remove(nodeToRemove);
this.removeEntryFromSortedCountMapIfListIsEmpty(count);
this.valueMap.delete(key);
this.countMap.delete(key);
this.valueMap.set(key, node);
this.countMap.set(key, count + 1);
this.sortedCountMap.set(count + 1, this.sortedCountMap.get(count + 1) || new DoubleLinkedList());
this.sortedCountMap.get(count + 1).add(node);
return this.valueMap.get(key).value;
}
python
def get(self, key):
if key not in self.value_map or self.size == 0:
return -1
node_to_remove = self.value_map[key]
node = Node(key, node_to_remove.value)
count = self.count_map[key]
self.sorted_count_map[count].remove(node_to_remove)
self.remove_entry_from_sorted_count_map_if_list_is_empty(count)
del self.value_map[key]
del self.count_map[key]
self.value_map[key] = node
self.count_map[key] = count + 1
if count + 1 not in self.sorted_count_map:
self.sorted_count_map[count + 1] = DoubleLinkedList()
self.sorted_count_map[count + 1].add(node)
return self.value_map[key].value
cpp
int get(int key) {
if (valueMap.find(key) == valueMap.end() || size == 0) {
return -1;
}
Node* nodeToRemove = valueMap_[key];
Node* node = new Node(key, nodeToRemove->value);
int count = countMap_[key];
sortedCountMap_[count]->remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(count);
valueMap_.erase(key);
countMap_.erase(key);
valueMap_[key] = node;
countMap_[key] = count + 1;
if (sortedCountMap_.find(count + 1) == sortedCountMap_.end()) {
sortedCountMap_[count + 1] = new DoubleLinkedList();
}
sortedCountMap_[count + 1]->add(node);
return valueMap_[key]->value;
}
csharp
public int Get(int key) {
if (!valueMap.ContainsKey(key) || size == 0) {
return -1;
}
Node nodeToRemove = valueMap[key];
Node node = new Node(key, nodeToRemove.value);
int count = countMap[key];
sortedCountMap[count].Remove(nodeToRemove);
RemoveEntryFromSortedCountMapIfListIsEmpty(count);
valueMap.Remove(key);
countMap.Remove(key);
valueMap[key] = node;
countMap[key] = count + 1;
if (!sortedCountMap.ContainsKey(count + 1)) {
sortedCountMap[count + 1] = new DoubleLinkedList();
}
sortedCountMap[count + 1].Add(node);
return valueMap[key].value;
}
go
func (cache *LRU_Cache) Get(key int) int {
if _, ok := cache.valueMap[key]; !ok || cache.size == 0 {
return -1
}
nodeToRemove := cache.valueMap[key]
node := &Node{key, nodeToRemove.value}
count := cache.countMap[key]
cache.sortedCountMap[count].remove(nodeToRemove)
cache.removeEntryFromSortedCountMapIfListIsEmpty(count)
delete(cache.valueMap, key)
delete(cache.countMap, key)
cache.valueMap[key] = node
cache.countMap[key] = count + 1
if _, ok := cache.sortedCountMap[count+1]; !ok {
cache.sortedCountMap[count+1] = new(DoubleLinkedList)
}
cache.sortedCountMap[count+1].add(node)
return cache.valueMap[key].value
}
Finally, let's have a look at how a new element can be inserted, or existing can be updated:
```java
public void put(int key, int value) {
if (!valueMap.containsKey(key) && size > 0) {
Node node = new Node(key, value);
if (valueMap.size() == size) {
int lowestFrequency = sortedCountMap.firstKey();
Node nodeToRemove = sortedCountMap.get(lowestFrequency).getHead();
sortedCountMap.get(lowestFrequency).remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(lowestFrequency);
int keyToRemove = nodeToRemove.getKey();
valueMap.remove(keyToRemove);
countMap.remove(keyToRemove);
}
sortedCountMap.computeIfAbsent(1, k -> new DoubleLinkedList()).add(node);
valueMap.put(key, node);
countMap.put(key, 1);
} else if (valueMap.containsKey(key) && size > 0) {
Node node = new Node(key, value);
Node nodeToRemove = valueMap.get(key);
int count = countMap.get(key);
sortedCountMap.get(count).remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(count);
valueMap.remove(key);
countMap.remove(key);
valueMap.put(key, node);
countMap.put(key, count + 1);
sortedCountMap.computeIfAbsent(count + 1, k -> new DoubleLinkedList()).add(node);
}
}
javascript
put(key, value) {
if (valueMap.has(key) == false && size > 0) {
var node = new Node(key, value);
if (valueMap.size() == size) {
var lowestFrequency = Math.min(...sortedCountMap.keys());
var nodeToRemove = sortedCountMap.get(lowestFrequency).getHead();
sortedCountMap.get(lowestFrequency).remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(lowestFrequency);
var keyToRemove = nodeToRemove.getKey();
valueMap.delete(keyToRemove);
countMap.delete(keyToRemove);
}
sortedCountMap.set(1, (sortedCountMap.get(1) || new DoubleLinkedList())).add(node);
valueMap.set(key, node);
countMap.set(key, 1);
} else if (valueMap.has(key) && size > 0) {
var node = new Node(key, value);
var nodeToRemove = valueMap.get(key);
var count = countMap.get(key);
sortedCountMap.get(count).remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(count);
valueMap.delete(key);
countMap.delete(key);
valueMap.set(key, node);
countMap.set(key, count + 1);
sortedCountMap.set(count + 1, (sortedCountMap.get(count + 1) || new DoubleLinkedList())).add(node);
}
}
python
def put(self, key, value):
if key not in self.valueMap and self.size > 0:
node = Node(key, value)
if len(self.valueMap) == self.size:
lowestFrequency = min(self.sortedCountMap.keys())
nodeToRemove = self.sortedCountMap[lowestFrequency].getHead()
self.sortedCountMap[lowestFrequency].remove(nodeToRemove)
self.removeEntryFromSortedCountMapIfListIsEmpty(lowestFrequency)
keyToRemove = nodeToRemove.getKey()
del self.valueMap[keyToRemove]
del self.countMap[keyToRemove]
self.sortedCountMap.setdefault(1, DoubleLinkedList()).add(node)
self.valueMap[key] = node
self.countMap[key] = 1
elif key in self.valueMap and self.size > 0:
node = Node(key, value)
nodeToRemove = self.valueMap[key]
count = self.countMap[key]
self.sortedCountMap[count].remove(nodeToRemove)
self.removeEntryFromSortedCountMapIfListIsEmpty(count)
del self.valueMap[key]
del self.countMap[key]
self.valueMap[key] = node
self.countMap[key] = count + 1
self.sortedCountMap.setdefault(count + 1, DoubleLinkedList()).add(node)
```cpp
void put(int key, int value) {
if (valueMap.find(key) == valueMap.end() && size > 0) {
Node* node = new Node(key, value);
if (valueMap.size() == size) {
int lowestFrequency = *sortedCountMap.begin();
Node* nodeToRemove = sortedCountMap[lowestFrequency]->getHead();
sortedCountMap[lowestFrequency]->remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(lowestFrequency);
int keyToRemove = nodeToRemove->getKey();
valueMap.erase(keyToRemove);
countMap.erase(keyToRemove);
}
sortedCountMap[1] = sortedCountMap[1] ? sortedCountMap[1] : new DoubleLinkedList();
sortedCountMap[1]->add(node);
valueMap[key] = node;
countMap[key] = 1;
} else if (valueMap.find(key) != valueMap.end() && size > 0) {
Node* node = new Node(key, value);
Node* nodeToRemove = valueMap[key];
int count = countMap[key];
sortedCountMap[count]->remove(nodeToRemove);
removeEntryFromSortedCountMapIfListIsEmpty(count);
valueMap.erase(key);
countMap.erase(key);
valueMap[key] = node;
countMap[key] = count + 1;
sortedCountMap[count + 1] = sortedCountMap[count + 1] ? sortedCountMap[count + 1] : new DoubleLinkedList();
sortedCountMap[count + 1]->add(node);
}
}
void Put(int key, int value) {
if (!valueMap.ContainsKey(key) && size > 0) {
Node node = new Node(key, value);
if (valueMap.Count == size) {
int lowestFrequency = sortedCountMap.Keys.Min();
Node nodeToRemove = sortedCountMap[lowestFrequency].GetHead();
sortedCountMap[lowestFrequency].Remove(nodeToRemove);
RemoveEntryFromSortedCountMapIfListIsEmpty(lowestFrequency);
int keyToRemove = nodeToRemove.GetKey();
valueMap.Remove(keyToRemove);
countMap.Remove(keyToRemove);
}
if (!sortedCountMap.ContainsKey(1))
sortedCountMap[1] = new DoubleLinkedList();
sortedCountMap[1].Add(node);
valueMap.Add(key, node);
countMap.Add(key, 1);
} else if (valueMap.ContainsKey(key) && size > 0) {
Node node = new Node(key, value);
Node nodeToRemove = valueMap[key];
int count = countMap[key];
sortedCountMap[count].Remove(nodeToRemove);
RemoveEntryFromSortedCountMapIfListIsEmpty(count);
valueMap.Remove(key);
countMap.Remove(key);
valueMap.Add(key, node);
countMap.Add(key, count + 1);
if(!sortedCountMap.ContainsKey(count + 1))
sortedCountMap[count + 1] = new DoubleLinkedList();
sortedCountMap[count + 1].Add(node);
}
}
func (self *LFUCache) Put(key int, value int) {
if _, ok := self.valueMap[key]; !ok && self.size > 0 {
node := Node{key: key, value: value}
if len(self.valueMap) == self.size {
lowestFrequency := minKey(self.sortedCountMap)
nodeToRemove := self.sortedCountMap[lowestFrequency].GetHead()
self.sortedCountMap[lowestFrequency].Remove(nodeToRemove)
removeEntryFromSortedCountMapIfListIsEmpty(lowestFrequency)
keyToRemove := nodeToRemove.getKey()
delete(self.valueMap,keyToRemove)
delete(self.countMap,keyToRemove)
}
self.sortedCountMap[1] = append(self.sortedCountMap[1], &node)
self.valueMap[key] = &node
self.countMap[key] = 1
} else if _, ok := self.valueMap[key]; ok && self.size > 0 {
node := Node{key: key, value: value}
nodeToRemove := self.valueMap[key]
count := self.countMap[key]
remove(nodeToRemove, self.sortedCountMap[count])
removeEntryFromSortedCountMapIfListIsEmpty(count)
delete(self.valueMap,key)
delete(self.countMap,key)
self.valueMap[key] = &node
self.countMap[key] = count + 1
self.sortedCountMap[count + 1] = append(self.sortedCountMap[count + 1], &node)
}
}
Multiple Choice
To find the element that has been least recently accessed with the lowest frequency takes O(n) time in Singly Linked List.
Conclusion
Conclusion
In this tutorial, we learned to implement an LFU cache using HashMap, Singly Linked List, and Doubly Linked List.
One Pager Cheat Sheet
One Pager Cheat Sheet
- An overview of LFU caching and its various
implementationswill be discussed in this tutorial. - The
LFU Cachealgorithmmeasures frequencyand replaces theblock with least frequencywhen the cache isfull. - LFU Cache
evictsblocks based on frequency,regardless of recent access, while LRU Cacheevictsblocks based on least recent access. - Design a data structure for an
LFU Cachewith operations toputandgetvalues, and a constructor to set the initialcapacitywhich also handlesreplacing the least frequently usedandleast recently usedkeys whenfull. - The
LFUcache evicts the least frequently used memory block, regardless of when it was last accessed, by keeping track of access counters for each key. - Implementing an LFU Cache using HashMap with O(1) access time and O(n) time to replace entries in a full cache.
- The main advantage of a HashMap is that it provides
constant timeaccess to items, regardless of the size of the map. - We can implement an LFU Cache using a Singly Linked List, which will allow us to access the element that has been least recently used in O(1) time, but the worst complexity for updating will be O(n).
- We can implement an LFU Cache using a Doubly Linked List, taking O(1) time for insertions/updates and lookups.
- It is much more efficient to use a Doubly Linked List to locate the least recently accessed entry with the lowest frequency in
O(1)time, as opposed to a Singly Linked List that requiresO(n)time. - We
implementedanLFU cacheusing HashMap, Singly Linked List, and Doubly Linked List.