Já faz um tempo desde aqueles anos escolares. Consegui um emprego como especialista em TI em um hospital. Tentando fazer alguma programação real agora. Estou trabalhando em árvores binárias agora e gostaria de saber qual seria a melhor maneira de determinar se a árvore tem equilíbrio de altura.
Eu estava pensando em algo sobre isso:
public boolean isBalanced(Node root){
if(root==null){
return true; //tree is empty
}
else{
int lh = root.left.height();
int rh = root.right.height();
if(lh - rh > 1 || rh - lh > 1){
return false;
}
}
return true;
}
Esta é uma boa implementação? Ou eu estou esquecendo de alguma coisa?
java
algorithm
data-structures
binary-tree
user69514
fonte
fonte
Respostas:
Tropecei nesta velha questão enquanto procurava por outra coisa. Percebi que você nunca obteve uma resposta completa.
A maneira de resolver esse problema é começar escrevendo uma especificação para a função que você está tentando escrever.
Especificação: Uma árvore binária bem formada é considerada "balanceada em altura" se (1) estiver vazia, ou (2) seus filhos esquerdo e direito estiverem balanceados em altura e a altura da árvore esquerda estiver dentro de 1 do altura da árvore certa.
Agora que você tem a especificação, o código é fácil de escrever. Basta seguir as especificações:
Traduzir isso para a linguagem de programação de sua escolha deve ser trivial.
Exercício bônus : este esboço de código ingênuo atravessa a árvore muitas vezes ao calcular as alturas. Você pode torná-lo mais eficiente?
Exercício de super bônus : suponha que a árvore esteja extremamente desequilibrada. Tipo, um milhão de nós de profundidade de um lado e três de profundidade do outro. Existe um cenário em que este algoritmo explode a pilha? Você pode consertar a implementação para que ela nunca estrague a pilha, mesmo quando dada uma árvore extremamente desequilibrada?
ATUALIZAÇÃO : Donal Fellows aponta em sua resposta que existem diferentes definições de 'equilibrado' que se pode escolher. Por exemplo, pode-se tomar uma definição mais estrita de "altura equilibrada" e exigir que o comprimento do caminho até a criança vazia mais próxima esteja dentro de um do caminho até a criança vazia mais distante . Minha definição é menos rígida do que essa e, portanto, admite mais árvores.
Alguém também pode ser menos rígido do que minha definição; pode-se dizer que uma árvore balanceada é aquela em que o comprimento máximo do caminho para uma árvore vazia em cada galho difere em não mais do que dois, ou três, ou alguma outra constante. Ou que o comprimento máximo do caminho é uma fração do comprimento mínimo do caminho, como metade ou um quarto.
Geralmente não importa. O objetivo de qualquer algoritmo de balanceamento de árvore é garantir que você não acabe na situação em que tenha um milhão de nós de um lado e três do outro. A definição de Donal é boa em teoria, mas na prática é uma dor chegar com um algoritmo de balanceamento de árvore que atenda a esse nível de rigidez. A economia de desempenho geralmente não justifica o custo de implementação. Você gasta muito tempo fazendo rearranjos desnecessários de árvores para atingir um nível de equilíbrio que na prática faz pouca diferença. Quem se importa se às vezes são necessários quarenta galhos para chegar à folha mais distante em uma árvore de um milhão de nós mal balanceada quando, em teoria, seriam necessários apenas vinte em uma árvore perfeitamente balanceada? A questão é que nunca leva um milhão. Passar do pior caso de um milhão para o pior caso de quarenta geralmente é suficiente; você não precisa ir até o caso ideal.
fonte
O equilíbrio é uma propriedade realmente sutil; você acha que sabe o que é, mas é tão fácil errar. Em particular, até a resposta (boa) de Eric Lippert está errada. Isso porque a noção de altura não basta. Você precisa ter o conceito de alturas mínimas e máximas de uma árvore (onde a altura mínima é o menor número de passos da raiz à folha, e o máximo é ... bem, você entendeu). Dado isso, podemos definir o equilíbrio como:
(Na verdade, isso implica que os ramos estão equilibrados; você pode escolher o mesmo ramo para máximo e mínimo.)
Tudo o que você precisa fazer para verificar essa propriedade é uma simples travessia da árvore mantendo o controle da profundidade atual. A primeira vez que você volta atrás, isso lhe dá uma profundidade de linha de base. Cada vez depois disso, quando você voltar atrás, você compara a nova profundidade com a linha de base
Em código:
Suponho que você possa fazer isso sem usar o padrão Observer, mas acho mais fácil raciocinar dessa forma.
[EDIT]: Por que você não pode simplesmente medir a altura de cada lado. Considere esta árvore:
OK, um confuso pouco, mas cada lado da raiz é equilibrado:
C
é profundidade 2,A
,B
,D
,E
estão profundidade 3, eF
,G
,H
,J
estão profundidade 4. A altura do ramo esquerdo é 2 (lembre-se a altura diminui à medida que atravessa o galho), a altura do galho direito é 3. No entanto, a árvore em geral não está equilibrada, pois há uma diferença de altura de 2 entreC
eF
. Você precisa de uma especificação minimax (embora o algoritmo real possa ser menos complexo, pois deve haver apenas duas alturas permitidas).fonte
Isso apenas determina se o nível superior da árvore está equilibrado. Ou seja, você poderia ter uma árvore com dois longos galhos na extrema esquerda e na extrema direita, sem nada no meio, e isso retornaria verdadeiro. Você precisa verificar recursivamente
root.left
eroot.right
para ver se eles estão balanceados internamente antes de retornar true.fonte
Resposta do exercício bônus. A solução simples. Obviamente, em uma implementação real, pode-se envolver isso ou algo assim para evitar que o usuário inclua altura em sua resposta.
fonte
out height
notação de variável " ")Solução de pós-pedido, atravesse a árvore apenas uma vez. A complexidade do tempo é O (n), o espaço é O (1), é melhor do que a solução de cima para baixo. Eu te dou uma implementação da versão java.
fonte
left == -1
significa? Quando seria esse o caso? Presumimos que a chamada recursiva implica que issoleft == -1
é verdadeiro se todas as subárvores dos filhos à esquerda estiverem desequilibradas?left == 1
significa que a subárvore esquerda está desequilibrada, então a árvore inteira está desequilibrada. Não precisamos mais verificar a subárvore certa e podemos retornar-1
.A definição de uma árvore binária de altura balanceada é:
Portanto, uma árvore binária vazia é sempre equilibrada em altura.
Uma árvore binária não vazia tem equilíbrio de altura se:
Considere a árvore:
Como visto, a subárvore esquerda de
A
é equilibrada em altura (pois está vazia) e também sua subárvore direita. Mas ainda assim a árvore não está equilibrada em altura, pois a condição 3 não é atendida como a altura da subárvore esquerda0
e a altura da subárvore direita2
.Além disso, a árvore a seguir não está equilibrada em altura, embora as alturas das subárvores esquerda e direita sejam iguais. Seu código existente retornará verdadeiro para ele.
Portanto, a palavra cada na definição é muito importante.
Isso vai funcionar:
Link Ideone
fonte
Se a árvore binária está balanceada ou não, pode ser verificada pela passagem de ordem de nível:
fonte
Isso está se tornando muito mais complicado do que realmente é.
O algoritmo é o seguinte:
Seja B = profundidade do nó de nível mais baixo
Se abs (AB) <= 1, então a árvore está equilibrada
fonte
O que significa equilibrado depende um pouco da estrutura em questão. Por exemplo, A B-Tree não pode ter nós mais do que uma certa profundidade da raiz, ou menos, todos os dados vivem em uma profundidade fixa da raiz, mas pode estar desequilibrado se a distribuição de folhas para folhas -mas-um nó é irregular. Listas de omissão Não tenha nenhuma noção de equilíbrio, confiando, em vez disso, na probabilidade de alcançar um desempenho decente. Árvores Fibonacci propositalmente caem fora de equilíbrio, adiando o reequilíbrio para alcançar desempenho assintótico superior em troca de atualizações ocasionalmente mais longas. Árvores AVL e Red-Black anexam metadados a cada nó para atingir um invariante de equilíbrio de profundidade.
Todas essas estruturas e mais estão presentes nas bibliotecas padrão dos sistemas de programação mais comuns (exceto python, RAGE!). Implementar um ou dois é uma boa prática de programação, mas provavelmente não é um bom uso de tempo para lançar o seu próprio para produção, a menos que seu problema tenha algum desempenho peculiar que não precise ser satisfeito por nenhuma coleção pronta para uso.
fonte
Nota 1: A altura de qualquer subárvore é calculada apenas uma vez.
Nota 2: Se a subárvore esquerda estiver desequilibrada, o cálculo da subárvore direita, potencialmente contendo milhões de elementos, é ignorado.
fonte
O equilíbrio geralmente depende do comprimento do caminho mais longo em cada direção. O algoritmo acima não fará isso por você.
O que você está tentando implementar? Existem árvores de equilíbrio automático ao redor (AVL / Red-black). Na verdade, as árvores Java são equilibradas.
fonte
Se for para o seu trabalho , sugiro:
fonte
fonte
1
(o mesmo para minDepth). A profundidade correta, porém, deve ser0
. A raiz de uma árvore sempre tem0
profundidadeAqui está uma solução testada e trabalhada completa em C # (desculpe, não sou dev java) (basta copiar e colar no aplicativo de console). Eu sei que a definição de balanceado varia, então nem todos podem gostar dos resultados do meu teste, mas por favor, basta olhar para a abordagem ligeiramente diferente de verificar a profundidade / altura em um loop recursivo e sair na primeira incompatibilidade sem salvar a altura / nível / profundidade do nó em cada nó (apenas mantendo-o em uma chamada de função).
fonte
fonte
RE: a solução de @ lucky usando um BFS para fazer uma travessia de ordem de nível.
Percorremos a árvore e mantemos uma referência a vars min / max-level que descreve o nível mínimo em que um nó é uma folha.
Eu acredito que a solução @lucky requer uma modificação. Como sugerido por @codaddict, em vez de verificar se um nó é uma folha, devemos verificar se O filho esquerdo ou direito é nulo (não ambos). Caso contrário, o algoritmo consideraria esta uma árvore balanceada válida:
Em Python:
Esta solução deve satisfazer todas as estipulações fornecidas na questão inicial, operando em tempo O (n) e espaço O (n). O estouro de memória seria direcionado para o heap em vez de explodir uma pilha de chamadas recursiva.
Alternativamente, poderíamos percorrer inicialmente a árvore para computar + cache máximas alturas para cada subárvore raiz iterativamente. Em seguida, em outra execução iterativa, verifique se as alturas em cache das subárvores esquerda e direita de cada raiz nunca diferem em mais de um. Isso também seria executado no tempo O (n) e no espaço O (n), mas de forma iterativa para não causar estouro de pilha.
fonte
Bem, você precisa encontrar uma maneira de determinar as alturas da esquerda e da direita e se a esquerda e a direita estão equilibradas.
E eu apenas
return height(node->left) == height(node->right);
Quanto a escrever uma
height
função, leia: Noções básicas sobre recursãofonte
De que tipo de árvore você está falando? Existem árvores com equilíbrio próprio lá fora. Verifique seus algoritmos onde eles determinam se precisam reordenar a árvore para manter o equilíbrio.
fonte
Aqui está uma versão baseada em uma travessia de profundidade genérica. Deve ser mais rápido do que a outra resposta correta e lidar com todos os "desafios" mencionados. Desculpas pelo estilo, eu realmente não conheço Java.
Você ainda pode torná-lo muito mais rápido retornando mais cedo, se máximo e mínimo estiverem definidos e tiverem uma diferença> 1.
fonte
fonte
fonte
Aqui está o que eu tentei para o exercício bônus de Eric. Tento desenrolar meus loops recursivos e retornar assim que descobrir que uma subárvore não está equilibrada.
fonte
fonte
Uma árvore vazia é equilibrada em altura. Uma árvore binária não vazia T é balanceada se:
1) A subárvore esquerda de T é balanceada
2) A subárvore direita de T está equilibrada
3) A diferença entre as alturas da subárvore esquerda e da subárvore direita não é superior a 1.
Complexidade de tempo: O (n)
fonte
Para ter um melhor desempenho, especialmente em árvores enormes, você pode economizar a altura em cada nó, por isso é uma troca espaço x desempenho:
Exemplo de implementação da adição e mesmo para exclusão
fonte
fonte
Não funcionaria?
Qualquer árvore desequilibrada sempre falharia nisso.
fonte