Once we have an updated linked list, we can find the LFU item easily by looking at the head of list. When we hit capacity and need to evict an key, we can delete the key at the head of the list.
O(n) space
class LFULinkedList:
def __init__(self, key=None, frequency=0):
self.key = key
self.frequency = frequency
self.next = None
self.prev = None
def __str__(self):
return ("LFULinkedList Node with key %s and freq %d,"
" prev %s, next %s" % (self.key, self.frequency,
self.prev.key if self.prev else None,
self.next.key if self.next else None))
def linkNext(self, nextNode):
"""
Links nextNode to the current node.
Sets this node's next pointer to nextNode, and sets nextNode's prev pointer to this node so that the nextNode follows this node.
Overwrites any existing connections to this node's follower and nextNode's predecessor.
"""
self.next = nextNode
nextNode.prev = self
def insertIntoList(self, insertionNode):
"""
Insert the given node into the linked list, following insertionNode.
"""
nextNode = insertionNode.next
insertionNode.linkNext(self)
self.linkNext(nextNode)
def cutFromList(self):
"""
Cuts this node from any linked list it is part of.
Sets this node's neighbors to point to each other, and set this node's next and prev pointers to None.
Does not assume that linked list has sentinel head and tail nodes.
"""
prevNode = self.prev
nextNode = self.next
if prevNode:
prevNode.next = nextNode
if nextNode:
nextNode.prev = prevNode
self.next = None
self.prev = None
class LFULinkedListSentinel(LFULinkedList):
def __init__(self):
super().__init__(key=None, frequency=-1)
def __str__(self):
return "LFU Linked List Sentinel"
class LFUCache:
def __init__(self, capacity: int):
self.maxCapacity = capacity
self.capacity = 0
self.cache = {}
self.nodeMap = {}
self.mostRecentPerCount = {}
# Pointer to head of a linked list, which is the least frequntly used key
self.headSentinel = LFULinkedListSentinel()
self.tailSentinel = LFULinkedListSentinel()
self.headSentinel.linkNext(self.tailSentinel)
self.leastFrequent = None
def leastFrequentNode(self):
return self.headSentinel.next
def leastFrequentCount(self):
return self.leastFrequentNode().frequency if self.leastFrequentNode() else -1
def updateMostRecentPerCount(self, node):
"""
Takes a node and updates the mostRecentPerCount if node is the most recent node for its frequency. If there are other nodes with the same frequency then the mostRecentPerCount will point to the most recent of those other nodes. Otherwise, deletes the mostRecentPerCount entry for node's frequency.
Does nothing if node is not the most recent for its frequency.
This function MUST be called while node is still part of the linked list and before its frequency is changed. If node's prev pointer is not set this function will fail.
"""
oldPrev = node.prev
if node == self.mostRecentPerCount[node.frequency]:
if oldPrev and oldPrev.frequency == node.frequency:
self.mostRecentPerCount[node.frequency] = oldPrev
else:
del self.mostRecentPerCount[node.frequency]
def incrementCount(self, key):
assert key in self.nodeMap, "Incrementing count for non-existent key"
node = self.nodeMap[key]
# Special case where node is the most recent of its frequency and there are no nodes with frequency + 1. In this case there are no changes to the linked list order.
if node == self.mostRecentPerCount.get(node.frequency, None) and (node.frequency + 1) not in self.mostRecentPerCount:
self.updateMostRecentPerCount(node)
node.frequency += 1
self.mostRecentPerCount[node.frequency] = node
return
self.updateMostRecentPerCount(node)
oldPrev = node.prev
oldNext = node.next
oldPrev.linkNext(oldNext)
# Increment frequency
node.frequency += 1
# Add node back into its new place
# Three cases for where we add the node after incrementing it's frequency to from f to f + 1. We choose, in order of descending priority:
# 1: There are already other keys with frequency f + 1. Add the node after the most recent of those nodes.
# 2: There are other keys with frequency f. Add the node after the most recent of those nodes. The most recent node with freq f is calculated after this node has been removed.
# 3: Add node after its original prev node. This is covered in the special case earlier
newPrev = self.mostRecentPerCount.get(node.frequency, None) or self.mostRecentPerCount.get(node.frequency - 1, None)
newNext = newPrev.next
newPrev.linkNext(node)
node.linkNext(newNext)
self.mostRecentPerCount[node.frequency] = node
def diagList(self):
"""
Debug function to print out the state of the linked list of cache keys.
"""
node = self.headSentinel
string = ["Diag List "]
while node:
if node.key is not None:
string.append(str(node))
node = node.next
def get(self, key: int) -> int:
# Update frequency
if key in self.nodeMap:
self.incrementCount(key)
return self.cache.get(key, -1)
def put(self, key: int, value: int) -> None:
if self.maxCapacity == 0:
return
if key in self.nodeMap:
self.incrementCount(key)
self.cache[key] = value
else:
if self.capacity == self.maxCapacity:
evicted = self.leastFrequentNode()
self.updateMostRecentPerCount(evicted)
evicted.cutFromList()
del self.cache[evicted.key]
del self.nodeMap[evicted.key]
self.capacity -= 1
self.cache[key] = value
newNode = LFULinkedList(key)
self.nodeMap[key] = newNode
precNode = self.mostRecentPerCount[self.leastFrequentCount()] if self.leastFrequentCount() == 0 else self.headSentinel
newNode.insertIntoList(precNode)
self.mostRecentPerCount[0] = newNode
self.capacity += 1
# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)