# LRU Cache en Python: implementación desde cero
# Dos enfoques: lista doblemente enlazada + dict, y OrderedDict
from typing import Optional
from collections import OrderedDict
# ?????????????????????????????????????????????????????????????
# Implementación 1: Lista doblemente enlazada + diccionario
# Todas las operaciones en O(1) amortizado
# ?????????????????????????????????????????????????????????????
class Node:
"""Nodo de la lista doblemente enlazada."""
def __init__(self, key: int, value: int):
self.key = key
self.value = value
self.prev: Optional['Node'] = None
self.next: Optional['Node'] = None
class LRUCache:
"""
LRU Cache con O(1) para get() y put().
Estructura interna:
- dict: key ? Node (acceso O(1) al nodo)
- Lista doblemente enlazada: head ? nodo más reciente ... nodo más antiguo ? tail
Los nodos 'head' y 'tail' son centinelas vacíos para evitar
comprobar None en cada operación.
"""
def __init__(self, capacity: int):
self.capacity = capacity
self.cache: dict = {} # key ? Node
# Centinelas: head.next = más reciente, tail.prev = más antiguo
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node: Node) -> None:
"""Desconecta un nodo de la lista."""
prev = node.prev
nxt = node.next
prev.next = nxt
nxt.prev = prev
def _insert_front(self, node: Node) -> None:
"""Inserta un nodo justo después del centinela head (posición 'más reciente')."""
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key: int) -> int:
"""
Devuelve el valor asociado a key.
Al acceder, el nodo se mueve al frente (más reciente).
Devuelve -1 si no existe.
"""
if key not in self.cache:
return -1
node = self.cache[key]
self._remove(node)
self._insert_front(node)
return node.value
def put(self, key: int, value: int) -> None:
"""
Inserta o actualiza key?value.
Si la clave ya existe, actualiza y mueve al frente.
Si la cache está llena, elimina el elemento menos usado (tail.prev).
"""
if key in self.cache:
# Actualizar y promover
node = self.cache[key]
self._remove(node)
node.value = value
self._insert_front(node)
else:
if len(self.cache) >= self.capacity:
# Eliminar el LRU: el nodo más cercano a tail
lru = self.tail.prev
self._remove(lru)
del self.cache[lru.key]
# Insertar nuevo nodo al frente
node = Node(key, value)
self._insert_front(node)
self.cache[key] = node
# ?????????????????????????????????????????????????????????????
# Implementación 2: OrderedDict (versión compacta)
# OrderedDict mantiene el orden de inserción y permite move_to_end()
# ?????????????????????????????????????????????????????????????
class LRUCacheOD:
"""
LRU Cache usando collections.OrderedDict.
Más conciso; internamente también O(1) para get/put.
"""
def __init__(self, capacity: int):
self.capacity = capacity
self.cache: OrderedDict = OrderedDict()
def get(self, key: int) -> int:
if key not in self.cache:
return -1
# Mover al final = 'más reciente'
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
# Eliminar el primero = 'menos reciente'
self.cache.popitem(last=False)
# ?????????????????????????????????????????????????????????????
# Tests
# ?????????????????????????????????????????????????????????????
def run_tests(CacheClass, name: str):
print(f"n=== {name} ===")
ok = 0
total = 0
def check(got, expected, msg):
nonlocal ok, total
total += 1
status = "OK" if got == expected else f"FALLO (esperado {expected}, obtenido {got})"
print(f" {msg}: {status}")
if got == expected:
ok += 1
# Test 1: operación básica
c = CacheClass(2)
c.put(1, 1)
c.put(2, 2)
check(c.get(1), 1, "get(1) tras put(1,1) put(2,2)")
# Test 2: eviction del LRU
c.put(3, 3) # 2 era el LRU al hacer get(1), así que 2 se elimina
check(c.get(2), -1, "get(2) tras insertar 3 debe estar eviccionado")
check(c.get(3), 3, "get(3) existe")
# Test 3: actualización de valor existente
c2 = CacheClass(2)
c2.put(1, 10)
c2.put(2, 20)
c2.put(1, 100) # actualiza clave 1, la convierte en más reciente
c2.put(3, 30) # clave 2 es el LRU ? se elimina
check(c2.get(2), -1, "clave 2 eviccionada tras actualizar clave 1")
check(c2.get(1), 100, "clave 1 actualizada a 100")
# Test 4: capacidad 1
c3 = CacheClass(1)
c3.put(1, 1)
c3.put(2, 2)
check(c3.get(1), -1, "capacidad 1: clave 1 eviccionada")
check(c3.get(2), 2, "capacidad 1: clave 2 existe")
print(f" ? {ok}/{total} tests pasados")
if __name__ == '__main__':
run_tests(LRUCache, "LRUCache (lista doblemente enlazada)")
run_tests(LRUCacheOD, "LRUCacheOD (OrderedDict)")
LRU Cache en Python desde cero
Implementación del LRU Cache (Least Recently Used) en Python con O(1) en get y put. Dos versiones: lista doblemente enlazada con dict para entender el mecanismo, y versión compacta con collections.OrderedDict. Pregunta clásica de entrevistas técnicas.
Descargar adjuntos
COMPARTE ESTE TUTORIAL
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP