Need some feedback on this implementation of LFU cache in python3.
Original problem : https://leetcode.com/problems/lfu-cache/
Would really appreciate some feedback on readability, understandability and areas of improvement.
""" ListNode class to create new nodes objects
@variables: key - Key from key,value pair stored by node
value - Value from key,value pair stored by node
next - Link to next ListNode object of doubly linked list
prev - Link to previous ListNode object of doubly linked list
frequency - Holds the number of times this node was accessed
in LFUCache.get() and LFUCache.put() methods.
Default is 1 at node creation on LFUCache.put() call
"""
class ListNode:
def __init__(self, key=0, val=0, next=None, prev=None):
self.key = key
self.val = val
self.next = next
self.prev = prev
self.frequency = 1
""" Main class to create LFU cache.
The idea is to maintain cache linked list in order of frequency and LRU within same frequency
A hash map (self.hash_map) will hold node key as key and pointer to node as value. Another hash map
will hold distinct node frequencies as key and pointer to head node for the frequency.
______ ______ ______ ______ ______
| | | | | |
hash_map | 23 | 56 | 14 | 6 | 19 |
|______|______|______|______|______|
| | | | |
_____________| ____| | |_____ |_______________
/ / | \ \
_______ ___V___ ____V__ __V____ _V_____ ___V___ _______
| | <--- | | <--- | | <--- | | <--- | | <--- | | <--- | |
LFUCache | Head | | 23(5) | | 56(3) | | 14(2) | | 6(2) | | 19(1) | | Tail |
|_______| ---> |_______| ---> |_______| ---> |_______| ---> |_______| ---> |_______| ---> |_______|
^ ^ ^ ^
\ \______ | /
\______________ \ | _____________________/
_\___ __\__ __|__ __/__
| | | | |
freq_map | 5 | 3 | 2 | 1 |
|_____|_____|_____|_____|
"""
class LFUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.hash_map = {} # Hash map to hold cache keys as key and link to node as value
self.freq_map = {} # Hash map to hold frequency as key and link to head node for that frequency
# Dummy head and tail cache nodes to make opertions on edge cases(one node and two node cache) simple
self.head = ListNode(0,0)
self.tail = ListNode(0,0)
# Initially set tail (dummy node) as head node for frequency 1
# so that first cache node is added before tail node
self.freq_map[1] = self.tail
# Link head cache node and tail cache nodes together
self.head.next,self.tail.prev = self.tail,self.head
""" This method will get the value of key (input) if it exists
in the LFU cache else returns -1. If key exists then this method
will call self._move() to move the node to front of it's new
frequency queue
"""
def get(self, key: int) -> int:
if self.hash_map.get(key):
# Key exists, get link to node from hash map
node = self.hash_map.get(key)
# Since frequency changed, move this node to
# front of new frequency queue
self._move(node)
return node.val
else:
return -1
""" This method will update the value of key (input) if it exists
in the LFU cache else inserts new node into the appropriate position
on LFU cache
"""
def put(self, key: int, value: int) -> None:
# Handle edge case when capacity is 0
if self.capacity == 0:
return
if self.hash_map.get(key):
# Key exists, get link to node from hash map
self.hash_map.get(key).val = value
# Move this node to front of new frequency queue
self._move(self.hash_map.get(key))
else:
# Key does not exist, need to create a new node. Check if capcaity is full
if len(self.hash_map) == self.capacity:
# If node to be removed is the head node for it's frequency then remove it from frequency map
# If frequency of node to be removed is 1 then we need to reset head node for frequency 1 to tail node
if self.freq_map.get(self.tail.prev.frequency) == self.tail.prev and self.tail.prev.frequency == 1:
self.freq_map[1] = self.tail
elif self.freq_map.get(self.tail.prev.frequency) == self.tail.prev:
self.freq_map.pop(self.tail.prev.frequency)
# Remove last node (tail.prev) from the cache list
self._remove(self.tail.prev)
# Create new node and
# - Add it to cache list
# - Set it as head node for frequency 1
node = ListNode(key,value)
head = self.freq_map.get(1)
self.freq_map[1] = node
self._add(node, head)
""" This method will add a new node(input) before the head(input) node
on LFU cache
"""
def _add(self, node :ListNode, head :ListNode) -> None:
# When adding in the middle of cache we don't have dummy head node
# so we set head.prev to play as dummy head node
if head != self.head:
head = head.prev
# Add node operation
node.next = head.next
head.next.prev = node
head.next = node
node.prev = head
self.hash_map[node.key] = node
""" This method will remove a node(input) from LFU cache and hash_map
"""
def _remove(self, node :ListNode) -> None:
node.prev.next = node.next
node.next.prev = node.prev
node.next = None
node.prev = None
self.hash_map.pop(node.key)
def _move(self, node :ListNode) -> None:
new_frequency = node.frequency + 1
# We need to find head node, before which we need to place the input node
# If new frequency already exists in the cache list then get the head node for that frequency
# else if new frequency does not exist then node will remain at same place in cache so set head as node.next
# else get the head node for current frequency of input node
if self.freq_map.get(new_frequency):
head = self.freq_map.get(new_frequency)
elif self.freq_map.get(node.frequency) == node:
head = node.next
else:
head = self.freq_map.get(node.frequency)
# Set new head nodes for current frequency of input node
if self.freq_map.get(node.frequency) == node and node.frequency == node.next.frequency:
self.freq_map[node.frequency] = node.next
elif self.freq_map.get(node.frequency) == node:
self.freq_map.pop(node.frequency)
self._remove(node)
self._add(node,head)
node.frequency = new_frequency
# Set this node as head node for new frequency (current frequency + 1)
self.freq_map[new_frequency] = node