Árvores binárias e percursos

Altura, profundidade, recursão estrutural e ordens de visita.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Uma árvore binária é vazia ou tem uma raiz e duas subárvores binárias, esquerda e direita. A posição de cada filho importa. Não existe, por definição, qualquer ordenação dos valores: uma árvore binária de pesquisa acrescenta essa propriedade na página seguinte.

Esta página fixa vocabulário de forma e percursos antes de usares a ordem das BST. Precisas apenas de recursão e pilha de chamadas das lições de complexidade.

Forma, altura e profundidade

A raiz é o nó sem pai. O pai é o nó diretamente acima; cada nó tem no máximo um pai. Uma folha é um nó sem filhos. A profundidade de um nó é o número de arestas da raiz até ele. A raiz tem profundidade zero. A altura de um nó é o maior número de arestas desse nó até uma folha. Adotamos altura -1 para a árvore vazia e 0 para uma folha. Alguns enunciados contam nós em vez de arestas; nesse caso os valores mudam em uma unidade.

Uma árvore completa tem todos os níveis preenchidos, exceto eventualmente o último, preenchido da esquerda para a direita. Uma árvore perfeita tem todos os níveis preenchidos. Uma árvore estrita tem zero ou dois filhos em cada nó. Estas condições são diferentes; «equilibrada» precisa de uma condição quantitativa sobre a altura.

Numa árvore de altura h, existem no máximo 2d2^d nós à profundidade d e no máximo 2h+1−12^{h+1}-1 nós no total. Logo uma árvore com n nós tem altura pelo menos ⌈log⁡2(n+1)⌉−1\lceil\log_2(n+1)\rceil-1. Uma cadeia de n nós tem altura n-1, o máximo possível.

Recursão estrutural

Para contares nós, o vazio contribui zero e uma raiz contribui um mais os tamanhos das subárvores. Para a altura, o vazio contribui -1 e um nó contribui um mais o máximo das alturas dos filhos. Não somes as alturas, porque um caminho descendente escolhe um filho de cada vez.

A prova é por indução na estrutura. O resultado é correto para o vazio. Se as chamadas devolvem tamanhos corretos para as duas subárvores, somar esses tamanhos com a raiz conta todos os nós uma única vez. A mesma hipótese sobre alturas justifica escolher o caminho mais longo.

Cada nó é visitado uma vez: tempo Θ(n)\Theta(n). A pilha tem no máximo um caminho da raiz à folha: espaço O(h+1)O(h+1), que pode ser linear numa cadeia. Uma árvore de dimensão grande e desequilibrada pode exceder a pilha de chamadas, por isso usa um percurso iterativo quando os limites o exigem.

Pré-ordem, em-ordem e pós-ordem

A diferença está no momento em que visitas a raiz:

PercursoOrdemUso frequente
pré-ordemraiz, esquerda, direitadescrever a estrutura a partir da raiz
em-ordemesquerda, raiz, direitaobter chaves ordenadas numa árvore de pesquisa
pós-ordemesquerda, direita, raizcalcular valores dependentes dos filhos; libertar nós
por níveisfila, da raiz para níveis seguintesprofundidades e largura

Começa na raiz 8. Em pré-ordem escreve-a antes de descer para 4. Em em-ordem suspende 8, visita toda a esquerda e só então a escreve. Em pós-ordem deixa 8 para depois das duas subárvores. Para a árvore desenhada, os percursos são pré-ordem 8 4 2 6 10 9, em-ordem 2 4 6 8 9 10 e pós-ordem 2 6 4 9 10 8. O percurso por níveis é 8 4 10 2 6 9.

Percursos, tamanho e altura da árvore
#include <algorithm>
#include <iostream>
#include <vector>
enum class Ordem { pre, em, pos };
struct No { int valor, esq, dir; };
const std::vector<No> a{{8,1,2},{4,3,4},{10,5,-1},{2,-1,-1},{6,-1,-1},{9,-1,-1}};
int altura(int p) { return p == -1 ? -1 : 1 + std::max(altura(a[p].esq), altura(a[p].dir)); }
int tamanho(int p) { return p == -1 ? 0 : 1 + tamanho(a[p].esq) + tamanho(a[p].dir); }
void visitar(int p, Ordem ordem, std::vector<int>& r) {
    if (p == -1) return;
    if (ordem == Ordem::pre) r.push_back(a[p].valor);
    visitar(a[p].esq, ordem, r);
    if (ordem == Ordem::em) r.push_back(a[p].valor);
    visitar(a[p].dir, ordem, r);
    if (ordem == Ordem::pos) r.push_back(a[p].valor);
}
int main() {
    for (Ordem ordem : {Ordem::pre, Ordem::em, Ordem::pos}) {
        std::vector<int> r; visitar(0, ordem, r);
        for (std::size_t i = 0; i < r.size(); ++i)
            std::cout << (i ? " " : "") << r[i];
        std::cout << '\n';
    }
    std::cout << "nos: " << tamanho(0) << "; altura: " << altura(0) << '\n';
    std::cout << "vazia: " << tamanho(-1) << ' ' << altura(-1) << '\n';
}
Resultado

Executa o código para ver a saída e os gráficos.

Os índices dos filhos no programa referem-se a posições válidas do vetor, ou a -1 para ausência. A estrutura fixa não tem ciclos nem nós partilhados entre pais. Se permitisses essas situações, deixaria de ser uma árvore e as recorrências poderiam visitar nós várias vezes ou não terminar.

Calcular propriedades em pós-ordem

Para contar nós com exatamente dois filhos, soma os resultados das subárvores e acrescenta um apenas quando ambas as ligações existem. Uma folha e um nó com um filho contribuem zero. Na árvore da figura, só 8 e 4 contribuem, logo o resultado é 2.

Para decidir se a árvore é perfeita, não basta comparar as alturas dos filhos da raiz. Cada subárvore também tem de ser perfeita. Devolve em cada chamada um resumo com a altura, a perfeição e a contagem de nós com dois filhos:

  • O vazio tem altura -1, contagem zero e é considerado perfeito neste contrato.
  • Um nó é perfeito se as duas subárvores forem perfeitas e tiverem alturas iguais.
  • A sua altura é um mais a maior altura; a contagem soma as dos filhos e a contribuição local.
Combinar propriedades sem repetir percursos
#include <algorithm>
#include <iostream>
struct No { int valor; const No* esq; const No* dir; };
struct Resumo { bool perfeita; int altura; int binarios; };
Resumo resumir(const No* p) {
    if (!p) return {true, -1, 0};
    const auto e = resumir(p->esq), d = resumir(p->dir);
    return {e.perfeita && d.perfeita && e.altura == d.altura,
            1 + std::max(e.altura, d.altura),
            e.binarios + d.binarios + (p->esq && p->dir ? 1 : 0)};
}
void mostrar(const No* p) {
    const auto r = resumir(p);
    std::cout << "perfeita=" << r.perfeita << "; altura=" << r.altura
              << "; dois filhos=" << r.binarios << '\n';
}
int main() {
    const No a{1,nullptr,nullptr}, b{3,nullptr,nullptr};
    const No folhaDireita{6,nullptr,nullptr};
    const No c{2,&a,&b}, umFilho{4,&folhaDireita,nullptr};
    const No alturasIguais{5,&c,&umFilho};
    mostrar(nullptr); mostrar(&a); mostrar(&c); mostrar(&alturasIguais);
    return 0;
}
Resultado

Executa o código para ver a saída e os gráficos.

Os primeiros casos mostram o vazio, uma folha e uma raiz com duas folhas: todos perfeitos, com alturas -1, 0 e 1 e contagens 0, 0 e 1. O último caso tem dois filhos de altura 1, mas a subárvore direita só tem um filho. A altura da árvore é 2 e há dois nós com dois filhos, mas não é perfeita. Esta entrada revela por que a igualdade de alturas só na raiz seria insuficiente.

O argumento é indutivo: duas subárvores perfeitas de igual altura enchem todos os níveis até à mesma profundidade; acrescentar a raiz enche o nível anterior. Se faltar um filho, as alturas das subárvores perfeitas seriam -1 e pelo menos 0, diferentes. O método visita cada nó uma vez, dando Θ(n)\Theta(n) de tempo e O(h+1)O(h+1) de pilha. Recalcular a altura por um percurso completo em cada nó pode dar custo quadrático numa cadeia.

Reconstruir e representar

Com valores distintos, pré-ordem e em-ordem determinam a árvore: o primeiro valor da pré-ordem é a raiz; a sua posição na em-ordem separa os valores das subárvores. Os tamanhos dessas regiões separam a pré-ordem restante. Repete o procedimento nas duas regiões.

Para pré-ordem [8,4,2,6,10,9] e em-ordem [2,4,6,8,9,10], a raiz é 8 e a esquerda tem três nós. A pré-ordem esquerda começa em 4; na em-ordem esquerda, 2 fica antes de 4 e 6 depois. A direita começa em 10 e tem 9 à esquerda. Procurar a raiz por percurso linear em cada chamada pode dar custo quadrático; um mapa de posições localiza cada chave sem repetir esses percursos. Com uma tabela hash de consultas esperadas constantes, a reconstrução é esperada linear; com um mapa ordenado, custa O(n log n).

Pré-ordem sozinha não determina uma árvore binária genérica: [1,2] pode ter 2 à esquerda ou à direita. Marcas explícitas de vazio resolvem a ambiguidade; uma folha em pré-ordem torna-se valor, vazio, vazio. Com chaves repetidas, pré-ordem mais em-ordem pode voltar a ser ambígua.

Para contares nós à profundidade k, o vazio contribui zero; com k=0, um nó existente contribui um; caso contrário, soma os resultados dos filhos com k-1. Na árvore desenhada, as folhas são 2, 6 e 9, três no total; a profundidade 1 tem os nós 4 e 10. Contar folhas soma um por folha e zero no vazio. Para k<0, devolve zero. O algoritmo só precisa de visitar até ao nível pedido; o limite de pior caso continua O(n)O(n).

Exercícios

As justificações e os programas em C++ são de autoavaliação: compara-os com os critérios da solução. Nas questões numéricas, a verificação automática confirma apenas o valor.

Distinguir altura e níveis

Uma árvore completa tem 10 nós. Qual é a altura, medida em arestas?

Pista

As capacidades até às alturas 2 e 3 são 7 e 15 nós.

Mais uma pista

O décimo nó está no quarto nível, com profundidade 3.

Solução

A altura é 3, porque a forma completa enche os primeiros níveis com 1+2+4=7 nós e os três restantes começam o nível de profundidade 3. Se fossem 10 nós numa árvore arbitrária, a altura poderia variar de 3 a 9.

Erros frequentes

Não respondas 4, que é o número de níveis; não concluas uma altura única sem a hipótese de completude.

Voltar à explicação
Reconstruir com duas ordens

Valores distintos têm pré-ordem [7,3,1,5,9] e em-ordem [1,3,5,7,9]. Desenha a árvore e dá a pós-ordem.

Pista

A raiz é o primeiro valor da pré-ordem.

Mais uma pista

À esquerda de 7 ficam três valores; a raiz desse ramo é 3.

Solução

A raiz 7 tem filhos 3 e 9; 3 tem filhos 1 e 5. A pós-ordem é [1,5,3,9,7], porque a em-ordem separa os membros das subárvores e o seu tamanho separa as regiões da pré-ordem. A conclusão usa valores distintos.

Confere a tua resposta:

  • Desenhei raiz 7, filhos 3 e 9, e filhos 1 e 5 de 3.
  • Obtive pós-ordem 1,5,3,9,7.
  • Separei as subárvores pela em-ordem e usei chaves distintas.

Erros frequentes

Não uses o primeiro valor da em-ordem como raiz; pré-ordem sozinha não distingue filhos esquerdos de direitos.

Voltar à explicação
Contar folhas e nível

Uma árvore tem raiz 8, filhos 4 e 10; 4 tem filhos 2 e 6, e 10 tem apenas o filho esquerdo 9. Os restantes filhos são vazios. Conta as folhas e os nós à profundidade 1, com profundidade zero na raiz. Mostra os retornos da recursão.

Pista
Folha não tem filhos.
Solução

Para folhas, cada folha devolve 1 e o vazio devolve 0. Em 4, o resultado é 1+1=2; em 10, é 1+0=1; em 8, é 2+1=3. São as folhas 2, 6 e 9. Para contar à profundidade 1, a chamada em 8 pede profundidade 0 a cada filho: 4 devolve 1 e 10 devolve 1. A soma é 2. Não precisamos de descer aos filhos desses nós. Com profundidade negativa ou árvore vazia, devolvemos zero.

Confere a tua resposta:

  • Distingui folha de nó interno.
  • Contei três folhas na árvore desenhada.
  • Mostrei o retorno em cada chamada para k=1.

Erros frequentes

Somar alturas em vez do máximo escolhe um caminho inexistente.
Voltar à explicação
Conferir os filhos e as subárvores

A raiz 10 tem filhos 4 e 20. O nó 4 tem duas folhas, 2 e 6; o nó 20 tem apenas a folha esquerda 15. A árvore é perfeita? Dá altura e número de nós com exatamente dois filhos. Descreve uma função recursiva que calcule as três propriedades num único percurso, sem chamar uma função de altura completa em cada nó. Considera o vazio perfeito, com altura -1.

Pista

Alturas iguais na raiz não dizem se cada subárvore está preenchida.

Solução

As folhas devolvem (perfeita=true,altura=0,binarios=0). O nó 4 devolve (true,1,1). Em 20, o filho esquerdo tem altura 0 e o direito -1: devolve (false,1,0). A raiz tem filhos com igual altura, mas a direita não é perfeita, portanto devolve (false,2,2). Os dois nós com dois filhos são 10 e 4. No vazio, devolve (true,-1,0); em cada nó, perfeição exige os dois resultados verdadeiros e alturas iguais, altura é um mais o máximo, e a contagem soma as dos filhos e um se ambos existirem. Cada nó é visitado uma vez: Θ(n) de tempo, O(h+1) de pilha. Exigir apenas diferença de alturas no máximo um verificaria outro critério e não a perfeição.

Confere a tua resposta:

  • Rejeitei a perfeição, apesar das alturas iguais dos filhos da raiz.
  • Obtive altura 2 e dois nós com exatamente dois filhos.
  • Combinei perfeição e altura dos filhos em cada nó, com vazio perfeito de altura -1.
  • Justifiquei uma visita por nó e espaço O(h+1).
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.