Aula 12 - Árvores Binárias de Pesquisa (ABP)
Na Aula 11 vimos o conceito de árvore binária e os quatro caminhamentos clássicos. Nesta aula estudamos a Árvore Binária de Pesquisa (Abp): uma árvore binária com uma propriedade de ordenação que permite buscar, inserir e remover elementos de forma eficiente. Veremos a definição da propriedade, a representação em C, todas as operações e sua complexidade — incluindo o caso degenerado que motiva o estudo de árvores balanceadas (Aula 14).
abp.zip — abp.h, abp.c e main.c.
|
1. A propriedade da Abp
Uma Árvore Binária de Pesquisa (Abp) é uma árvore binária que satisfaz a propriedade de ordenação: para todo nó x,
- todos os valores na subárvore esquerda de
xsão menores que o valor dex; - todos os valores na subárvore direita de
xsão maiores que o valor dex.
Essa propriedade vale recursivamente para toda subárvore, não só para a raiz. É o que permite, a cada passo de uma busca, descartar metade (ou mais) da árvore restante — do mesmo jeito que a busca binária em vetores ordenados descarta metade do vetor.
50 <- tudo à esquerda < 50; tudo à direita > 50
/ \
30 70 <- subárvore esq.: tudo < 30 à esq de 30; tudo > 30 à dir de 30
/ \ / \ (e todos < 50 pela propriedade da raiz)
20 40 60 80
Uma consequência direta: o caminhamento em-ordem de uma Abp sempre produz os valores em ordem crescente — decorrência direta da propriedade de ordenação aplicada recursivamente.
2. Representação em C e interface do TAD
Cada nó guarda o dado e dois ponteiros, esq e dir. A árvore inteira é representada pelo ponteiro para o nó raiz; uma árvore vazia é simplesmente NULL:
typedef struct { int cod; char nome[50]; float preco; } Produto; typedef struct str_NodoArv NodoArv; struct str_NodoArv { Produto dado; NodoArv *esq; NodoArv *dir; }; /* Inicializar = declarar NULL: NodoArv *raiz = NULL; */
As operações que modificam a árvore (inserir, remover) recebem o ponteiro raiz como parâmetro e o devolvem atualizado. O chamador precisa reatribuir:
NodoArv* abpBuscar (NodoArv *raiz, int cod); /* ponteiro p/ nó com dado.cod==cod, ou NULL */ NodoArv* abpInserir (NodoArv *raiz, Produto p); /* insere p preservando propriedade */ NodoArv* abpRemover (NodoArv *raiz, int cod); /* remove cod preservando propriedade */ void abpDestruir (NodoArv *raiz); /* libera toda a memória */ void abpEmOrdem (const NodoArv *raiz); /* imprime em ordem crescente de cod */ /* Uso: */ raiz = abpInserir(raiz, produto); raiz = abpRemover(raiz, cod);
Por que não usar um struct descritor, como nas listas e filas? Nas listas e filas, o descritor agrupa vários campos de controle (frente e final na fila; ini e fim na lista dupla), e as funções recebem um ponteiro para esse descritor e o modificam internamente — o chamador não precisa reatribuir nada. Numa Abp, os algoritmos são recursivos, e a recursão usa uma idioma natural: cada chamada retorna o ponteiro raiz da subárvore que ficou após a operação, e o nível acima reatribui esq ou dir com esse retorno. Esse encadeamento de retornos é o que "repropaga" a nova raiz até o topo sem precisar de ponteiros para ponteiros. Se usássemos um descritor, teríamos que ou passar Descritor* e atualizar desc->raiz só no nível mais externo (complicando a recursão que já funciona naturalmente), ou retornar o descritor inteiro (sem ganho). A convenção de retorno é, portanto, a forma mais limpa de combinar recursão com atualização do ponteiro raiz.
|
3. Operações
Todas as operações principais seguem o mesmo padrão recursivo da Aula 11: caso base para NULL, e duas chamadas recursivas que descem pela subárvore esquerda ou direita. A propriedade da Abp decide qual subárvore percorrer.
3.1 Buscar
Ideia: começa na raiz. Se o valor buscado é igual ao nó atual, encontrou. Se é menor, vai à esquerda; se é maior, vai à direita. Repete até encontrar ou chegar a NULL (não existe). A cada passo, descarta-se uma subárvore inteira — exatamente como a busca binária.
Exemplo: buscar 40 na árvore abaixo.
50
/ \
30 70
/ \ / \
20 40 60 80
abpBuscar([50], 40): 40 < 50 → desce à esquerda
abpBuscar([30], 40): 40 > 30 → desce à direita
abpBuscar([40], 40): 40 == 40 → encontrou! retorna [40]
retorna [40]
retorna [40]
Exemplo: buscar 45 (não existe).
abpBuscar([50], 45): 45 < 50 → desce à esquerda
abpBuscar([30], 45): 45 > 30 → desce à direita
abpBuscar([40], 45): 45 > 40 → desce à direita
abpBuscar(NULL, 45): NULL → não encontrado, retorna NULL
retorna NULL
retorna NULL
retorna NULL
NodoArv* abpBuscar(NodoArv *raiz, int cod) { if (raiz == NULL) return NULL; // não encontrado if (cod == raiz->dado.cod) return raiz; // encontrado if (cod < raiz->dado.cod) return abpBuscar(raiz->esq, cod); // só olha a esquerda return abpBuscar(raiz->dir, cod); // só olha a direita }
Versão iterativa. A busca é a operação mais simples de tornar iterativa: basta substituir as chamadas recursivas por um while que avança atual pelo caminho correto até encontrar o elemento ou chegar em NULL.
NodoArv* abpBuscarIter(NodoArv *raiz, int cod) { NodoArv *atual = raiz; while (atual != NULL) { if (cod == atual->dado.cod) return atual; /* encontrado */ if (cod < atual->dado.cod) atual = atual->esq; else atual = atual->dir; } return NULL; /* não encontrado */ }
3.2 Inserir
Ideia: desce a árvore como se estivesse buscando o valor. Quando chega a NULL, esse é o lugar certo para o novo nó (qualquer outro lugar violaria a propriedade). O truque do retorno: cada chamada devolve o ponteiro que o nível acima deve usar para esq ou dir — assim o novo nó é "pendurado" automaticamente na subida da recursão, sem precisar de um ponteiro "pai" extra.
Exemplo: inserir 35 na árvore.
50
/ \
30 70
/ \ / \
20 40 60 80
abpInserir([50], 35): 35 < 50 → desce à esquerda
abpInserir([30], 35): 35 > 30 → desce à direita
abpInserir([40], 35): 35 < 40 → desce à esquerda
abpInserir(NULL, 35): NULL → cria [35], retorna [35]
[40].esq = [35]; retorna [40]
[30].dir = [40]; retorna [30]
[50].esq = [30]; retorna [50]
Resultado:
50
/ \
30 70
/ \ / \
20 40 60 80
/
35
| Antes (inserir 35) | Depois |
|---|---|
50
/ \
30 70
/ \ / \
20 40 60 80
|
50
/ \
30 70
/ \ / \
20 40 60 80
/
35 <-- novo nó
|
NodoArv* abpInserir(NodoArv *raiz, Produto valor) {
if (raiz == NULL) {
NodoArv *novo = (NodoArv*) malloc(sizeof(NodoArv));
novo->dado = valor;
novo->esq = NULL;
novo->dir = NULL;
return novo; // pai vai "pendurar" este nó
}
if (valor.cod < raiz->dado.cod)
raiz->esq = abpInserir(raiz->esq, valor);
else if (valor.cod > raiz->dado.cod)
raiz->dir = abpInserir(raiz->dir, valor);
// se igual: código já existe, não insere duplicado
return raiz; // devolve raiz (inalterada) ao pai
}
3.3 Remover
Ideia: primeiro desce pela árvore como na busca, até encontrar o nó. Ao encontrá-lo, há três casos dependendo do número de filhos. O mesmo idioma de retorno da inserção faz o pai "religar" o ponteiro correto automaticamente na subida.
Os três casos:
- Caso 1 — nó é folha (0 filhos): libera o nó e retorna
NULL. O pai, ao receberNULL, passa a apontar para o vazio — correto. - Caso 2 — nó tem 1 filho: libera o nó e retorna o único filho. O filho "sobe" para o lugar do nó removido, e a propriedade da Abp é preservada pois o filho já estava do lado correto.
- Caso 3 — nó tem 2 filhos: não dá para remover diretamente, pois dois filhos competem pelo mesmo lugar. A solução é não remover o nó: em vez disso, encontra-se o sucessor em-ordem (o menor valor da subárvore direita — o nó mais à esquerda possível a partir da direita), copia-se o seu valor para o nó atual, e remove-se o sucessor de onde ele estava. O sucessor nunca tem filho esquerdo (caso contrário não seria o mínimo), então sua remoção é sempre caso 1 ou 2.
Por que o sucessor preserva a propriedade? O sucessor é o menor valor da subárvore direita: logo, é maior que tudo na subárvore esquerda e menor que todo o restante da subárvore direita. Colocá-lo no lugar do nó removido mantém a propriedade intacta.
Caso 1 — folha (0 filhos): remover 20
| Antes | Depois (20 removido) |
|---|---|
50
/ \
30 70
/ \ \
20 40 80
|
50
/ \
30 70
\ \
40 80
|
Caso 2 — um filho: remover 70 (filho único: 80)
| Antes | Depois (70 removido, 80 sobe) |
|---|---|
50
/ \
30 70
/ \ \
20 40 80
|
50
/ \
30 80 <-- 80 subiu para o lugar de 70
/ \
20 40
|
Caso 3 — dois filhos: remover 30 (sucessor em-ordem: 40)
| Antes | Depois (30 substituído por 40) |
|---|---|
50
/ \
30 70
/ \ \
20 40 80
|
50
/ \
40 70 <-- 40 (sucessor) copiado para o lugar de 30
/ \ o nó original 40 foi removido (era folha)
20 80
|
Exemplo com os três casos. Usaremos a árvore abaixo e faremos três remoções diferentes:
Árvore inicial:
50
/ \
30 70
/ \ \
20 40 80
Remoção 1: remover 20 — Caso 1 (folha)
abpRemover([50], 20): 20 < 50 → desce à esquerda
abpRemover([30], 20): 20 < 30 → desce à esquerda
abpRemover([20], 20): achou! é folha (caso 1) → free([20]), retorna NULL
[30].esq = NULL; retorna [30]
[50].esq = [30]; retorna [50]
Resultado:
50
/ \
30 70
\ \
40 80
Remoção 2: remover 70 — Caso 2 (um filho: só 80 à direita)
(partindo do resultado anterior, sem o 20)
abpRemover([50], 70): 70 > 50 → desce à direita
abpRemover([70], 70): achou! tem só filho direito (caso 2) → free([70]), retorna [80]
[50].dir = [80]; retorna [50]
Resultado:
50
/ \
30 80
\
40
Remoção 3: remover 30 — Caso 3 (dois filhos: 20 e 40)
(voltando à árvore inicial, com todos os nós)
50
/ \
30 70
/ \ \
20 40 80
abpRemover([50], 30): 30 < 50 → desce à esquerda
abpRemover([30], 30): achou! tem 2 filhos (caso 3)
sucessor = abpMinimo([40]) = [40] (nó mais à esquerda a partir de 30.dir)
[30].dado ← dado de [40] (copia o valor 40 para o lugar de 30)
agora remove o [40] original da subárvore direita de [30]:
abpRemover([40], 40): achou! é folha (caso 1) → free([40]), retorna NULL
[30].dir = NULL; retorna [30] (mas agora [30] contém o valor 40)
[50].esq = [30]; retorna [50]
Resultado:
50
/ \
40 70
/ \
20 80
// encontra o nó de menor valor (mais à esquerda) de uma subárvore NodoArv* abpMinimo(NodoArv *raiz) { while (raiz->esq != NULL) raiz = raiz->esq; return raiz; } NodoArv* abpRemover(NodoArv *raiz, int cod) { if (raiz == NULL) return NULL; // não encontrado if (cod < raiz->dado.cod) raiz->esq = abpRemover(raiz->esq, cod); // procura/remove na esquerda else if (cod > raiz->dado.cod) raiz->dir = abpRemover(raiz->dir, cod); // procura/remove na direita else { // achou o nó a remover if (raiz->esq == NULL && raiz->dir == NULL) { free(raiz); // caso 1: folha return NULL; } if (raiz->esq == NULL) { NodoArv *filho = raiz->dir; // caso 2: só filho direito free(raiz); return filho; } if (raiz->dir == NULL) { NodoArv *filho = raiz->esq; // caso 2: só filho esquerdo free(raiz); return filho; } // caso 3: dois filhos NodoArv *sucessor = abpMinimo(raiz->dir); raiz->dado = sucessor->dado; // copia valor do sucessor raiz->dir = abpRemover(raiz->dir, sucessor->dado.cod); // remove o sucessor } return raiz; }
3.4 Destruir
abpDestruir usa pós-ordem: libera os filhos antes do próprio nó, pois o free tornaria os ponteiros esq/dir inválidos.
void abpDestruir(NodoArv *raiz) { if (raiz == NULL) return; abpDestruir(raiz->esq); abpDestruir(raiz->dir); free(raiz); } abpDestruir(raiz); raiz = NULL; // responsabilidade do chamador
3.5 Exemplo de uso completo (main)
int main() { NodoArv *raiz = NULL; /* árvore vazia */ NodoArv *encontrado; Produto p; /* --- inserir alguns produtos --- */ p.cod = 50; p.preco = 99.90; raiz = abpInserir(raiz, p); p.cod = 30; p.preco = 49.90; raiz = abpInserir(raiz, p); p.cod = 70; p.preco = 79.90; raiz = abpInserir(raiz, p); p.cod = 20; p.preco = 19.90; raiz = abpInserir(raiz, p); p.cod = 40; p.preco = 39.90; raiz = abpInserir(raiz, p); /* em-ordem imprime: 20 30 40 50 70 */ abpEmOrdem(raiz); /* --- buscar --- */ encontrado = abpBuscar(raiz, 30); if (encontrado != NULL) printf("Encontrado: cod=%d preco=%.2f\n", encontrado->dado.cod, encontrado->dado.preco); else printf("Nao encontrado.\n"); /* --- remover --- */ raiz = abpRemover(raiz, 30); /* caso 3: 30 tem dois filhos */ raiz = abpRemover(raiz, 20); /* caso 1: 20 é folha */ raiz = abpRemover(raiz, 70); /* caso 1: 70 virou folha */ /* em-ordem agora imprime: 40 50 */ abpEmOrdem(raiz); /* --- destruir e anular o ponteiro --- */ abpDestruir(raiz); raiz = NULL; return 0; }
4. Complexidade das operações
Todas as operações principais da Abp (buscar, inserir, remover) descem a árvore por um único caminho, da raiz até, no pior caso, uma folha — seu custo é sempre proporcional à altura h da árvore:
| Operação | Custo | Por quê |
|---|---|---|
| Buscar | O(h) |
desce um único caminho da raiz até encontrar o valor ou chegar a NULL. |
| Inserir | O(h) |
mesmo caminho de uma busca, até achar o ponto (NULL) onde o novo nó entra. |
| Remover | O(h) |
busca do nó (O(h)) + no caso de dois filhos, busca do sucessor mínimo (também O(h), pois desce só pela subárvore direita). |
| Destruir | O(n) |
é obrigatório visitar todos os n nós, pois todos precisam ser liberados. |
| Caminhamentos | O(n) |
por definição, visitam todos os n nós. |
Abp não é automaticamente rápida. Nada nas operações de inserção impede que a árvore degenere em uma lista encadeada. Se os elementos forem inseridos em ordem já crescente (ou já decrescente), cada novo nó vira filho único do anterior, e a árvore fica com altura h = n - 1:
inserir 10, 20, 30, 40 (em ordem crescente):
10
\
20
\
30
\
40 // árvore "torta": h = n-1 = 3
Nesse cenário, buscar, inserir e remover custam O(h) = O(n) — a mesma complexidade de uma busca em lista encadeada, perdendo toda a vantagem da árvore. Esse é exatamente o problema que as árvores balanceadas (Aula 15) resolvem, garantindo h = O(log₂ n) sempre, independentemente da ordem de inserção.
|
Resumindo: no melhor caso (árvore perfeitamente balanceada), h = O(log₂ n), e buscar/inserir/remover custam O(log₂ n). No pior caso (árvore degenerada), h = O(n), e essas mesmas operações custam O(n).
Resumo
|
Exercícios
Questões teóricas
- Explique a propriedade da Abp com suas próprias palavras. Por que ela precisa valer recursivamente para toda subárvore e não apenas para a raiz? Dê um exemplo de árvore binária que satisfaz a propriedade somente na raiz mas que não é uma Abp válida.
-
Dada a sequência de inserções 50, 30, 70, 20, 40, 60, 80:
- Desenhe a Abp resultante.
- Escreva o caminhamento em-ordem, pré-ordem e pós-ordem.
- Qual é a altura da árvore?
- Mostre o passo a passo da remoção do nó 30 (que tem dois filhos) na árvore do exercício anterior. Qual nó ocupa o lugar de 30 e por quê?
- Explique os três casos de remoção de um nó numa Abp. Por que o caso de dois filhos utiliza o sucessor em-ordem? Prove que o sucessor em-ordem de um nó com dois filhos nunca tem filho esquerdo.
-
A busca, inserção e remoção numa Abp custam
O(h), ondehé a altura. No melhor casoh = O(log₂ n); no pior casoh = O(n).- Dê um exemplo de sequência de inserções que produz o pior caso (árvore degenerada).
- Dê um exemplo de sequência que produz o melhor caso (árvore balanceada) com os mesmos elementos.
- Por que a ordem de inserção afeta a altura?
-
Por que
abpDestruirusa pós-ordem (libera os filhos antes do próprio nó) e não pré-ordem? O que aconteceria se ofree(raiz)fosse chamado antes das chamadas recursivas?
Questões de implementação
Os exercícios a seguir pedem a implementação de funções em C sobre a Abp. Use recursão. Os tipos NodoArv e Produto são os definidos nesta aula.
-
Implemente
int abpAltura(const NodoArv *raiz)que retorna a altura da árvore (convenção: árvore vazia tem altura −1; árvore com só a raiz tem altura 0). -
Implemente
int abpContar(const NodoArv *raiz)que retorna o número total de nós da árvore. -
Implemente
int abpContarFolhas(const NodoArv *raiz)que retorna o número de folhas (nós sem filhos). -
Implemente
void abpImprimirEntre(const NodoArv *raiz, int a, int b)que imprime, em ordem crescente, todos os elementos cujodado.codestá no intervalo fechado [a, b]. Aproveite a propriedade da Abp para não descer por subárvores que certamente não contenham valores no intervalo. -
Implemente
int abpSomaChaves(const NodoArv *raiz)que retorna a soma de todos os valoresdado.codda árvore. -
Implemente
int abpContarMaiores(const NodoArv *raiz, int val)que retorna a quantidade de nós comdado.cod > val. Use a propriedade da Abp para podar a busca quando possível. -
Implemente
NodoArv* abpCopiar(const NodoArv *raiz)que retorna uma cópia profunda (deep copy) da árvore, alocando novos nós commalloc. A árvore copiada deve ser independente da original. -
Implemente
int abpEstahPresente(const NodoArv *raiz, int cod)usando recursão (semwhileoufor). A função deve retornar 1 se ocodexistir na árvore, 0 caso contrário.
Sugestões de Respostas dos Exercícios
Questões teóricas
Q1 — Propriedade da Abp
Para todo nó x, todos os valores da sua subárvore esquerda são menores que x, e todos da direita são maiores. A propriedade precisa valer recursivamente porque a busca toma decisões em cada nó que percorre: ao descer à direita de x, o algoritmo assume que tudo à direita é maior. Se um nó interno violasse a propriedade apenas localmente (mas os seus ancestrais não), a busca poderia ir pelo caminho errado e nunca encontrar o elemento.
Contraexemplo — satisfaz a raiz, mas não é Abp válida:
50
/ \
30 70
\
80 <- 80 > 50, mas está na subárvore ESQUERDA de 50 (violação!)
A raiz 50 vê 30 à esquerda (ok) e 70 à direita (ok). Mas o nó 30 tem 80 à direita, e 80 > 50 — logo 80 está no lado errado em relação à raiz. Uma busca por 80 desceria à esquerda de 50 (pois 80 > 50... espera, não: como 80 > 50, a busca iria à direita, encontraria 70 e pararia — nunca encontraria 80).
Q2 — Inserções e caminhamentos
50
/ \
30 70
/ \ / \
20 40 60 80
Pré-ordem: 50 30 20 40 70 60 80
Em-ordem: 20 30 40 50 60 70 80 (ordem crescente)
Pós-ordem: 20 40 30 60 80 70 50
Altura: 2
Q3 — Remoção do nó 30
30 tem dois filhos (20 e 40). Aplicamos o caso 3: encontramos o sucessor em-ordem de 30, que é o menor valor da sua subárvore direita — o nó mais à esquerda a partir de 40, que é o próprio 40 (sem filho esquerdo). Copiamos o valor 40 para o lugar de 30 e removemos o nó 40 original (que era folha — caso 1):
50
/ \
40 70 <- 40 subiu para o lugar de 30
/ / \
20 60 80
40 foi escolhido porque é o menor valor maior que tudo na subárvore esquerda de 30 (20), garantindo que a propriedade da Abp seja preservada.
Q4 — Três casos de remoção
Caso 1 (folha): basta liberar o nó; o pai passa a apontar para NULL.
Caso 2 (um filho): o filho único "sobe" para o lugar do nó removido; a propriedade é preservada porque o filho já estava do lado correto.
Caso 3 (dois filhos): não há como simplesmente remover o nó — precisaria escolher qual dos dois filhos sobe, e qualquer um poderia violar a propriedade com o outro. A solução é substituir o valor do nó pelo seu sucessor em-ordem (o menor da subárvore direita) e depois remover o sucessor de onde ele estava.
Por que o sucessor funciona? O sucessor é o menor valor da subárvore direita, logo: (a) é maior que todos os valores da subárvore esquerda do nó removido; (b) é menor ou igual a todos os demais valores da subárvore direita. Colocá-lo no lugar preserva a propriedade.
Prova de que o sucessor nunca tem filho esquerdo: seja S o sucessor em-ordem de um nó com dois filhos — S é o nó de menor cod na subárvore direita. Se S tivesse um filho esquerdo F, então F < S, logo F seria um valor ainda menor na mesma subárvore, contradizendo que S é o mínimo. Portanto S não pode ter filho esquerdo; sua remoção é sempre caso 1 ou 2.
Q5 — Complexidade e degeneração
Pior caso (degenerada): inserir em ordem crescente: 10, 20, 30, 40, 50. Cada nó vira filho direito do anterior; altura = n−1 = 4.
Melhor caso (balanceada): inserir na ordem 30, 10, 50, 20, 40 (ou qualquer sequência que insira a mediana antes dos extremos de cada metade). A altura fica ≈ log₂(n).
Por que a ordem importa? A posição de cada novo nó é determinada pelos nós já existentes — um elemento sempre entra como folha no local onde a busca pelo seu valor falha. Se os elementos chegam em ordem crescente, cada busca nunca descarta nada à esquerda, e a árvore cresce só para a direita, virando uma lista.
Q6 — Por que pós-ordem no destruir
abpDestruir usa pós-ordem porque a única forma de alcançar os filhos de um nó é pelos seus ponteiros esq e dir. Se chamássemos free(raiz) primeiro (pré-ordem), esses ponteiros deixariam de ser válidos — o bloco de memória do nó seria liberado e seu conteúdo se tornaria indefinido. As chamadas recursivas que viriam depois leriam ponteiros inválidos (dangling pointers), causando comportamento indefinido (e, na prática, acesso a memória já liberada ou corrompida). Liberar os filhos antes garante que, ao chamar free(raiz), todos os descendentes já estão liberados e não há mais referências pendentes a esse nó.
Questões de implementação
Exercício 1 — abpAltura
int abpAltura(const NodoArv *raiz) { int altEsq, altDir; if (raiz == NULL) return -1; // convenção: vazia tem altura -1 altEsq = abpAltura(raiz->esq); altDir = abpAltura(raiz->dir); if (altEsq > altDir) return 1 + altEsq; else return 1 + altDir; }
Exercício 2 — abpContar
int abpContar(const NodoArv *raiz) { if (raiz == NULL) return 0; return 1 + abpContar(raiz->esq) + abpContar(raiz->dir); }
Exercício 3 — abpContarFolhas
int abpContarFolhas(const NodoArv *raiz) { if (raiz == NULL) return 0; if (raiz->esq == NULL && raiz->dir == NULL) return 1; // é folha return abpContarFolhas(raiz->esq) + abpContarFolhas(raiz->dir); }
Exercício 4 — abpImprimirEntre
void abpImprimirEntre(const NodoArv *raiz, int a, int b) { if (raiz == NULL) return; // só desce à esquerda se puder haver valores >= a lá if (raiz->dado.cod > a) abpImprimirEntre(raiz->esq, a, b); if (raiz->dado.cod >= a && raiz->dado.cod <= b) printf("%d ", raiz->dado.cod); // só desce à direita se puder haver valores <= b lá if (raiz->dado.cod < b) abpImprimirEntre(raiz->dir, a, b); }
A poda é o ponto central: se o nó atual já é maior que b, não faz sentido descer à direita (todos os valores lá são ainda maiores). Analogamente para a esquerda e a.
Exercício 5 — abpSomaChaves
int abpSomaChaves(const NodoArv *raiz) { if (raiz == NULL) return 0; return raiz->dado.cod + abpSomaChaves(raiz->esq) + abpSomaChaves(raiz->dir); }
Exercício 6 — abpContarMaiores
int abpContarMaiores(const NodoArv *raiz, int val) { if (raiz == NULL) return 0; if (raiz->dado.cod <= val) // este nó e toda a subárvore esquerda são <= val; só a direita interessa return abpContarMaiores(raiz->dir, val); // este nó é > val: conta ele + tudo na direita + possíveis na esquerda return 1 + abpContarMaiores(raiz->dir, val) + abpContarMaiores(raiz->esq, val); }
Exercício 7 — abpCopiar
NodoArv* abpCopiar(const NodoArv *raiz) { NodoArv *novo; if (raiz == NULL) return NULL; novo = (NodoArv*) malloc(sizeof(NodoArv)); novo->dado = raiz->dado; novo->esq = abpCopiar(raiz->esq); // copia subárvore esquerda novo->dir = abpCopiar(raiz->dir); // copia subárvore direita return novo; }
Usa pré-ordem: cria o nó atual antes de copiar os filhos, de modo que os ponteiros esq/dir do novo nó já recebem as raizes das subarvores copiadas.
Exercício 8 — abpEstahPresente
int abpEstahPresente(const NodoArv *raiz, int cod) { if (raiz == NULL) return 0; if (cod == raiz->dado.cod) return 1; if (cod < raiz->dado.cod) return abpEstahPresente(raiz->esq, cod); return abpEstahPresente(raiz->dir, cod); }
Equivalente a abpBuscar != NULL, mas retorna diretamente 0 ou 1. É essencialmente a mesma busca, só mudando o tipo de retorno.