@@ -17,4 +17,76 @@ To determine the least frequently used key, a use counter is maintained for each
1717smallest use counter is the least frequently used key.
1818
1919When a key is first inserted into the cache, its use counter is set to 1 (due to the put operation). The use counter
20- for a key in the cache is incremented either a get or put operation is called on it.
20+ for a key in the cache is incremented either a get or put operation is called on it.
21+
22+ ## Solution
23+
24+ The LFU cache algorithm tracks how often each key is accessed to determine which keys to remove when the cache is full.
25+ It uses one hash map to store key-value pairs and another to group keys by their access frequency. Each group in this
26+ frequency hash map contains nodes arranged in a doubly linked list. Additionally, it keeps track of the current least
27+ frequency to quickly identify the least used keys. When the cache reaches its limit, the key with the lowest frequency
28+ is removed first, specifically from the head of the corresponding linked list.
29+
30+ Each time a key is accessed, its frequency increases, and its position in the frequency hash map is updated, ensuring
31+ that the least used keys are prioritized for removal. This is where the doubly linked list is helpful, as the node being
32+ updated might be located somewhere in the middle of the list. Shifting the node to the next frequency level can be done
33+ in constant time, making the update process efficient.
34+
35+ Let’s discuss the algorithm of the LFU cache data structure in detail. We maintain two hash maps, ` lookup ` and ` frequencyMap ` ,
36+ and an integer, ` minimum_frequency ` , as follows:
37+
38+ - ` lookup ` keeps the key-node pairs.
39+ - The node contains three values: ` key ` , ` value ` , and ` frequency ` .
40+
41+ - ` frequencyMap ` maintains doubly linked lists against every frequency existing in the data.
42+ - For example, all the keys that have been accessed only once reside in the double linked list stored at ` frequencyMap[1] ` ,
43+ all the keys that have been accessed twice reside in the double linked list stored at ` frequencyMap[2] ` , and so on.
44+
45+ - ` minimum_frequency ` keeps the frequency of the least frequently used key.
46+
47+ Apart from the required functions i.e., Get and Put, we implement a helper function, ` PromoteKey ` that helps us maintain
48+ the order of the keys with respect to the frequency of their use. This function is implemented as follows:
49+
50+ - First, retrieve the node associated with the key.
51+ - If node's ` frequency ` is 0, the key is new. We simply increment its ` frequency ` and insert it at the tail of the
52+ linked list corresponding to the frequency 1
53+ - Otherwise, detach the ` node ` from its corresponding linked list.
54+ - If the corresponding linked list becomes empty after detaching the node, and the node’s ` frequency ` equals ` minimum_frequency ` ,
55+ there's no key left with a frequency equal to ` minimum_frequency ` . Hence, increment ` minimum_frequency ` .
56+ - Increment ` frequency ` of the key
57+ - Insert node at the tail of the linked list associated with the frequency corresponding to the updated ` frequency ` .
58+ - Before inserting it, check if the linked list exists. Suppose it doesn’t, create one.
59+
60+ After implementing ` PromoteKey() ` , the LFU cache functions are implemented as follows:
61+ - ` Get ` : We check if the key exists in the cache.
62+ - If it doesn't, we return ` None `
63+ - Otherwise, we promote the key using ` PromoteKey() ` function and return the value associated with the key.
64+ - ` Put ` : We check if the key exists in the cache.
65+ - If it doesn't, we must add this (key, value) pair to our cache.
66+ - Before adding it, we check if the cache has already reached capacity. If it has, we remove the LFU key. To do that,
67+ we remove the head node of the linked list associated with the frequency equal to ` minimum_frequency ` .
68+ - Then we add the new key.
69+ - If the key already exists, we simply update its value.
70+ - At the end of both cases, we adjust the frequency order of the key using ` PromoteKey() ` .
71+
72+ ![ Solution 1] ( ./images/solutions/lfu_cache_solution_1.png )
73+ ![ Solution 2] ( ./images/solutions/lfu_cache_solution_2.png )
74+ ![ Solution 3] ( ./images/solutions/lfu_cache_solution_3.png )
75+ ![ Solution 4] ( ./images/solutions/lfu_cache_solution_4.png )
76+ ![ Solution 5] ( ./images/solutions/lfu_cache_solution_5.png )
77+ ![ Solution 6] ( ./images/solutions/lfu_cache_solution_6.png )
78+ ![ Solution 7] ( ./images/solutions/lfu_cache_solution_7.png )
79+ ![ Solution 8] ( ./images/solutions/lfu_cache_solution_8.png )
80+ ![ Solution 9] ( ./images/solutions/lfu_cache_solution_9.png )
81+ ![ Solution 10] ( ./images/solutions/lfu_cache_solution_10.png )
82+
83+ ### Time Complexity
84+
85+ The time complexity of ` PromoteKey() ` is ` O(1) ` because the time taken to detach a node from a doubly linked list and
86+ insert a node at the tail of a linked list is ` O(1) ` . The time complexity of both Put and Get functions is ` O(1) ` because
87+ they utilize ` PromoteKey() ` and some other constant time operations.
88+
89+ ### Space Complexity
90+
91+ The space complexity of this algorithm is linear, ` O(n) ` , where ` n ` refers to the capacity of the data structure. This
92+ is the space occupied by the hash maps.
0 commit comments