A Árvore Binária aqui pode não ser necessariamente uma Árvore de Pesquisa Binária.
A estrutura pode ser tomada como -
struct node {
int data;
struct node *left;
struct node *right;
};
A solução máxima que eu consegui encontrar com um amigo foi algo desse tipo -
considere esta árvore binária :
O percurso transversal da ordem produz - 8, 4, 9, 2, 5, 1, 6, 3, 7
E a passagem pós-ordem produz - 8, 9, 4, 5, 2, 6, 7, 3, 1
Por exemplo, se queremos encontrar o ancestral comum dos nós 8 e 5, fazemos uma lista de todos os nós que estão entre 8 e 5 na travessia da árvore de ordem de entrada, que neste caso é [4, 9 2] Em seguida, verificamos qual nó nesta lista aparece por último no percurso da ordem posterior, que é 2. Portanto, o ancestral comum de 8 e 5 é 2.
A complexidade desse algoritmo, creio, é O (n) (O (n) para travessias dentro / fora da ordem, o restante das etapas sendo novamente O (n), pois elas nada mais são do que simples iterações em matrizes). Mas há uma forte chance de que isso esteja errado. :-)
Mas essa é uma abordagem muito grosseira, e não tenho certeza se ela se quebra em alguns casos. Existe alguma outra solução (possivelmente mais ideal) para esse problema?
Respostas:
Nick Johnson está certo de que um algoritmo de complexidade de tempo O (n) é o melhor que você pode fazer se não tiver ponteiros pai.) Para uma versão recursiva simples desse algoritmo, veja o código na publicação de Kinding, que é executada em tempo O (n) .
Mas lembre-se de que, se seus nós tiverem ponteiros pai, um algoritmo aprimorado será possível. Para os dois nós em questão, construa uma lista contendo o caminho da raiz para o nó, iniciando no nó e inserindo o pai com uma frente.
Portanto, no exemplo 8, você obtém (mostrando as etapas): {4}, {2, 4}, {1, 2, 4}
Faça o mesmo para o outro nó em questão, resultando em (etapas não mostradas): {1, 2}
Agora compare as duas listas criadas procurando o primeiro elemento em que a lista difere ou o último elemento de uma das listas, o que ocorrer primeiro.
Este algoritmo requer tempo O (h) em que h é a altura da árvore. No pior caso, O (h) é equivalente a O (n), mas se a árvore estiver equilibrada, isso é apenas O (log (n)). Também requer espaço O (h). É possível uma versão aprimorada que utilize apenas espaço constante, com o código mostrado no post do CEGRD
Independentemente de como a árvore é construída, se esta for uma operação que você executa muitas vezes na árvore sem alterá-la, existem outros algoritmos que você pode usar que requerem preparação do tempo O (n) [linear], mas depois encontram qualquer par leva apenas tempo O (1) [constante]. Para referências a esses algoritmos, consulte a página com problemas mais baixos de ancestrais comuns na Wikipedia . (Agradecemos a Jason por postar originalmente este link)
fonte
O(h)
é apenasO(log(n))
se a árvore estiver equilibrada. Para qualquer árvore, seja binária ou não, se você tiver ponteiros pai, poderá determinar o caminho de uma folha para a raiz noO(h)
tempo, simplesmente seguindo o ponteiro pai váriash
vezes. Isso fornece o caminho da folha para a raiz. Se os caminhos forem armazenados como uma pilha, a iteração da pilha fornecerá o caminho da raiz para a folha. Se você não possui ponteiros pai e não possui uma estrutura especial para a árvore, encontrar o caminho da raiz para a folha levaO(n)
tempo.Iniciando a partir do
root
nó e movendo-se para baixo, se você encontrar algum nó que tenhap
ouq
como filho direto, é o LCA. (editar - este deve ser sep
ouq
for o valor do nó, retorne-o. Caso contrário, falhará quando um dep
ouq
for um filho direto do outro.)Caso
p
contrário, se você encontrar um nó na subárvore direita (ou esquerda) eq
na subárvore esquerda (ou direita), será o LCA.O código fixo se parece com:
O código abaixo falha quando um deles é filho direto de outro.
Código em ação
fonte
Aqui está o código de trabalho em JAVA
fonte
As respostas dadas até agora usam recursão ou armazenam, por exemplo, um caminho na memória.
Ambas as abordagens podem falhar se você tiver uma árvore muito profunda.
Aqui está minha opinião sobre essa questão. Quando verificamos a profundidade (distância da raiz) de ambos os nós, se forem iguais, podemos mover com segurança para cima dos dois nós em direção ao ancestral comum. Se uma das profundidades for maior, devemos mover para cima a partir do nó mais profundo enquanto permanecermos no outro.
Aqui está o código:
A complexidade do tempo desse algoritmo é: O (n). A complexidade do espaço desse algoritmo é: O (1).
Em relação ao cálculo da profundidade, podemos primeiro lembrar a definição: Se v é raiz, profundidade (v) = 0; Caso contrário, profundidade (v) = profundidade (pai (v)) + 1. Podemos calcular a profundidade da seguinte forma:
fonte
Bem, isso depende da estrutura da sua árvore binária. Presumivelmente, você tem alguma maneira de encontrar o nó da folha desejado, dada a raiz da árvore - basta aplicar isso a ambos os valores até que os galhos escolhidos divergam.
Se você não tem como encontrar a folha desejada, dada a raiz, sua única solução - tanto na operação normal quanto no último nó comum - é uma busca de força bruta na árvore.
fonte
Isso pode ser encontrado em: - http://goursaha.freeoda.com/DataStructure/LowestCommonAncestor.html
fonte
O algoritmo de ancestrais menos comuns off-line de Tarjan é bom o suficiente (cf. também Wikipedia ). Há mais sobre o problema (o menor problema ancestral comum) na Wikipedia .
fonte
Para descobrir o ancestral comum de dois nós: -
Isso funcionaria para a árvore de pesquisa binária.
fonte
Fiz uma tentativa com imagens ilustrativas e código de trabalho em Java,
http://tech.bragboy.com/2010/02/least-common-ancestor-without-using.html
fonte
O algoritmo recursivo abaixo será executado em O (log N) para uma árvore binária balanceada. Se um dos nós passados para a função getLCA () for o mesmo que a raiz, a raiz será o LCA e não haverá necessidade de executar nenhuma recussão.
Casos de teste. [1] Ambos os nós n1 e n2 estão na árvore e residem em ambos os lados do nó pai. [2] O nó n1 ou n2 é a raiz, o LCA é a raiz. [3] Somente n1 ou n2 está na árvore, o LCA será o nó raiz da subárvore esquerda da raiz da árvore ou o LCA será o nó raiz da subárvore direita da raiz da árvore.
[4] Nem n1 nem n2 estão na árvore, não há ACV. [5] Ambos n1 e n2 estão em uma linha reta um ao lado do outro, o LCA será um dos n1 ou n2 que está sempre próximo à raiz da árvore.
fonte
Apenas desça da árvore inteira,
root
desde que os dois nós, digamos,p
eq
para os quais o Ancestor seja encontrado, estejam na mesma subárvore (o que significa que seus valores são menores ou maiores que os da raiz).Isso caminha diretamente da raiz para o menos ancestral comum, sem olhar para o resto da árvore, por isso é praticamente o mais rápido possível. Algumas maneiras de fazer isso.
em caso de estouro, eu faria (root.val - (long) p.val) * (root.val - (long) q.val)
fonte
fonte
Considere esta árvore
Se fizermos a travessia pós-encomenda e pré-encomenda e encontrarmos o primeiro predecessor e sucessor comum, obteremos o ancestral comum.
pós-encomenda => 0,2,1,5,4,6,3,8,10,11,9,14,15,13,12,7 pré-encomenda => 7,3,1,0,2,6,4 5,12,9,8,11,10,13,15,14
Ancestral menos comum de 8,11
em pós-encomenda temos => 9,14,15,13,12,7 após 8 e 11 em pré-encomenda temos => 7,3,1,0,2,6,4,5,12,9 antes de 8 e 11
9 é o primeiro número comum que ocorre após 8 e 11 na pós-encomenda e antes de 8 e 11 na pré-encomenda, portanto 9 é a resposta
Menos ancestral comum de 5,10
11,9,14,15,13,12,7 na pré-encomenda 7,3,1,0,2,6,4 na pré-encomenda
7 é o primeiro número que ocorre após 5,10 na pós-encomenda e antes de 5,10 na pré-encomenda, portanto 7 é a resposta
fonte
Se for uma árvore binária completa com filhos do nó x como 2 * x e 2 * x + 1, haverá uma maneira mais rápida de fazê-lo.
Como funciona
Isso funciona porque basicamente divide o número maior por dois recursivamente até que ambos os números sejam iguais. Esse número é o ancestral comum. Dividir é efetivamente a operação de turno certo. Então, precisamos encontrar um prefixo comum de dois números para encontrar o ancestral mais próximo
fonte
No scala, você pode:
fonte
fonte
Aqui está a maneira C ++ de fazer isso. Tentaram manter o algoritmo o mais fácil possível para entender:
Como usá-lo:
fonte
A maneira mais fácil de encontrar o antepassado comum mais baixo é usando o seguinte algoritmo:
fonte
Eu encontrei uma solução
Dependendo de três percursos, você pode decidir quem é o LCA. No LCA, encontre a distância dos dois nós. Adicione essas duas distâncias, que é a resposta.
fonte
Aqui está o que eu penso,
Complexidade: passo 1: O (n), passo 2 = ~ O (n), total = ~ O (n).
fonte
Aqui estão duas abordagens em c # (.net) (ambas discutidas acima) para referência:
Versão recursiva de localização de LCA na árvore binária (O (N) - como no máximo cada nó é visitado) (os principais pontos da solução é LCA é (a) único nó na árvore binária em que ambos os elementos residem nos dois lados das subárvores (esquerda e à direita) é LCA. (b) E também não importa qual nó está presente em ambos os lados - inicialmente eu tentei manter essas informações e, obviamente, a função recursiva se tornou tão confusa.Depois que percebi, ficou muito elegante.
Pesquisando ambos os nós (O (N)) e mantendo o controle de caminhos (usa espaço extra -, o número 1 é provavelmente superior, mesmo que o espaço seja provavelmente insignificante se a árvore binária estiver bem equilibrada, pois o consumo de memória extra será apenas O (log (N)).
para que os caminhos sejam comparados (essencialmente semelhantes à resposta aceita - mas os caminhos são calculados assumindo que o nó do ponteiro não está presente no nó da árvore binária)
Apenas para a conclusão ( não relacionada à pergunta ), LCA no BST (O (log (N))
Testes
Recursivo:
onde a versão recursiva privada acima é invocada pelo seguinte método público:
Solução, acompanhando os caminhos dos dois nós:
onde FindNodeAndPath é definido como
BST (LCA) - não relacionado (apenas para conclusão para referência)
Testes unitários
fonte
Se alguém interessado em pseudo-código (para trabalhos domésticos em universidades), aqui está um.
fonte
Embora isso já tenha sido respondido, esta é minha abordagem para esse problema usando a linguagem de programação C. Embora o código mostre uma árvore de pesquisa binária (no que diz respeito a insert ()), o algoritmo também funciona para uma árvore binária. A idéia é passar por todos os nós que se encontram do nó A ao nó B no percurso em ordem, procurando os índices para eles no percurso pós-ordem. O nó com índice máximo na travessia pós-ordem é o menor ancestral comum.
Este é um código C em funcionamento para implementar uma função para encontrar o menor ancestral comum em uma árvore binária. Também estou fornecendo todas as funções utilitárias, etc., mas pule para CommonAncestor () para entender rapidamente.
fonte
Pode haver mais uma abordagem. No entanto, não é tão eficiente quanto o já sugerido nas respostas.
Crie um vetor de caminho para o nó n1.
Crie um segundo vetor de caminho para o nó n2.
Vetor de caminho que implica os nós definidos a partir daquele que seria percorrido para alcançar o nó em questão.
Compare os dois vetores de caminho. O índice em que são incompatíveis retorna o nó nesse índice - 1. Isso daria o LCA.
Contras para esta abordagem:
Precisa percorrer a árvore duas vezes para calcular os vetores do caminho. Precisa de espaço O (h) adicional para armazenar vetores de caminho.
No entanto, isso é fácil de implementar e entender também.
Código para calcular o vetor de caminho:
fonte
Tente assim
fonte
Maneira bruta:
O problema com o método acima é que faremos o "find" várias vezes, ou seja, existe a possibilidade de cada nó ser percorrido várias vezes. Podemos superar esse problema se conseguirmos registrar as informações para não processá-las novamente (pense em programação dinâmica).
Portanto, em vez de encontrar todos os nós, mantemos um registro do que já foi encontrado.
Melhor maneira:
Código:
fonte
Código para uma primeira amplitude de pesquisa para garantir que ambos os nós estejam na árvore. Somente então avance com a pesquisa LCA. Por favor, comente se você tem alguma sugestão para melhorar. Acho que provavelmente podemos marcá-los como visitados e reiniciar a pesquisa em um determinado ponto em que paramos para melhorar o segundo nó (se ele não for encontrado VISITE)
fonte
Você está certo de que, sem um nó pai, a solução com travessia fornecerá complexidade de tempo O (n).
Abordagem transversal Suponha que você esteja encontrando LCA para os nós A e B, a abordagem mais direta é primeiro obter o caminho da raiz para A e, em seguida, obter o caminho da raiz para B. Depois de ter esses dois caminhos, é possível iterá-los facilmente e encontre o último nó comum, que é o menor ancestral comum de A e B.
Solução recursiva Outra abordagem é usar a recursão. Primeiro, podemos obter LCA da árvore esquerda e da árvore direita (se existir). Se A ou B for o nó raiz, a raiz será o LCA e retornaremos a raiz, que é o ponto final da recursão. À medida que continuamos dividindo a árvore em subárvores, eventualmente, atingimos A e B.
Para combinar soluções de subproblemas, se LCA (árvore esquerda) retornar um nó, sabemos que A e B localizam na árvore esquerda e o nó retornado é o resultado final. Se LCA (esquerda) e LCA (direita) retornarem nós não vazios, significa que A e B estão na árvore esquerda e direita, respectivamente. Nesse caso, o nó raiz é o nó comum mais baixo.
Verifique o menor ancestral comum para análise e solução detalhadas.
fonte
Algumas das soluções aqui assumem que há referência ao nó raiz, outras assumem que a árvore é uma BST. Compartilhar minha solução usando o hashmap, sem referência ao
root
nó e à árvore, pode ser BST ou não-BST:fonte
Solução 1: Recursiva - Mais Rápida
Solução 2: Iterativo - Usando ponteiros pai - Mais lento
fonte