Skip to content

Commit ad68c63

Browse files
authored
merge: Added new clean LFUCache class (TheAlgorithms#939)
* feat: added new clean LFUCache class * fixed: resolved spell mistake & added test casses
1 parent 9d2a7f1 commit ad68c63

File tree

2 files changed

+290
-106
lines changed

2 files changed

+290
-106
lines changed

Cache/LFUCache.js

Lines changed: 233 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,254 @@
1-
class DoubleLinkedListNode {
2-
// Double Linked List Node built specifically for LFU Cache
3-
constructor (key, val) {
1+
class CacheNode {
2+
constructor (key, value, frequency) {
43
this.key = key
5-
this.val = val
6-
this.freq = 0
7-
this.next = null
8-
this.prev = null
4+
this.value = value
5+
this.frequency = frequency
6+
7+
return Object.seal(this)
98
}
109
}
1110

12-
class DoubleLinkedList {
13-
// Double Linked List built specifically for LFU Cache
14-
constructor () {
15-
this.head = new DoubleLinkedListNode(null, null)
16-
this.rear = new DoubleLinkedListNode(null, null)
17-
this.head.next = this.rear
18-
this.rear.prev = this.head
19-
}
11+
// This frequency map class will act like javascript Map DS with more two custom method refresh & insert
12+
class FrequencyMap extends Map {
13+
static get [Symbol.species] () { return Map } // for using Symbol.species we can access Map constructor @see -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species
14+
get [Symbol.toStringTag] () { return '' }
2015

21-
_positionNode (node) {
22-
// Helper function to position a node based on the frequency of the key
23-
while (node.prev.key && node.prev.freq > node.freq) {
24-
const node1 = node
25-
const node2 = node.prev
26-
node1.prev = node2.prev
27-
node2.next = node1.prev
28-
node1.next = node2
29-
node2.prev = node1
30-
}
31-
}
16+
/**
17+
* @method refresh
18+
* @description - It's revive a CacheNode, increment of this nodes frequency and refresh the frequencyMap via new incremented nodes frequency
19+
* @param {CacheNode} node
20+
*/
21+
refresh (node) {
22+
const { frequency } = node
23+
const freqSet = this.get(frequency)
24+
freqSet.delete(node)
25+
26+
node.frequency++
3227

33-
add (node) {
34-
// Adds the given node to the end of the list (before rear) and positions it based on frequency
35-
const temp = this.rear.prev
36-
temp.next = node
37-
node.prev = temp
38-
this.rear.prev = node
39-
node.next = this.rear
40-
this._positionNode(node)
28+
this.insert(node)
4129
}
4230

43-
remove (node) {
44-
// Removes and returns the given node from the list
45-
const tempLast = node.prev
46-
const tempNext = node.next
47-
node.prev = null
48-
node.next = null
49-
tempLast.next = tempNext
50-
tempNext.prev = tempLast
31+
/**
32+
* @method insert
33+
* @description - Add new CacheNode into HashSet by the frequency
34+
* @param {CacheNode} node
35+
*/
36+
insert (node) {
37+
const { frequency } = node
5138

52-
return node
39+
if (!this.has(frequency)) {
40+
this.set(frequency, new Set())
41+
}
42+
43+
this.get(frequency).add(node)
5344
}
5445
}
5546

5647
class LFUCache {
57-
// LFU Cache to store a given capacity of data
58-
// The Double Linked List is used to store the order of deletion from the cache
59-
// The rear.prev holds the most frequently used key and the head.next holds the least used key
60-
// When the number of elements reaches the capacity, the least frequently used item is removed before adding the next key
61-
constructor (capacity) {
62-
this.list = new DoubleLinkedList()
63-
this.capacity = capacity
64-
this.numKeys = 0
65-
this.hits = 0
66-
this.miss = 0
67-
this.cache = {}
68-
}
48+
#capacity
49+
#frequencyMap
6950

70-
cacheInfo () {
71-
// Return the details for the cache instance [hits, misses, capacity, current_size]
72-
return `CacheInfo(hits=${this.hits}, misses=${this.miss}, capacity=${this.capacity}, current size=${this.numKeys})`
73-
}
51+
/**
52+
* @param {number} capacity - The range of LFUCache
53+
* @returns {LFUCache} - sealed
54+
*/
55+
constructor (capacity) {
56+
this.#capacity = capacity
57+
this.#frequencyMap = new FrequencyMap()
58+
this.misses = 0
59+
this.hits = 0
60+
this.cache = new Map()
61+
62+
return Object.seal(this)
63+
}
64+
65+
/**
66+
* Get the capacity of the LFUCache
67+
* @returns {number}
68+
*/
69+
get capacity () {
70+
return this.#capacity
71+
}
72+
73+
/**
74+
* Get the current size of LFUCache
75+
* @returns {number}
76+
*/
77+
get size () {
78+
return this.cache.size
79+
}
80+
81+
/**
82+
* Set the capacity of the LFUCache if you decrease the capacity its removed CacheNodes following the LFU - least frequency used
83+
*/
84+
set capacity (newCapacity) {
85+
if (this.#capacity > newCapacity) {
86+
let diff = this.#capacity - newCapacity // get the decrement number of capacity
87+
88+
while (diff--) {
89+
this.#removeCacheNode()
90+
}
7491

75-
set (key, value) {
76-
// Sets the value for the input key and updates the Double Linked List
77-
if (!(key in this.cache)) {
78-
if (this.numKeys >= this.capacity) {
79-
const keyToDelete = this.list.head.next.key
80-
this.list.remove(this.cache[keyToDelete])
81-
delete this.cache[keyToDelete]
82-
this.numKeys -= 1
92+
this.cache.size === 0 && this.#frequencyMap.clear()
8393
}
84-
this.cache[key] = new DoubleLinkedListNode(key, value)
85-
this.list.add(this.cache[key])
86-
this.numKeys += 1
87-
} else {
88-
const node = this.list.remove(this.cache[key])
89-
node.val = value
90-
this.list.add(node)
94+
95+
this.#capacity = newCapacity
9196
}
92-
}
9397

94-
get (key) {
95-
// Returns the value for the input key and updates the Double Linked List. Returns null if key is not present in cache
96-
if (key in this.cache) {
97-
this.hits += 1
98-
this.list.add(this.list.remove(this.cache[key]))
99-
return this.cache[key].val
98+
get info () {
99+
return Object.freeze({
100+
misses: this.misses,
101+
hits: this.hits,
102+
capacity: this.capacity,
103+
currentSize: this.size,
104+
leastFrequency: this.leastFrequency
105+
})
106+
}
107+
108+
get leastFrequency () {
109+
const freqCacheIterator = this.#frequencyMap.keys()
110+
let leastFrequency = freqCacheIterator.next().value || null
111+
112+
// select the non-empty frequency Set
113+
while (this.#frequencyMap.get(leastFrequency)?.size === 0) {
114+
leastFrequency = freqCacheIterator.next().value
115+
}
116+
117+
return leastFrequency
118+
}
119+
120+
#removeCacheNode () {
121+
const leastFreqSet = this.#frequencyMap.get(this.leastFrequency)
122+
// Select the least recently used node from the least Frequency set
123+
const LFUNode = leastFreqSet.values().next().value
124+
125+
leastFreqSet.delete(LFUNode)
126+
this.cache.delete(LFUNode.key)
127+
}
128+
129+
/**
130+
* if key exist then return true otherwise false
131+
* @param {any} key
132+
* @returns {boolean}
133+
*/
134+
has (key) {
135+
key = String(key) // converted to string
136+
137+
return this.cache.has(key)
138+
}
139+
140+
/**
141+
* @method get
142+
* @description - This method return the value of key & refresh the frequencyMap by the oldNode
143+
* @param {string} key
144+
* @returns {any}
145+
*/
146+
get (key) {
147+
key = String(key) // converted to string
148+
149+
if (this.cache.has(key)) {
150+
const oldNode = this.cache.get(key)
151+
this.#frequencyMap.refresh(oldNode)
152+
153+
this.hits++
154+
155+
return oldNode.value
156+
}
157+
158+
this.misses++
159+
return null
160+
}
161+
162+
/**
163+
* @method set
164+
* @description - This method stored the value by key & add frequency if it doesn't exist
165+
* @param {string} key
166+
* @param {any} value
167+
* @param {number} frequency
168+
* @returns {LFUCache}
169+
*/
170+
set (key, value, frequency = 1) {
171+
key = String(key) // converted to string
172+
173+
if (this.#capacity === 0) {
174+
throw new RangeError('LFUCache ERROR: The Capacity is 0')
175+
}
176+
177+
if (this.cache.has(key)) {
178+
const node = this.cache.get(key)
179+
node.value = value
180+
181+
this.#frequencyMap.refresh(node)
182+
183+
return this
184+
}
185+
186+
// if the cache size is full, then it's delete the Least Frequency Used node
187+
if (this.#capacity === this.cache.size) {
188+
this.#removeCacheNode()
189+
}
190+
191+
const newNode = new CacheNode(key, value, frequency)
192+
193+
this.cache.set(key, newNode)
194+
this.#frequencyMap.insert(newNode)
195+
196+
return this
197+
}
198+
199+
/**
200+
* @method parse
201+
* @description - This method receive a valid LFUCache JSON & run JSON.prase() method and merge with existing LFUCache
202+
* @param {JSON} json
203+
* @returns {LFUCache} - merged
204+
*/
205+
parse (json) {
206+
const { misses, hits, cache } = JSON.parse(json)
207+
208+
this.misses += misses ?? 0
209+
this.hits += hits ?? 0
210+
211+
for (const key in cache) {
212+
const { value, frequency } = cache[key]
213+
this.set(key, value, frequency)
214+
}
215+
216+
return this
217+
}
218+
219+
/**
220+
* @method clear
221+
* @description - This method cleared the whole LFUCache
222+
* @returns {LFUCache}
223+
*/
224+
clear () {
225+
this.cache.clear()
226+
this.#frequencyMap.clear()
227+
228+
return this
229+
}
230+
231+
/**
232+
* @method toString
233+
* @description - This method generate a JSON format of LFUCache & return it.
234+
* @param {number} indent
235+
* @returns {string} - JSON
236+
*/
237+
toString (indent) {
238+
const replacer = (_, value) => {
239+
if (value instanceof Set) {
240+
return [...value]
241+
}
242+
243+
if (value instanceof Map) {
244+
return Object.fromEntries(value)
245+
}
246+
247+
return value
248+
}
249+
250+
return JSON.stringify(this, replacer, indent)
100251
}
101-
this.miss += 1
102-
return null
103-
}
104252
}
105253

106-
export { LFUCache }
254+
export default LFUCache

0 commit comments

Comments
 (0)