|
1 | | -from collections import defaultdict |
2 | | -from typing import Any, Union, Dict |
| 1 | +from datastructures.lfucache.lfu_cache_node import LfuCacheNode |
| 2 | +from datastructures.lfucache.lfu_cache import LFUCache |
| 3 | +from datastructures.lfucache.lfu_cache_v2 import LFUCacheV2 |
3 | 4 |
|
4 | | -from datastructures.linked_lists.doubly_linked_list import DoublyLinkedList |
5 | | -from datastructures.linked_lists.doubly_linked_list.node import DoubleNode |
6 | | - |
7 | | - |
8 | | -class LfuCacheNode(DoubleNode): |
9 | | - def __init__(self, data): |
10 | | - super().__init__(data) |
11 | | - self.frequency = 1 |
12 | | - |
13 | | - |
14 | | -class LFUCache: |
15 | | - def __init__(self, capacity: int): |
16 | | - """ |
17 | | - Initializes an instance of a LFUCache |
18 | | - @param capacity: Capacity of the cache |
19 | | - @type capacity int |
20 | | -
|
21 | | - 1. Dict named node self._lookup for retrieval of all nodes given a key. O(1) time to retrieve a node given a key |
22 | | - 2. Each frequency has a DoublyLinkedList stored in self._frequency where key is the frequency and value is an |
23 | | - object of DoublyLinkedList |
24 | | - 3. minimum frequency through all nodes, this can be maintained in O(1) time, taking advantage of the fact that |
25 | | - the frequency can only increment by 1. use the following 2 rules: |
26 | | - i. Whenever we see the size of the DoublyLinkedList of current min frequency is 0, increment min_frequency |
27 | | - by 1 |
28 | | - ii. Whenever we put in a new (key, value), the min frequency must be 1 (the new node) |
29 | | - """ |
30 | | - self.capacity = capacity |
31 | | - self._current_size = 0 |
32 | | - self._lookup = dict() |
33 | | - self._frequency: Dict[int, DoublyLinkedList] = defaultdict(DoublyLinkedList) |
34 | | - self._minimum_frequency = 0 |
35 | | - |
36 | | - def __update(self, node: LfuCacheNode): |
37 | | - """ |
38 | | - Helper function used in 2 cases: |
39 | | - 1. When get(key) is called |
40 | | - 2. When put(key, value) is called and key exists |
41 | | -
|
42 | | - Common point of the 2 cases: |
43 | | - 1. no new node comes in |
44 | | - 2. node is visited one more time -> node.frequency changed -> thus the place of this node will change |
45 | | -
|
46 | | - Logic: |
47 | | - 1. Pop node from 'old' DoublyLinkedList with frequency |
48 | | - 2. Append node to 'new' DoublyLinkedList with frequency + 1 |
49 | | - 3. If 'old' DoublyLinkedList has size 0 & self.minimum_frequency is frequency, update self.minimum_frequency |
50 | | - to frequency + 1 |
51 | | -
|
52 | | - Complexity Analysis: |
53 | | - Time Complexity: O(1) time |
54 | | -
|
55 | | - @param node: Node to update in the Cache |
56 | | - @type node LfuCacheNode |
57 | | - """ |
58 | | - frequency = node.frequency |
59 | | - |
60 | | - # pop the node from the 'old' DoublyLinkedList |
61 | | - self._frequency[frequency].delete_node(node) |
62 | | - |
63 | | - if self._minimum_frequency == frequency and not self._frequency[frequency]: |
64 | | - self._minimum_frequency += 1 |
65 | | - |
66 | | - node.frequency += 1 |
67 | | - frequency = node.frequency |
68 | | - |
69 | | - # add to 'new' DoublyLinkedList with new frequency |
70 | | - self._frequency[frequency].prepend(node) |
71 | | - |
72 | | - def get(self, key: int) -> Union[Any, None]: |
73 | | - """ |
74 | | - Gets an item from the Cache given the key |
75 | | - @param key: Key to use to fetch data from Cache |
76 | | - @return: Data mapped to the key |
77 | | - """ |
78 | | - if key not in self._lookup: |
79 | | - return None |
80 | | - |
81 | | - node = self._lookup[key] |
82 | | - data = node.data |
83 | | - self.__update(node) |
84 | | - return data |
85 | | - |
86 | | - def put(self, key: int, value: Any) -> None: |
87 | | - """ |
88 | | - If key is already present in the self._lookup, we perform same operations as get, except updating the node data |
89 | | - to new value |
90 | | -
|
91 | | - Otherwise, below operations are performed: |
92 | | - 1. If cache reaches capacity, pop least frequently used item. |
93 | | - 2 Facts: |
94 | | - a. we maintain self._minimum_frequency, minimum possible frequency in cache |
95 | | - b. All cache with the same frequency are stored as a DoublyLinkedList, with recently used order (Always |
96 | | - append to head). |
97 | | -
|
98 | | - Consequence is that the tail of the DoublyLinkedList with self._minimum_frequency is the least recently used |
99 | | - one, pop it. |
100 | | -
|
101 | | - 2. Add new node to self._lookup |
102 | | - 3. add new node to DoublyLinkedList with frequency of 1 |
103 | | - 4. reset minimum_frequency to 1 |
104 | | -
|
105 | | - @param key: Key to use for lookup |
106 | | - @param value: Value to store in the cache |
107 | | - @return: None |
108 | | - """ |
109 | | - |
110 | | - if self.capacity == 0: |
111 | | - return None |
112 | | - |
113 | | - if key in self._lookup: |
114 | | - node = self._lookup[key] |
115 | | - self.__update(node) |
116 | | - node.data = value |
117 | | - else: |
118 | | - if self._current_size == self.capacity: |
119 | | - node = self._frequency[self._minimum_frequency].pop() |
120 | | - self._lookup.pop(node.key) |
121 | | - self._current_size -= 1 |
122 | | - |
123 | | - node = DoubleNode(data=value, key=key) |
124 | | - self._lookup[key] = node |
125 | | - self._frequency[1].append(node) |
126 | | - self._minimum_frequency = 1 |
127 | | - self._current_size += 1 |
| 5 | +__all__ = ["LFUCache", "LFUCacheV2", "LfuCacheNode"] |
0 commit comments