Alguém pode ajudar a explicar como a construção de um heap pode ser de O (n) complexidade?
A inserção de um item em um heap é O(log n)
e a inserção é repetida n / 2 vezes (o restante são folhas e não podem violar a propriedade heap). Então, isso significa que a complexidade deveria ser O(n log n)
, eu acho.
Em outras palavras, para cada item que "heapizamos", é possível filtrar uma vez para cada nível do heap até agora (que é o nível de log n).
o que estou perdendo?
Respostas:
Eu acho que há várias perguntas enterradas neste tópico:
buildHeap
para que seja executado em O (n) tempo?buildHeap
é executado no tempo O (n) quando implementado corretamente?Como você implementa
buildHeap
para que seja executado em O (n) tempo?Freqüentemente, as respostas a essas perguntas concentram-se na diferença entre
siftUp
esiftDown
. Fazer a escolha correta entresiftUp
esiftDown
é essencial para obter o desempenho de O (n)buildHeap
, mas não ajuda a entender a diferença entrebuildHeap
eheapSort
em geral. De fato, as implementações adequadas de ambosbuildHeap
eheapSort
será única utilizarsiftDown
. AsiftUp
operação é necessária apenas para executar inserções em um heap existente, portanto, ela seria usada para implementar uma fila de prioridade usando um heap binário, por exemplo.Eu escrevi isso para descrever como um heap máximo funciona. Esse é o tipo de heap normalmente usado para classificação de heap ou para uma fila de prioridade em que valores mais altos indicam prioridade mais alta. Uma pilha mínima também é útil; por exemplo, ao recuperar itens com chaves inteiras em ordem crescente ou cadeias em ordem alfabética. Os princípios são exatamente os mesmos; basta mudar a ordem de classificação.
A propriedade heap especifica que cada nó em um heap binário deve ser pelo menos tão grande quanto os dois filhos. Em particular, isso implica que o maior item do heap está na raiz. Peneirar e peneirar são essencialmente a mesma operação em direções opostas: mova um nó ofensivo até satisfazer a propriedade heap:
siftDown
troca um nó que é muito pequeno com seu maior filho (movendo-o para baixo) até que ele seja pelo menos tão grande quanto os dois nós abaixo dele.siftUp
troca um nó que é muito grande com seu pai (movendo-o para cima) até que não seja maior que o nó acima dele.O número de operações necessárias
siftDown
esiftUp
é proporcional à distância que o nó pode ter que se mover. PoissiftDown
, é a distância até o fundo da árvore, por issosiftDown
é caro para os nós no topo da árvore. ComsiftUp
, o trabalho é proporcional à distância até o topo da árvore, por issosiftUp
é caro para os nós na parte inferior da árvore. Embora ambas as operações sejam O (log n) no pior caso, em um heap, apenas um nó está no topo, enquanto metade dos nós está na camada inferior. assim ele não deve ser muito surpreendente que, se temos de aplicar uma operação para cada nó, nós preferimossiftDown
maissiftUp
.A
buildHeap
função pega uma matriz de itens não classificados e os move até que todos satisfaçam a propriedade heap, produzindo um heap válido. Existem duas abordagens que você podebuildHeap
usar para usar as operaçõessiftUp
esiftDown
que descrevemos.Comece na parte superior da pilha (o início da matriz) e chame
siftUp
cada item. Em cada etapa, os itens peneirados anteriormente (os itens anteriores ao item atual na matriz) formam um heap válido, e a peneiração do próximo item o coloca em uma posição válida no heap. Após selecionar cada nó, todos os itens atendem à propriedade heap.Ou siga na direção oposta: comece no final da matriz e mova-se para trás em direção à frente. A cada iteração, você peneira um item até que ele esteja no local correto.
Qual implementação
buildHeap
é mais eficiente?Ambas as soluções produzirão um heap válido. Sem surpresa, a mais eficiente é a segunda operação que utiliza
siftDown
.Deixe h = log n representar a altura da pilha. O trabalho necessário para a
siftDown
abordagem é dado pela somaCada termo na soma tem a distância máxima que um nó na altura especificada terá que se mover (zero para a camada inferior, h para a raiz) multiplicado pelo número de nós nessa altura. Por outro lado, a soma para chamar
siftUp
cada nó éDeve ficar claro que a segunda soma é maior. O primeiro termo por si só é hn / 2 = 1/2 n log n , portanto, essa abordagem tem complexidade na melhor das hipóteses O (n log n) .
Como provamos que a soma da
siftDown
abordagem é realmente O (n) ?Um método (existem outras análises que também funcionam) é transformar a soma finita em uma série infinita e depois usar a série de Taylor. Podemos ignorar o primeiro termo, que é zero:
Se você não sabe ao certo por que cada uma dessas etapas funciona, eis uma justificativa para o processo em palavras:
Como a soma infinita é exatamente n , concluímos que a soma finita não é maior e, portanto, é O (n) .
Por que a classificação de heap requer tempo O (n log n) ?
Se é possível executar
buildHeap
em tempo linear, por que a classificação de heap requer tempo O (n log n) ? Bem, a classificação de heap consiste em dois estágios. Primeiro, chamamosbuildHeap
a matriz, que requer tempo O (n) se implementada de maneira ideal. O próximo estágio é excluir repetidamente o maior item da pilha e colocá-lo no final da matriz. Como excluímos um item do heap, sempre há um ponto aberto logo após o final do heap, onde podemos armazenar o item. Portanto, a classificação da pilha atinge uma ordem classificada, removendo sucessivamente o próximo item maior e colocando-o na matriz, começando na última posição e movendo-se para a frente. É a complexidade desta última parte que domina na classificação da pilha. O loop fica assim:Claramente, o loop executa O (n) vezes ( n - 1 para ser mais preciso, o último item já está no lugar). A complexidade de
deleteMax
um heap é O (log n) . Geralmente, é implementado removendo a raiz (o maior item deixado na pilha) e substituindo-a pelo último item da pilha, que é uma folha e, portanto, um dos itens menores. Essa nova raiz quase certamente violará a propriedade heap, portanto, você deve ligarsiftDown
até movê-la de volta para uma posição aceitável. Isso também tem o efeito de mover o próximo item maior para a raiz. Observe que, ao contrário dobuildHeap
local para a maioria dos nós que estamos chamando Embora a árvore esteja encolhendo, ela não encolhe rápido o suficientesiftDown
da parte inferior da árvore, agora estamos chamandosiftDown
do topo da árvore em cada iteração!: A altura da árvore permanece constante até você remover a primeira metade dos nós (quando você limpa a camada inferior completamente). Então, para o próximo trimestre, a altura é h - 1 . Portanto, o trabalho total para esta segunda etapa éObserve a opção: agora o caso de trabalho zero corresponde a um único nó e o caso de trabalho h corresponde à metade dos nós. Essa soma é O (n log n), assim como a versão ineficiente do
buildHeap
disso é implementada usando o siftUp. Mas, neste caso, não temos escolha, pois estamos tentando classificar e exigimos que o próximo maior item seja removido em seguida.Em resumo, o trabalho para classificação de heap é a soma dos dois estágios: O (n) tempo para buildHeap e O (n log n) para remover cada nó em ordem , portanto, a complexidade é O (n log n) . Você pode provar (usando algumas idéias da teoria da informação) que, para uma classificação baseada em comparação, O (n log n) é o melhor que você poderia esperar de qualquer maneira; portanto, não há razão para se decepcionar com isso ou esperar que a classificação da pilha atinja o O (n) tempo limite que
buildHeap
sim.fonte
siftUp
cada item, ou começa no final, avançando e chamandosiftDown
. Independentemente da abordagem escolhida, você está selecionando o próximo item na parte não classificada da matriz e executando a operação apropriada para movê-lo para uma posição válida na parte ordenada da matriz. A única diferença é o desempenho.Sua análise está correta. No entanto, não é apertado.
Não é realmente fácil explicar por que construir uma pilha é uma operação linear; você deve lê-la melhor.
Uma ótima análise do algoritmo pode ser vista aqui .
A idéia principal é que no
build_heap
algoritmo oheapify
custo real não éO(log n)
para todos os elementos.Quando
heapify
é chamado, o tempo de execução depende da distância que um elemento pode mover para baixo na árvore antes que o processo termine. Em outras palavras, isso depende da altura do elemento na pilha. Na pior das hipóteses, o elemento pode descer até o nível da folha.Vamos contar o trabalho realizado, nível por nível.
No nível mais baixo, existem
2^(h)
nós, mas não chamamosheapify
nenhum deles, portanto o trabalho é 0. No nível seguinte, existem2^(h − 1)
nós, e cada um pode descer 1 nível. No terceiro nível, a partir da parte inferior, existem2^(h − 2)
nós, e cada um pode descer 2 níveis.Como você pode ver, nem todas as operações de heapify são
O(log n)
, é por isso que você está obtendoO(n)
.fonte
Heapify
éO(n)
quando termina comsiftDown
masO(n log n)
quando termina comsiftUp
. A classificação real (extrair itens do heap um por um) deve ser feita com osiftUp
mesmo, portantoO(n log n)
.i
partir da parte inferior de uma árvore de altura h devem também fazer2* log(h-i)
comparações e devem ser consideradas também @ The111. O que você acha?Intuitivamente:
Nem tanto. Sua lógica não produz um limite estreito - ela superestima a complexidade de cada heapify. Se construída de baixo para cima, a inserção (heapify) pode ser muito menor que
O(log(n))
. O processo é como se segue:(Etapa 1) Os primeiros
n/2
elementos vão para a linha inferior da pilha.h=0
, portanto, o heapify não é necessário.(Etapa 2) Os próximos elementos vão na linha 1 acima da parte inferior. , empilhe os filtros 1 nível abaixo.
n/22
h=1
(Etapa i ) Os próximos elementos são alinhados de baixo para cima. , empilhe os níveis dos filtros para baixo.
n/2i
i
h=i
i
( Log da etapa (n) ) O último elemento entra na linha acima da parte inferior. , empilhe os níveis dos filtros para baixo.
n/2log2(n) = 1
log(n)
h=log(n)
log(n)
AVISO: após a etapa um,
1/2
os elementos(n/2)
já estão no heap e nem precisamos chamar heapify uma vez. Além disso, observe que apenas um único elemento, a raiz, incorre em toda alog(n)
complexidade.Teoricamente:
As etapas totais
N
para criar um monte de tamanhon
podem ser gravadas matematicamente.Em altura
i
, mostramos (acima) que haverá elementos que precisam chamar heapify, e sabemos que heapify em altura é . Isto dá:n/2i+1
i
O(i)
A solução para o último somatório pode ser encontrada tomando a derivada de ambos os lados da bem conhecida equação de série geométrica:
Finalmente, a conexão com
x = 1/2
a equação acima rende2
. Conectar isso à primeira equação fornece:Assim, o número total de etapas é de tamanho
O(n)
fonte
Seria O (n log n) se você construísse o heap inserindo elementos repetidamente. No entanto, você pode criar um novo heap com mais eficiência, inserindo os elementos em ordem arbitrária e aplicando um algoritmo para "heapificá-los" na ordem correta (dependendo do tipo de heap, é claro).
Veja http://en.wikipedia.org/wiki/Binary_heap , "Criando uma pilha" para um exemplo. Nesse caso, você trabalha essencialmente a partir do nível inferior da árvore, trocando os nós pai e filho até que as condições da pilha sejam atendidas.
fonte
Já existem ótimas respostas, mas eu gostaria de adicionar uma pequena explicação visual
Agora, dê uma olhada na imagem, existem
n/2^1
nós verdes com altura 0 (aqui 23/2 = 12)n/2^2
nós vermelhos com altura 1 (aqui 23/4 = 6)n/2^3
nó azul com altura 2 (aqui 23/8 = 3)n/2^4
nós roxos com altura 3 (aqui 23/16 = 2)para que haja
n/2^(h+1)
nós para altura hPara encontrar a complexidade do tempo, conte a quantidade de trabalho realizado ou o número máximo de iterações executadas por cada nó
agora pode-se notar que cada nó pode executar (no máximo) iterações == altura do nó
portanto, para qualquer nó com altura h, o trabalho máximo realizado é n / 2 ^ (h + 1) * h
Agora, o trabalho total realizado é
agora para qualquer valor de h , a sequência
nunca excederá 1
Portanto, a complexidade do tempo nunca excederá O (n) para construir heap
fonte
Como sabemos que a altura de um heap é log (n) , onde n é o número total de elementos.Vamos representá-lo como h
Quando executamos uma operação de heapify, os elementos no último nível ( h ) não se moverão nem um único degrau.
O número de elementos no segundo nível anterior ( h-1 ) é 2 h-1 e eles podem se mover no nível máximo 1 (durante o heapify).
Da mesma forma, para o i th , nível, temos 2 i elementos que podem se mover oi posições.
Portanto, número total de movimentos = S = 2 h * 0 + 2 h-1 * 1 + 2 h-2 * 2 + ... 2 0 * h
S = 2 h {1/2 + 2/2 2 + 3/2 3 + ... h / 2 h } ----------------------- -------------------------- 1
esta é a série AGP , para resolver essa divisão dos dois lados por 2
S / 2 = 2 h {1/2 2 + 2/2 3 + ... h / 2 h + 1 } --------------------------------- ---------------- 2
subtraindo a equação 2 de 1, obtém
S / 2 = 2 h { 1/2 + 1/2 2 + 1/2 3 + ... + 1 / 2 h + h / 2 h + 1 }
S = 2 h + 1 {1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h + h / 2 h + 1 }
agora 1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h está diminuindo GP cuja soma é menor que 1 (quando h tende ao infinito, a soma tende a 1). Em uma análise mais aprofundada, vamos usar um limite superior na soma que é 1.
Isso dá S = 2 h + 1 {1 + h / 2 h + 1 }
= 2 h + 1 + h
~ 2 h + h
como h = log (n) , 2 h = n
Portanto S = n + log (n)
T (C) = O (n)
fonte
Ao criar um monte, digamos que você esteja adotando uma abordagem ascendente.
fonte
No caso de construir a pilha, começamos a partir da altura, logn -1 (onde logn é a altura da árvore de n elementos). Para cada elemento presente na altura 'h', subimos no máximo até (logn -h).
fonte
Inserções sucessivas podem ser descritas por:
Por aproximação de estorninho
n! =~ O(n^(n + O(1)))
, portanto,T =~ O(nlog(n))
Espero que isso ajude, a maneira ideal
O(n)
é usar o algoritmo de pilha de compilação para um determinado conjunto (a ordenação não importa).fonte
Basicamente, o trabalho é feito apenas em nós que não são folhas durante a construção de um heap ... e o trabalho realizado é a quantidade de trocas para satisfazer a condição do heap ... em outras palavras (no pior caso), o valor é proporcional à altura do nó ... apesar de toda a complexidade do problema, é proporcional à soma das alturas de todos os nós não foliares .. que é (2 ^ h + 1 - 1) -h-1 = nh-1 = Em)
fonte
O @bcorso já demonstrou a prova da análise de complexidade. Mas, para quem ainda está aprendendo a análise da complexidade, tenho que acrescentar:
A base do seu erro original se deve a uma interpretação incorreta do significado da instrução "a inserção em um heap leva tempo O (log n)". A inserção em um heap é realmente O (log n), mas é necessário reconhecer que n é o tamanho do heap durante a inserção .
No contexto de inserção de n objetos em um heap, a complexidade da i-ésima inserção é O (log n_i), em que n_i é o tamanho do heap como na inserção i. Somente a última inserção tem uma complexidade de O (log n).
fonte
Vamos supor que você tenha N elementos em um heap. Então sua altura seria Log (N)
Agora você deseja inserir outro elemento, então a complexidade seria: Log (N) , temos que comparar todo o caminho UP à raiz.
Agora você está tendo N + 1 elementos & height = Log (N + 1)
Usando a técnica de indução , pode-se provar que a complexidade da inserção seria ∑logi .
Agora usando
Isso simplifica para: logi = log (n!)
que na verdade é O (NlogN)
Mas
estamos fazendo algo errado aqui, como em todos os casos em que não alcançamos o topo. Portanto, durante a execução na maioria das vezes, podemos descobrir que não estamos indo nem pela metade da árvore. Portanto, esse limite pode ser otimizado para ter outro limite mais apertado usando a matemática fornecida nas respostas acima.
Essa percepção veio a mim depois de um detalhe e experimentação no Heaps.
fonte
Eu realmente gosto da explicação de Jeremy West .... outra abordagem realmente fácil de entender é dada aqui http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity
Como o buildheap depende do uso, depende da heapify e é usada a abordagem shiftdown, que depende da soma das alturas de todos os nós. Então, para encontrar a soma da altura dos nós que é dada por S = somatório de i = 0 a i = h de (2 ^ i * (oi)), onde h = logn é a altura da árvore que resolve s, obtemos s = 2 ^ (h + 1) - 1 - (h + 1), uma vez que n = 2 ^ (h + 1) - 1 s = n - h - 1 = n- logn - 1 s = O (n), e, portanto, a complexidade do buildheap é O (n).
fonte
"O limite linear de tempo do build Heap pode ser mostrado calculando a soma das alturas de todos os nós no heap, que é o número máximo de linhas tracejadas. Para a árvore binária perfeita da altura h contendo N = 2 ^ ( h + 1) - 1 nós, a soma das alturas dos nós é N - H - 1. Portanto, é O (N). "
fonte
Prova de O (n)
A prova não é chique, e bem direta, eu só provei o caso de uma árvore binária completa; o resultado pode ser generalizado para uma árvore binária completa.
fonte
Obtemos o tempo de execução da construção do heap, descobrindo a movimentação máxima que cada nó pode executar. Portanto, precisamos saber quantos nós estão em cada linha e a que distância cada nó pode ir.
Começando no nó raiz, cada linha seguinte tem o dobro dos nós que a linha anterior, portanto, respondendo com que frequência podemos duplicar o número de nós até não termos mais nós, obtemos a altura da árvore. Ou, em termos matemáticos, a altura da árvore é log2 (n), sendo n o comprimento da matriz.
Para calcular os nós em uma linha que começamos pela parte de trás, sabemos que n / 2 estão na parte inferior; portanto, ao dividir por 2, obtemos a linha anterior e assim por diante.
Com base nisso, obtemos esta fórmula para a abordagem Siftdown: (0 * n / 2) + (1 * n / 4) + (2 * n / 8) + ... + (log2 (n) * 1)
O termo na última parêntese é a altura da árvore multiplicada pelo nó que está na raiz; o termo na primeira parêntese é todos os nós na linha inferior multiplicados pelo comprimento que podem percorrer, 0. A mesma fórmula no smart:
Trazendo o n de volta, temos 2 * n, 2 pode ser descartado porque é uma constante e tada, temos o pior tempo de execução da abordagem Siftdown: n.
fonte
pense que você está cometendo um erro. Dê uma olhada no seguinte: http://golang.org/pkg/container/heap/ Construir um heap não é O (n). No entanto, a inserção é O (lg (n). Suponho que a inicialização seja O (n) se você definir um tamanho de heap b / c, o heap precisará alocar espaço e configurar a estrutura de dados. Se você tiver n itens para colocar no heap, sim, cada inserção é lg (n) e há n itens, então você obtém n * lg (n) conforme indicado
fonte