Teori LRU
LRU atau Least Recently Used akan membuang item yang paling jarang digunakan terlebih dahulu. Jadi tujuan akhirnya adalah cache yang berisi item yang sering digunakan.
Contoh sederhana penggunaanya adalah ketika kita mengetik alamat web pada browser. Browser akan mencari item berdasarkan alamat yang diketikan dan mencari dalam cache, link mana yang paling sering digunakan.
Pendekatan LRU Cache yang akan kita lakukan adalah menggunakan double linked list dan hash table untuk mempercepat proses pencarian cache.

Double linked list adalah dimana setiap node memiliki referensi untuk prev dan next node. Head adalah node pertama dalam list, sementara tail adalah node terakhir dalam list.
Untuk melacak item dalam linked list, digunakan age bits, makin sering suatu item digunakan, makin besar nilai age bits.
Ketika cache penuh, kita perlu membuang item yang paling jarang digunakan. Pendekatan lain adalah, item yang lebih dekat dengan head diutamakan.
Ketika suata item diinput, ada kemungkinan item tersebut sudah ada didalam linked list. Kita perlu mencari item tersebut dan melakukan update. Saat melakukan pencarian, operasi ini memerlukan O(N) time complexity. Oleh karena itu kita gunakan hash table untuk mempersingkat waktu menjadi O(1).
Agar lebih jelas, berikut simulasi LRU Cache, menggunakan skenario alamat URL. Dimana ukuran cache adalah 5.

Setiap node pada linked list harus diberi ID yang akan digunakan pada hashtable.
Misalnya kita masuk ke URL A, dengan ID 0, sebelum melakukan insert kedalam linked list, kita periksa dahulu dalam hash table, jika tersedia dalam hash table, dilakukan update, jika tidak maka dilakukan insert.

Kemudian, misalnya URL B diakses. Sama seperti proses sebelumnya, periksa dalam hash table, jika sudah ada dilakukan proses update, jika belum dilakukan proses insert.

Contoh berikutnya, alamat C diakses. Dapat diperhatikan, proses insert di head. Karena sesuai nama algoritma Least Recently Used, atau yang paling jarang digunakan, jadi makin kebelakang makin jarang digunakan.
Untuk singkat cerita, kita linked list sudah terisi semua, seperti gambar dibawah. Saat ini cache sudah penuh.

Jika Kita mengakses URL F, maka item terakhir harus dibuang, shift node ke kanan, lalu insert node F di head.

Simulasi berikutnya yang mungkin terjadi adalah, mengakses URL yang sudah pernah di insert, katakanlah URL D. Ketika kita periksa pada hash table, URL D sudah ada, maka kita perlu melakukan update dengan memindahkan node D ke head. Lalu shift node lainnya ke kanan.

Implementasi LRU Cache Dalam Python
Berikut contoh implementasi LRU cache dalam Python
#capacity of the cache (how many items it can store)
CAPACITY = 4
#we implement the cache with doubly linked lists - this is the node class
class Node:
def __init__(self,id,data):
self.id = id
self.data = data
self.previous_node = None
self.next_node = None
#we implement the cache with doubly linked lists
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
class LRUCache:
def __init__(self):
self.actual_size = 0
#we store integer node id - Node pairs: this is how we can find a node in O(1)
self.dictionary = {}
self.linked_list = DoublyLinkedList()
def put(self,id,data):
#update the node if already exists (we have to check whether the dict is empty first)
if id in self.dictionary:
node = self.dictionary[id]
node.data = data
#update the node to be the head (cache feature)
self.update(node)
return
#the data is not present in the cache so insert
node = Node(id,data)
#we have to insert into the cache + set it to be the head node
if self.actual_size < CAPACITY:
self.actual_size = self.actual_size + 1
self.add(node)
else:
#the cache is full: we have to remove the last item + insert the new one
self.remove_tail()
self.add(node)
#add node to the head
def add(self,node):
#the node will be the new head: so update accordingly
node.next_node = self.linked_list.head
#it is the first node: no previous node
node.previous_node = None
#we have to update the previous head: point to the new head (which is the node)
if self.linked_list.head:
self.linked_list.head.previous_node = node
#update the head node
self.linked_list.head = node
#if there is 1 node in the list: it is the head as well as the tail
if not self.linked_list.tail:
self.linked_list.tail = node
#we have to update the dictionary with the node !!!
self.dictionary[node.id] = node
#remove last item (least frequently used)
def remove_tail(self):
#get node from the dictionary
last_node = self.dictionary[self.linked_list.tail.id]
#new tail node is the previous node (because we remove the actual one)
self.linked_list.tail = self.linked_list.tail.previous_node
#set the next node to be a NULL: because it is the right-most item
if self.linked_list.tail:
self.linked_list.tail.next_node = None
#avoid obsolete references
last_node = None
#get the item with ID id + move to the head because we've visited this item
def get(self,id):
#the dictionary does not contain the item [O(1) running time!!!]
if not self.dictionary[id]:
return None
#the dictionary contains the item
node = self.dictionary[id]
#move to the head (frequently visited items must be close to the head)
self.update(node)
return node
#move the given node to the front (head)
def update(self,node):
#doubly linked list: we can get the previous node and the next node
previous_node = node.previous_node
next_node = node.next_node
#so it is a middle node (not the head) in the list
if previous_node:
previous_node.next_node = next_node
else: #we know that this is the head (first node)
self.linked_list.head = next_node
#so not the last node
if next_node:
next_node.previous_node = previous_node
else: #we know it is the last node
self.linked_list.tail = previous_node
#we have to move the node to the front
self.add(node)
def show(self):
actual_node = self.linked_list.head
#consider all the nodes in the linked list
while actual_node:
print("%s->"%actual_node.data)
actual_node = actual_node.next_node
if __name__ == "__main__":
cache = LRUCache()
cache.put(0, 'A')
cache.put(1, 'B')
cache.put(2, 'C')
cache.put(3, 'D')
cache.put(4, 'E')
cache.put(5, 'F')
cache.put(6, 'G')
cache.show()
print("Getting node: ",cache.get(6).data)
cache.show()
print()
print("Getting node: ",cache.get(3).data)
cache.show()
print()
print("Getting node: ",cache.get(4).data)
cache.show()