-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
LfuCache.cs
157 lines (135 loc) · 5.23 KB
/
LfuCache.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System;
using System.Collections.Generic;
namespace DataStructures.Cache;
/// <summary>
/// Least Frequently Used (LFU) cache implementation.
/// </summary>
/// <typeparam name="TKey">The type of the key (must be not null).</typeparam>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <remarks>
/// Cache keeps up to <c>capacity</c> items. When new item is added and cache is full,
/// one of the least frequently used item is removed (e.g. it keeps N items that were the most
/// frequently requested using <c>Get()</c> or <c>Put()</c> methods).
/// When there are multiple items with the same frequency, the least recently used item is removed.
///
/// Cache is built on top of two data structures:
/// - <c>Dictionary</c>. Allows items to be looked up by key in O(1) time. Another dictionary
/// is used to store the frequency of each key.
/// - <c>LinkedList</c> - Allows items with the same frequency to be ordered by the last
/// usage in O(1) time.
///
/// Useful links:
/// https://en.wikipedia.org/wiki/Cache_replacement_policies
/// https://www.enjoyalgorithms.com/blog/least-frequently-used-cache
/// https://www.educative.io/answers/what-is-least-frequently-used-cache-replace-policy
/// https://leetcode.com/problems/lfu-cache/ .
/// </remarks>
public class LfuCache<TKey, TValue> where TKey : notnull
{
private class CachedItem
{
public TKey Key { get; set; } = default!;
public TValue? Value { get; set; }
public int Frequency { get; set; }
}
private const int DefaultCapacity = 100;
private readonly int capacity;
// Note that <c>Dictionary</c> stores <c>LinkedListNode</c> as it allows
// removing the node from the <c>LinkedList</c> in O(1) time.
private readonly Dictionary<TKey, LinkedListNode<CachedItem>> cache = new();
// Map frequency (number of times the item was requested or updated)
// to the LRU linked list.
private readonly Dictionary<int, LinkedList<CachedItem>> frequencies = new();
// Track the minimum frequency with non-empty linked list in <c>frequencies</c>.
// When the last item with the minFrequency is promoted (after being requested or updated),
// the <c>minFrequency</c> is increased.
// When a new item is added, the <c>minFrequency</c> is set to 1.
private int minFrequency = -1;
/// <summary>
/// Initializes a new instance of the <see cref="LfuCache{TKey, TValue}"/> class.
/// </summary>
/// <param name="capacity">The max number of items the cache can store.</param>
public LfuCache(int capacity = DefaultCapacity)
{
this.capacity = capacity;
}
public bool Contains(TKey key) => cache.ContainsKey(key);
/// <summary>
/// Gets the cached item by key.
/// </summary>
/// <param name="key">The key of cached item.</param>
/// <returns>The cached item or <c>default</c> if item is not found.</returns>
/// <remarks> Time complexity: O(1). </remarks>
public TValue? Get(TKey key)
{
if (!cache.ContainsKey(key))
{
return default;
}
var node = cache[key];
UpdateFrequency(node, isNew: false);
return node.Value.Value;
}
/// <summary>
/// Adds or updates the value in the cache.
/// </summary>
/// <param name="key">The key of item to cache.</param>
/// <param name="value">The value to cache.</param>
/// <remarks>
/// Time complexity: O(1).
/// If the value is already cached, it is updated and the item is moved
/// to the end of the LRU list.
/// If the cache is full, one of the least frequently used items is removed.
/// </remarks>
public void Put(TKey key, TValue value)
{
if (cache.ContainsKey(key))
{
var existingNode = cache[key];
existingNode.Value.Value = value;
UpdateFrequency(existingNode, isNew: false);
return;
}
if (cache.Count >= capacity)
{
EvictOneItem();
}
var item = new CachedItem { Key = key, Value = value };
var newNode = new LinkedListNode<CachedItem>(item);
UpdateFrequency(newNode, isNew: true);
cache.Add(key, newNode);
}
private void UpdateFrequency(LinkedListNode<CachedItem> node, bool isNew)
{
var item = node.Value;
if (isNew)
{
item.Frequency = 1;
minFrequency = 1;
}
else
{
// Remove the existing node from the LRU list with its previous frequency.
var lruList = frequencies[item.Frequency];
lruList.Remove(node);
if (lruList.Count == 0 && minFrequency == item.Frequency)
{
minFrequency++;
}
item.Frequency++;
}
// Insert item to the end of the LRU list that corresponds to its new frequency.
if (!frequencies.ContainsKey(item.Frequency))
{
frequencies[item.Frequency] = new LinkedList<CachedItem>();
}
frequencies[item.Frequency].AddLast(node);
}
private void EvictOneItem()
{
var lruList = frequencies[minFrequency];
var itemToRemove = lruList.First!.Value;
lruList.RemoveFirst();
cache.Remove(itemToRemove.Key);
}
}