24 de Março
Mediana em fluxo de dados
Como manter a mediana sempre pronta separando metade de baixo e metade de cima com duas estruturas de heap.
Heap Avancado 25 min TypeScript
Implemente uma estrutura que suporte:
addNum(num)findMedian()
A estrutura recebe numeros ao longo do tempo e precisa responder a mediana de forma eficiente.
Na versão original, isso costuma vir como uma classe chamada MedianFinder.
Aqui no SeniorPath, use:
operations: lista de operaçõesvalues: argumentos de cada operação
E retorne um array com as respostas. Operações sem retorno devem devolver null.
Exemplo:
Input:
operations = ["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
values = [null,[1],[2],null,[3],null]
Output:
[null,null,null,1.5,null,2]
O que perceber antes de codar
- A mediana depende da divisao em duas metades, não da lista inteira ordenada na sua frente.
- Depois de cada inserção, a estrutura precisa continuar equilibrada.
- Saber rebalancear e tao importante quanto saber inserir.
Erros comuns
- deixar um dos heaps com mais de um elemento de diferença em relação ao outro
- esquecer de manter todos os valores da esquerda menores ou iguais aos da direita
Passo 1 - Entender o que a mediana realmente olha
A mediana não quer "a lista toda ordenada bonitinha".
Ela quer:
- o elemento do meio
- ou a media dos dois do meio
Entao o ponto central e manter as duas metades separadas.
Passo 2 - Escrever a versão inicial mais direta
A versão inicial natural e:
- guardar todos os numeros em um array
- em cada
addNum, inserir e ordenar tudo - em
findMedian, olhar o meio
Funciona.
Mas você esta reordenando mais do que precisa.
Passo 3 - Perguntar que estado precisa continuar pronto
Se a mediana divide a lista em duas metades, faz sentido guardar:
- a metade menor
- a metade maior
E só garantir duas regras:
- tamanhos quase iguais
- tudo da esquerda menor ou igual a tudo da direita
Passo 4 - Transformar as duas metades em dois heaps com papeis diferentes
A estrutura forte fica assim:
Assim você acessa rápido:
- o maior da esquerda
- o menor da direita
E a mediana sai dali.
O editor interativo precisa de JavaScript. Voce ainda pode ler o desafio e copiar o codigo inicial abaixo.
Codigo inicial
export function runMedianFinder(operations, values) {
// operations example: ["MedianFinder", "addNum", "findMedian"]
// values example: [null, [3], null]
// 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(log n)
Espaço: O(n)
Solução 1 - Array ordenado a cada inserção
Boa versão inicial porque deixa o objetivo da mediana bem visível.
export function runMedianFinder(operations, values) {
const numbers = []
const output = []
for (let index = 0; index < operations.length; index += 1) {
const operation = operations[index]
const args = values[index]
if (operation === "MedianFinder") {
output.push(null)
continue
}
if (operation === "addNum") {
numbers.push(args[0])
numbers.sort((a, b) => a - b)
output.push(null)
continue
}
const middle = Math.floor(numbers.length / 2)
if (numbers.length % 2 === 1) {
output.push(numbers[middle])
} else {
output.push((numbers[middle - 1] + numbers[middle]) / 2)
}
}
return output
}
Funciona, mas ordenar tudo de novo em cada inserção não escala bem.
Solução 2 - Dois heaps balanceados com inserção em O(log n)
Agora a estrutura mantem a resposta pronta sem reordenacao global.
class Heap {
constructor(compare) {
this.compare = compare
this.data = []
}
peek() {
return this.data[0] ?? null
}
size() {
return this.data.length
}
push(value) {
this.data.push(value)
this.bubbleUp(this.data.length - 1)
}
pop() {
if (this.data.length === 0) {
return null
}
const top = this.data[0]
const last = this.data.pop()
if (this.data.length > 0) {
this.data[0] = last
this.bubbleDown(0)
}
return top
}
bubbleUp(index) {
while (index > 0) {
const parent = Math.floor((index - 1) / 2)
if (this.compare(this.data[index], this.data[parent]) >= 0) {
break
}
;[this.data[index], this.data[parent]] = [this.data[parent], this.data[index]]
index = parent
}
}
bubbleDown(index) {
const length = this.data.length
while (true) {
let best = index
const left = index * 2 + 1
const right = index * 2 + 2
if (left < length && this.compare(this.data[left], this.data[best]) < 0) {
best = left
}
if (right < length && this.compare(this.data[right], this.data[best]) < 0) {
best = right
}
if (best === index) {
break
}
;[this.data[index], this.data[best]] = [this.data[best], this.data[index]]
index = best
}
}
}
export function runMedianFinder(operations, values) {
const lower = new Heap((a, b) => b - a)
const upper = new Heap((a, b) => a - b)
const output = []
function rebalance() {
if (lower.size() > upper.size() + 1) {
upper.push(lower.pop())
} else if (upper.size() > lower.size()) {
lower.push(upper.pop())
}
}
for (let index = 0; index < operations.length; index += 1) {
const operation = operations[index]
const args = values[index]
if (operation === "MedianFinder") {
output.push(null)
continue
}
if (operation === "addNum") {
const value = args[0]
if (lower.peek() === null || value <= lower.peek()) {
lower.push(value)
} else {
upper.push(value)
}
rebalance()
output.push(null)
continue
}
if (lower.size() === upper.size()) {
output.push((lower.peek() + upper.peek()) / 2)
} else {
output.push(lower.peek())
}
}
return output
}
O ponto forte aqui e pensar em metades balanceadas, não em lista global, enquanto cada inserção continua em O(log n).
O que dizer na entrevista
Uma explicação curta e boa seria:
A versão inicial ordena tudo de novo a cada inserção. A versão forte separa os numeros em duas metades: um
heapmáximo para a metade menor e umheapmínimo para a metade maior. Eu mantenho os dois heaps balanceados e garanto que tudo da esquerda e menor ou igual a tudo da direita. A mediana sai do topo deles, e cada inserção continua emO(log n).
Isso mostra que você:
- modelou a mediana como fronteira entre metades
- escolheu a estrutura certa para cada lado
- explicou por que rebalanceamento faz parte da regra
Quando esse padrão aparece de novo
Esse padrão reaparece quando você precisa:
- manter uma estatistica atualizada sem recomputar tudo
- dividir dados em metade de baixo e metade de cima
- usar
heapquando o topo da estrutura importa mais que a ordenação completa - desenhar estrutura que preserva invariantes apos cada operação
O ponto não e decorar essa estrutura.
O ponto e aprender a manter a resposta certa enquanto os dados continuam chegando.