24 de Março
Cache LRU
Como combinar mapa hash com lista duplamente ligada para achar rápido e reorganizar por uso.
Estruturas de Dados Intermediario 30 min TypeScript
Implemente um cache LRU com:
get(key): retorna o valor se a chave existir, senao-1put(key, value): insere ou atualiza a chave
Quando a capacidade for excedida, remova a chave menos recentemente usada.
Tanto get quanto put devem rodar em O(1).
Na versão original, isso costuma vir como uma classe. Aqui no SeniorPath, use:
operations: lista de operaçõesvalues: argumentos de cada operação
E retorne um array com as respostas. Operações que não retornam valor devem devolver null.
Exemplo:
Input:
operations = ["LRUCache","put","put","get","put","get","put","get","get","get"]
values = [[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
Output:
[null,null,null,1,null,-1,null,-1,3,4]
O que perceber antes de codar
- Uma unica estrutura costuma falhar em uma das duas exigencias: busca rápida por chave ou reordenacao rápida.
- Todo `get` também muda o estado, porque a chave vira a mais recente.
- O desafio e sobre invariantes de recencia, não sobre a casca de classe da interface original.
Erros comuns
- usar array e chamar isso de O(1)
- atualizar o valor e esquecer de mover a chave para a posição mais recente
Passo 1 - Entender as duas exigencias ao mesmo tempo
O cache precisa fazer duas coisas:
- achar a chave rápido
- reordenar rápido quando a chave e usada
Esse "ao mesmo tempo" e o coracao do problema.
Passo 2 - Escrever a versão inicial mais direta
A versão inicial natural e:
- guardar pares
[key, value]em um array na ordem de uso - procurar com
findIndex - mover para o fim quando usar
- remover o mais antigo do começo quando lotar
Funciona.
Mas findIndex, splice e shift custam O(n).
Passo 3 - Perguntar por que uma unica estrutura não basta
Mapa hash resolve busca por chave.
Mas não sabe remover ou mover um item arbitrario na ordem de uso sem ajuda.
Uma lista duplamente ligada resolve bem a ordem:
- remove um no do meio
- coloca um no fim
- remove o mais antigo do começo
Tudo em O(1), se você já tiver o no na mao.
Passo 4 - Juntar as duas responsabilidades
O desenho forte e:
Map<key, node>para busca por chave- lista duplamente ligada para a ordem de recencia
O mapa responde "onde esta essa chave?" A lista responde "quem e o menos recente e quem e o mais recente?"
O editor interativo precisa de JavaScript. Voce ainda pode ler o desafio e copiar o codigo inicial abaixo.
Codigo inicial
export function runLRUCache(operations, values) {
// operations example: ["LRUCache", "put", "get"]
// values example: [[2], [1, 10], [1]]
// sua solução aqui
return []
}
Ainda não resolveu?
Ver a solução agora pode reduzir o aprendizado. Vale a pena tentar mais um pouco.
Abrir a solucao de referencia
Sem JavaScript, a solucao de referencia aparece inline em vez de um dialogo.
Solução
Complexidade final
Tempo: O(1)
Espaço: O(capacity)
Solução 1 - Array mantendo a ordem de uso
Boa versão inicial porque deixa claro o gargalo das operações lineares.
export function runLRUCache(operations, values) {
let capacity = 0
const entries = []
const output = []
for (let index = 0; index < operations.length; index += 1) {
const operation = operations[index]
const args = values[index]
if (operation === "LRUCache") {
capacity = args[0]
output.push(null)
continue
}
if (operation === "get") {
const key = args[0]
const foundIndex = entries.findIndex(([entryKey]) => entryKey === key)
if (foundIndex === -1) {
output.push(-1)
continue
}
const [entry] = entries.splice(foundIndex, 1)
entries.push(entry)
output.push(entry[1])
continue
}
const [key, value] = args
const foundIndex = entries.findIndex(([entryKey]) => entryKey === key)
if (foundIndex !== -1) {
entries.splice(foundIndex, 1)
} else if (entries.length === capacity) {
entries.shift()
}
entries.push([key, value])
output.push(null)
}
return output
}
Funciona, mas não atende o O(1) pedido.
Solução 2 - Hash map + lista duplamente ligada
Agora cada estrutura faz a parte em que ela e boa.
export function runLRUCache(operations, values) {
let capacity = 0
const nodes = new Map<number, any>()
const output = []
const head = { key: 0, value: 0, prev: null, next: null }
const tail = { key: 0, value: 0, prev: null, next: null }
head.next = tail
tail.prev = head
function remove(node) {
node.prev.next = node.next
node.next.prev = node.prev
}
function append(node) {
node.prev = tail.prev
node.next = tail
tail.prev.next = node
tail.prev = node
}
for (let index = 0; index < operations.length; index += 1) {
const operation = operations[index]
const args = values[index]
if (operation === "LRUCache") {
capacity = args[0]
output.push(null)
continue
}
if (operation === "get") {
const key = args[0]
const node = nodes.get(key)
if (!node) {
output.push(-1)
continue
}
remove(node)
append(node)
output.push(node.value)
continue
}
const [key, value] = args
if (nodes.has(key)) {
const existing = nodes.get(key)
existing.value = value
remove(existing)
append(existing)
output.push(null)
continue
}
const node = { key, value, prev: null, next: null }
nodes.set(key, node)
append(node)
if (nodes.size > capacity) {
const leastRecent = head.next
remove(leastRecent)
nodes.delete(leastRecent.key)
}
output.push(null)
}
return output
}
As invariantes importantes sao:
head.nexte sempre o menos recentetail.preve sempre o mais recente- o mapa sempre aponta para o no atual de cada chave
O que dizer na entrevista
Uma explicação curta e boa seria:
O problema pede busca por chave em O(1) e também atualizar a ordem de recencia em O(1). Só um array não fecha isso. Entao eu separo responsabilidades: hash map para achar o no da chave e lista duplamente ligada para remover, anexar no fim e expulsar o menos recente sem percorrer nada.
Isso mostra que você:
- entende por que a versão inicial falha
- justifica a composição de estruturas
- fala em invariantes em vez de só descrever ponteiros
Quando esse padrão aparece de novo
Esse padrão reaparece quando você precisa:
- combinar busca rápida com ordem mutavel
- projetar cache ou fila com politica de prioridade/recencia
- pensar em invariantes de estrutura, não só em algoritmos isolados
O ponto não e decorar esse problema.
O ponto e aprender quando um problema pede composição de estruturas.