Link: https://leetcode.com/problems/lfu-cache/

Solution:

Topics: linked list, hash map, SortedList, LRUcache

Intuition
This is a great problem and a very nice twist on the LRUcache problem. Basically, we must remove the least frequently used element and in the case of a tie, we remove the least recent.

The key is to store nodes in their own doubly linked lists by frequency…that way when we must remove the least frequent, we just key into the smallest count and then remove the tail of that list (because the tail would be the least recently used).

There is one slight complication for the get method. We must remove the node from its current list and insert it into the count+1 list. Its also useful to delete empty lists as the number of lists can get very high.

The put method is slightly tricky…we have a hash map of lists that are keyed by frequency, so how do we get the least frequent key? Well one way would be to take the min of the keys, but this is not the most efficient operation. SortedDict is very useful here, because its essentially a hash map sorted by key (red-black tree) and we will be able to always access the least frequent (key, value) using the peakitem(0) method.

The implementation is not too bad if we use the lessons learned from LRUcache. Algorithmically, cache policies are not terribly interesting but the implementation is definitely a useful exercise in OOP (especially this one).

Implementation

class Node:
	def __init__(self, key, value):
		self.key = key
		self.value = value
		self.count = 1
		self.prev = None
		self.Next = None
		
class LL:
	def __init__(self):
		self.head = Node(-1, -1)
		self.tail = Node(-1, -1)
		self.head.next = self.tail
		self.tail.prev = self.head
		self.empty = True
		
	def add(self, node):
		first = self.head.next
		node.next = first
		first.prev = node
		self.head.next = node
		node.prev = self.head
		self.empty = False
	
	def remove(self, node):
		before = node.prev
		after = node.next
		before.next = after
		after.prev = before
		if self.head.next = self.tail:
			self.empty = True
 
from sortedcontainers import SortedDict
class LFUCache:
	def __init__(self, capacity):
		self.capacity = capacity
		self.nodes = {}
		self.lists = SortedDict()
 
	def add(self, node):
		if node.count not in self.lists:
			self.lists[node.count] = LL()
		linkedlist = self.lists[node.count]
		linkedlist.add(node)
 
	def remove(self, node):
		linkedlist = self.lists[node.count]
		linkedlist.remove(node)
		if linkedlist.empty:
			del self.lists[node.count]
 
	def get(self, key):
		if key not in self.nodes:
			return -1
		node = self.nodes[key]
		linkedlist = self.lists[node.count]
		self.remove(node)
		node.count += 1
		self.add(node)
		return node.value
 
	def put(self, key, value):
		if key in self.nodes:
			node = self.nodes[key]
			node.value = value
			self.remove(node)
			node.count += 1
			self.add(node)
			return
		if self.capacity == 0:
			linkedlist = self.lists.peakitem(0)[1]
			node = linkedlist.tail.prev
			self.remove(node)
			del self.nodes[node.key]
			self.capacity += 1
		node = Node(key, value)
		self.add(node)
		self.nodes[key] = node
		self.capacity -= 1
		
#time: o(n)
#memory: o(n)

Mnemonic
Imagine pieces of string hanging from another piece of string.

Visual

Review 1
Nice problem. I solved it mentally because implementation would take a while. Revisit when preparing for OOP.

Review 2
I re-implemented it. Still a nightmare of edge-cases.

review
hard
OOP