INF01203 - Estruturas de Dados - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



Aula 23 - B-Trees e B+-Trees


Motivação: o problema do disco

Tudo o que estudamos até agora — ABP, AVL, tabela hash — tem uma premissa implícita: os dados cabem na memória RAM. Quando isso é verdade, as operações custam O(log n) ou O(1) com constantes pequenas, e tudo funciona bem.

Na prática, porém, sistemas reais armazenam bilhões de registros:

  • Um banco de dados de e-commerce pode ter 10 bilhões de transações históricas.
  • Um sistema de arquivos do Linux indexa milhões de arquivos em um único disco.
  • Um índice de mecanismo de busca contém trilhões de URLs.

Esses dados vivem em disco (HD ou SSD). E disco tem uma propriedade fundamental, diferente da RAM:

Acesso ao disco é feito em BLOCOS. O disco não lê um único inteiro — ele lê um bloco inteiro de uma vez (tipicamente 4KB a 64KB). Ler 4 bytes ou ler 4.000 bytes custa exatamente o mesmo tempo se estiverem no mesmo bloco. E um acesso ao disco (mesmo SSD) é 100.000× mais lento que um acesso à RAM.

Agora pense numa AVL com 1 bilhão de nós. A altura é ≈ 30. Uma busca faz 30 comparações, cada uma em um nó diferente — nós espalhados aleatoriamente pelo disco. Resultado: 30 leituras de disco. Cada leitura traz um bloco de 4KB, mas usa apenas os ≈ 20 bytes do nó AVL. 99,5% do bloco é desperdiçado.

Exemplo concreto: um banco com 1 bilhão de contas, buscando uma conta pelo número dela.
  • Acesso à RAM: ≈ 100 ns.
  • Acesso ao disco (SSD, leitura aleatória): ≈ 100 µs — 1.000× mais lento que a RAM.
  • Acesso ao disco (HD mecânico, com busca da cabeça de leitura): ≈ 10 ms — 100.000× mais lento que a RAM.
Com uma AVL (altura ≈ 30, um nó por bloco lido):
  • SSD: 30 × 100 µs = 3 ms por busca.
  • HD: 30 × 10 ms = 300 ms por busca — quase um terço de segundo só para achar uma conta.
Com uma B-Tree (grau mínimo t ≈ 170, cabem ~340 chaves por bloco de 4KB; altura ≈ 4 para 1 bilhão de chaves):
  • SSD: 4 × 100 µs = 0,4 ms — cerca de 7,5× mais rápido que a AVL.
  • HD: 4 × 10 ms = 40 ms — mesma vantagem de 7,5×, e já viável num sistema interativo.
A diferença entre 30 e 4 leituras de disco é a diferença entre um sistema lento e um responsivo — e é só isso que a B-Tree faz: trocar "1 chave por nó" por "centenas de chaves por nó" para achatar a árvore.

A solução é mudar o raciocínio: em vez de guardar 1 chave por nó, guardar centenas de chaves por nó, preenchendo um bloco inteiro. Assim, cada leitura de disco traz informação máxima, e a altura da árvore despenca de 30 para 4 ou 5. Esta é a ideia central da B-Tree.


Exemplos retráteis: os exemplos desta aula ficam recolhidos (clique em "Ver exemplo" ou no título do passo). Expandir todos | Recolher todos

1. B-Tree (Árvore B)

1.1 Definição formal

Uma B-Tree é uma árvore de busca em que cada nó guarda várias chaves em ordem (e não apenas uma, como na ABP e na AVL). Ela é descrita por um número inteiro t ≥ 2, o grau mínimo (minimum degree), que controla o quão cheio ou vazio cada nó pode estar. Propriedades de uma B-Tree de grau mínimo t:

  1. Número de filhos. Todo nó interno tem no máximo 2t filhos e, exceto a raiz, no mínimo t.
  2. Número de chaves. Todo nó tem no máximo 2t−1 chaves e, exceto a raiz, no mínimo t−1.
  3. Raiz. Pode ter menos chaves que os demais nós (no mínimo 1); se for interna (não folha), precisa ter pelo menos 2 filhos.
  4. Balanceamento. Todas as folhas estão na mesma profundidade: a árvore é perfeitamente balanceada em altura.
  5. Ordem. As chaves dentro de cada nó estão em ordem crescente.
  6. Chaves e filhos. Um nó interno com k chaves possui exatamente k+1 filhos, cada um responsável por um intervalo de valores: o 1.º filho guarda as chaves menores que a 1.ª chave do nó; o i-ésimo filho guarda as chaves entre a (i−1)-ésima e a i-ésima chave do nó; o último guarda as chaves maiores que a última chave.
Grau mínimo Chaves por nó (não-raiz) Filhos por nó interno (não-raiz) Chaves na raiz
t (geral)t−1 a 2t−1t a 2t1 a 2t−1
t = 21 a 32 a 41 a 3
t = 32 a 53 a 61 a 5
t = 170169 a 339170 a 3401 a 339

Exemplo da propriedade 6: um nó [20, 40, 60] tem 3 chaves e portanto 4 filhos: o 1.º guarda valores < 20; o 2.º, valores entre 20 e 40; o 3.º, entre 40 e 60; o 4.º, valores > 60.

Por que essas regras? O máximo (2t−1) é o que cabe num bloco de disco. O mínimo (t−1) impede nós quase vazios, garantindo que cada bloco lido seja bem aproveitado e que a árvore seja baixa. A regra das folhas no mesmo nível garante que toda busca percorre o mesmo número de nós. As operações das seções seguintes (divisão, empréstimo e fusão) existem justamente para manter essas propriedades verdadeiras depois de cada inserção e remoção.

Exemplo prático: Um bloco de disco de 4KB guarda chaves inteiras (4 bytes cada) e ponteiros de 64 bits (8 bytes cada). Em um nó com m chaves e m+1 ponteiros: 4m + 8(m+1) ≤ 4096, logo m ≤ 340. Portanto t ≈ 170 — cada nó da B-Tree cabe num bloco e carrega ~340 chaves. Isso é dramaticamente diferente de 1 chave por nó (AVL).

Nota: outros livros usam convenções diferentes (por exemplo, a ordem m = máximo de filhos, com m = 2t). Aqui adotamos o grau mínimo t do livro Introduction to Algorithms (Cormen, Leiserson, Rivest e Stein), conhecido pela sigla CLRS, formada pelas iniciais dos autores.

1.2 Estrutura e exemplo usado na seção

Para desenhar, usamos t = 2: cada nó tem de 1 a 3 chaves (a raiz pode ter 1 a 3) e, se for interno, de 2 a 4 filhos. Dizemos que um nó estoura quando passa de 3 chaves (4 chaves) e que fica abaixo do mínimo quando tem menos de 1 chave (0 chaves, vazio). Em um nó, as chaves ficam lado a lado e os filhos penduram entre elas: um nó com k chaves tem k+1 filhos.

Exemplo usado em toda a seção: uma B-Tree de 5 nós com as chaves 10, 20, ..., 90 (a raiz e 4 folhas).

20 40 60 10 30 50 70 80 90

Verificando as propriedades:

  • Raiz [20, 40, 60]: 3 chaves = 2t−1, o máximo permitido (a raiz tem 4 filhos, entre 2 e 2t).
  • Folhas [10], [30] e [50]: 1 chave = t−1, o mínimo permitido. Folha [70, 80, 90]: 3 chaves, o máximo.
  • Todas as folhas estão na profundidade 1, e as chaves estão em ordem dentro de cada nó.
  • Guias corretas: a folha [10] só tem valores < 20; [30] está entre 20 e 40; [50] entre 40 e 60; [70, 80, 90] só tem valores > 60.

Esta árvore é exatamente o que se obtém inserindo 10, 20, 30, ..., 90 nessa ordem a partir da árvore vazia (veja o exemplo completo, passos 1 a 9). Numa ABP, essa mesma ordem de inserção produziria uma lista encadeada.

Verde = caminho percorrido (verde forte = encontrado); vermelho = chave a remover, ou nó com problema (estourado ou abaixo do mínimo, tracejado); amarelo = nó novo ou alterado; amarelo forte = chave inserida ou que mudou de lugar.

1.3 Inserção

Inserção em uma B-Tree:

  1. Localizar a folha onde a nova chave deve ser inserida, descendo a partir da raiz.
  2. Inserir a chave na posição correta, mantendo a ordenação.
  3. Se o nó ultrapassar o máximo de chaves (2t−1), ele estoura: realizar um split (divisão), deixando as t−1 chaves menores num nó e as t maiores em outro.
  4. Promover a chave mediana (a t-ésima) para o nó pai.
  5. Se o pai também ultrapassar o máximo, repetir o processo, subindo até a raiz. Se a raiz estourar, cria-se uma nova raiz (é a única forma de a altura crescer).

Ideia principal: inserir na folha e dividir os nós quando necessário, mantendo a árvore balanceada. Os casos:

Caso 1 — A folha tem espaço

Basta inserir a chave na posição certa. Nada mais muda.

Ver exemplo: Inserir 35

Raiz [20, 40, 60]: 20 < 35 < 40, desce para a folha [30], que tem 1 chave e comporta até 3: vira [30, 35].

20 40 60 10 30 50 70 80 90
1. Antes (caminho percorrido em verde)
20 40 60 10 30 35 50 70 80 90
2. Depois (35 cabe na folha: nada é dividido)

Caso 2 — A folha estoura e o pai tem espaço

Divide-se a folha; a chave do meio sobe e entra em ordem no pai, que continua com no máximo 3 chaves. (Se a folha for a própria raiz, não há pai: a chave que sobe forma uma nova raiz.)

Ver exemplo: Inserir 60 numa árvore com raiz [20] e folhas [10] e [30, 40, 50]

Essa é a árvore do passo 5 do exemplo completo (a árvore base, com a raiz cheia, não serve aqui, porque o pai não teria espaço). 60 vai para [30, 40, 50], que estoura com 4 chaves. A 2.ª chave (40) sobe e as demais se separam em [30] e [50, 60]. O pai [20] tinha espaço e vira [20, 40].

20 10 30 40 50
20 10 30 40 50
1. Antes (caminho percorrido em verde)
20 10 30 40 50 60
2. Insere 60 na folha: ela estoura (4 chaves; o máximo é 3)
20 40 10 30 50 60
3. Depois: Divide a folha: 40 sobe para o pai

Caso 3 — A divisão se propaga: o pai também estoura

Quando a chave que sobe faz o pai passar de 3 chaves, divide-se o pai do mesmo jeito, e assim por diante. Se a propagação chegar à raiz, nasce uma nova raiz e a altura aumenta em 1.

Ver exemplo: Inserir 85 na árvore base

85 vai para a folha [70, 80, 90], que estoura. Sobe 80 e a folha vira [70] e [85, 90]. A raiz [20, 40, 60] recebe 80, fica com 4 chaves e também estoura: sobe 40, que forma a nova raiz; sobram [20] (filhos [10] e [30]) e [60, 80] (filhos [50], [70] e [85, 90]).

20 40 60 10 30 50 70 80 90
1. Antes (caminho percorrido em verde)
20 40 60 10 30 50 70 80 85 90
2. Insere 85 na folha: ela estoura (4 chaves; o máximo é 3)
20 40 60 80 10 30 50 70 85 90
3. Divide a folha: 80 sobe para o pai; o pai estoura (4 chaves)
40 20 10 30 60 80 50 70 85 90
4. Depois: Divide a raiz: 40 sobe e forma a nova raiz (altura + 1)

Caso 4 — A chave já existe

Como as chaves são únicas, nada é inserido (a busca pela posição encontra a chave).

Ver exemplo: Inserir 50 na árvore base

A busca por 50 chega à folha [50]: a chave já está lá, então a árvore não muda.

20 40 60 10 30 50 70 80 90
1. A chave 50 já existe: nada muda

1.4 Busca

Busca em uma B-Tree:

  1. Começar pela raiz.
  2. Comparar a chave buscada com as chaves do nó atual, que estão em ordem.
  3. Se for igual a uma delas, a chave foi encontrada.
  4. Se não for e o nó for folha, a chave não existe.
  5. Caso contrário, descer para o filho cujo intervalo contém a chave e repetir.

Ideia principal: como na ABP, mas escolhendo entre até 2t caminhos por nó; cada nó visitado é uma leitura de disco, e a altura é pequena. Os casos:

Caso 1 — Encontrada num nó interno

Na B-Tree, as chaves (com seus dados) ficam em qualquer nó, então a busca pode terminar antes de chegar às folhas.

Ver exemplo: Buscar 40

A raiz [20, 40, 60] contém 40: encontrada com 1 única leitura de disco.

20 40 60 10 30 50 70 80 90
1. Busca por 40: encontrada

Caso 2 — Encontrada numa folha

Desce-se um nível por leitura até a folha que contém a chave.

Ver exemplo: Buscar 80

Raiz [20, 40, 60]: 80 > 60, então desce pelo último filho. A folha [70, 80, 90] contém 80: 2 leituras.

20 40 60 10 30 50 70 80 90
1. Busca por 80: encontrada

Caso 3 — Não encontrada

Chegamos a uma folha que não contém a chave: não há mais para onde descer.

Ver exemplo: Buscar 35

Raiz [20, 40, 60]: 20 < 35 < 40, desce pelo 2.º filho. A folha [30] não tem 35 e não tem filhos: a chave não existe (2 leituras).

20 40 60 10 30 50 70 80 90
1. Busca por 35: não encontrada

1.5 Remoção

Remoção em uma B-Tree: a remoção sempre acontece numa folha. Se a chave estiver num nó interno, troca-se por seu predecessor (maior chave da subárvore da esquerda) ou sucessor (menor da subárvore da direita), que estão em folhas, e remove-se este (de preferência o de uma folha com folga). Depois de remover da folha, existem três situações:

  1. Remoção simples: a folha continua com pelo menos t−1 chaves; nada mais a fazer.
  2. Empréstimo (redistribuição): a folha ficou com poucas chaves e um irmão adjacente tem folga; ele empresta uma chave, passando pelo pai (rotação).
  3. Fusão (merge): nenhum irmão pode emprestar; junta-se a folha, o irmão e a chave separadora do pai num único nó. O pai perde uma chave e pode ficar com poucas, repetindo o problema um nível acima.

Se o problema chegar à raiz e ela ficar vazia, seu único filho vira a nova raiz (a altura diminui).

Ideia principal: manter todos os nós dentro dos limites de ocupação (pelo menos t−1 chaves) e preservar o balanceamento da árvore. Os casos:

Caso 1 — A chave não existe

A busca termina numa folha sem a chave: nada muda.

Ver exemplo: Remover 55 da árvore base

Raiz [20, 40, 60]: 40 < 55 < 60, desce para [50]. Não há 55.

20 40 60 10 30 50 70 80 90
1. A chave 55 não existe: nada muda

Caso 2 — A chave está numa folha com folga

A folha tem mais que t−1 chaves: remove-se a chave e pronto.

Ver exemplo: Remover 80 da árvore base

A folha [70, 80, 90] tem 3 chaves; sem o 80 sobram 2, mais que o mínimo (1).

20 40 60 10 30 50 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
20 40 60 10 30 50 70 90
2. Depois (80 removido da folha, que continua com chaves suficientes)

Caso 3 — A chave está num nó interno

Troca-se a chave pelo sucessor (ou predecessor) e remove-se este da folha. Se a folha ficar abaixo do mínimo, aplicam-se os casos 4 a 6.

Ver exemplo: Remover 60 da árvore base

60 está na raiz. Seu predecessor (50) está numa folha no mínimo; o sucessor (70) está na folha [70, 80, 90], que tem folga. Troca-se 60 por 70 e remove-se 70 da folha, que vira [80, 90].

20 40 60 10 30 50 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
20 40 70 10 30 50 70 80 90
2. Troca 60 pelo sucessor 70 (que está numa folha)
20 40 70 10 30 50 80 90
3. Depois (70 removido da folha, que continua com chaves suficientes)

Caso 4 — A folha fica abaixo do mínimo e um irmão pode emprestar

Rotação: a chave separadora do pai desce para a folha e a chave do irmão mais próxima dela sobe para o pai. O irmão perde uma chave, a folha ganha uma, o pai mantém o mesmo número.

Ver exemplo: Remover 50 da árvore base

Sem o 50, a folha fica vazia. O irmão da esquerda, [30], está no mínimo e não pode emprestar; o da direita, [70, 80, 90], pode. O separador 60 desce para a folha e 70 sobe para o pai.

20 40 60 10 30 50 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
20 40 60 10 30 70 80 90
2. Remove 50 da folha: ela fica abaixo do mínimo (mínimo: 1)
20 40 70 10 30 60 80 90
3. Depois: Empréstimo do irmão direito (rotação): 60 desce do pai e 70 sobe do irmão

Caso 5 — A folha fica abaixo do mínimo, nenhum irmão pode emprestar e o pai tem folga

Fusão: a folha, o irmão e a chave separadora do pai viram um único nó (com no máximo (t−1) + 1 + (t−1) = 2t−1 chaves, sempre cabe). O pai perde uma chave; como tinha mais que o mínimo, termina.

Ver exemplo: Remover 10 da árvore base

A folha [10] fica vazia e seu único irmão, [30], está no mínimo. Funde-se [ ] + 20 (que desce da raiz) + [30] = [20, 30]. A raiz passa de 3 para 2 chaves, ainda válida.

20 40 60 10 30 50 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
20 40 60 30 50 70 80 90
2. Remove 10 da folha: ela fica abaixo do mínimo (mínimo: 1)
40 60 20 30 50 70 80 90
3. Depois: Funde os dois nós com o separador 20 que desce do pai

Caso 6 — A fusão se propaga: o pai também fica abaixo do mínimo

O pai passa por um empréstimo (6a) ou por outra fusão (6b), como se fosse uma folha. A diferença é que um nó interno também carrega filhos: na rotação, o filho que fica "entre" as chaves muda de pai; na fusão, os filhos dos dois nós ficam juntos. Se a raiz esvaziar, a árvore perde um nível.

Ver exemplo 6a: Remover 10 da árvore de 3 níveis obtida ao inserir 85 (caso 3)

A folha [10] fica vazia e funde-se com [30] e o separador 20: [20, 30]. O pai [20] fica sem chaves. O irmão [60, 80] tem folga e empresta por rotação: 40 desce da raiz e 60 sobe para a raiz; o filho [50], que estava à esquerda de 60, passa a ser filho do nó [40].

40 20 10 30 60 80 50 70 85 90
1. Antes (caminho em verde; chave a remover em vermelho)
40 20 30 60 80 50 70 85 90
2. Remove 10 da folha: ela fica abaixo do mínimo (mínimo: 1)
40 20 30 60 80 50 70 85 90
3. Funde os dois nós com o separador 20 que desce do pai; o pai fica abaixo do mínimo
60 40 20 30 50 80 70 85 90
4. Depois: Empréstimo do irmão direito (rotação): 40 desce do pai e 60 sobe do irmão
Ver exemplo 6b: Remover 10 numa B-Tree de 3 níveis com todos os nós no mínimo

Aqui nenhum nó pode emprestar. A fusão das folhas deixa o pai [20] vazio; ele se funde com o irmão [50] e o separador 30 que desce da raiz, e a raiz esvazia. Resultado: a altura cai de 2 para 1.

30 20 10 25 50 40 60
1. Antes (caminho em verde; chave a remover em vermelho)
30 20 25 50 40 60
2. Remove 10 da folha: ela fica abaixo do mínimo (mínimo: 1)
30 20 25 50 40 60
3. Funde os dois nós com o separador 20 que desce do pai; o pai fica abaixo do mínimo
30 50 20 25 40 60
4. Funde os dois nós com o separador 30 que desce do pai; a raiz fica sem chaves
30 50 20 25 40 60
5. Depois: Funde os dois nós com o separador 30 que desce do pai; o único filho da raiz vazia vira a nova raiz (altura − 1)

1.6 Exemplo completo: todos os casos em sequência

Exemplos retráteis: os exemplos desta aula ficam recolhidos (clique em "Ver exemplo" ou no título do passo). Expandir todos | Recolher todos

Uma sequência de operações sobre uma única B-Tree (t = 2), partindo da árvore vazia. Cada passo indica o caso (das seções acima) que exemplifica; no conjunto, os passos cobrem todos os casos de busca, inserção e remoção. Nos desenhos, cada passo mostra a árvore após a operação (nas remoções, antes e depois). Ao fim do passo 9 temos a árvore base de 5 nós.

OperaçãoCasoPasso(s)
InserçãoCaso 1 — a folha tem espaço1, 2, 3, 5, 7, 9, 11
InserçãoCaso 2 — a folha estoura e o pai tem espaço (ou nasce a raiz)4, 6, 8
InserçãoCaso 3 — a divisão se propaga10
InserçãoCaso 4 — a chave já existe12
BuscaCaso 1 — encontrada num nó interno13
BuscaCaso 2 — encontrada numa folha14
BuscaCaso 3 — não encontrada15
RemoçãoCaso 1 — a chave não existe16
RemoçãoCaso 2 — folha com folga17
RemoçãoCaso 3 — chave em nó interno18
RemoçãoCaso 4 — empréstimo do irmão21
RemoçãoCaso 5 — fusão com o pai folgado22
RemoçãoCaso 6a — a fusão se propaga e o pai empresta do irmão19
RemoçãoCaso 6b — fusão que se propaga até a raiz20

Verde = caminho percorrido (verde forte = encontrado); vermelho = chave a remover, ou nó com problema (estourado ou abaixo do mínimo, tracejado); amarelo = nó novo ou alterado; amarelo forte = chave inserida ou que mudou de lugar.

Passo 1 — Inserir 10 (Inserção, caso 1)

Árvore vazia: cria-se um único nó, que é ao mesmo tempo raiz e folha, com a chave 10.

10
1. Depois (10 cabe na folha: nada é dividido)
Passo 2 — Inserir 20 (Inserção, caso 1)

A raiz-folha tem espaço (o máximo é 3 chaves): 20 entra em ordem.

10 20
1. Depois (20 cabe na folha: nada é dividido)
Passo 3 — Inserir 30 (Inserção, caso 1)

30 também cabe. A raiz-folha fica cheia, com 3 chaves = 2t−1.

10 20 30
1. Depois (30 cabe na folha: nada é dividido)
Passo 4 — Inserir 40 (Inserção, caso 2)

Inserir 40 deixaria a raiz com 4 chaves, [10, 20, 30, 40]: ela estoura. A 2ª chave (20) sobe; as menores ficam à esquerda ([10]) e as maiores à direita ([30, 40]). Como não existia pai, 20 forma uma nova raiz: a altura passa de 0 para 1.

20 10 30 40
1. Depois: Divide a folha (que era a raiz): 20 sobe e forma a nova raiz (altura + 1)
Passo 5 — Inserir 50 (Inserção, caso 1)

50 > 20: vai para o filho da direita, [30, 40], que tem espaço.

20 10 30 40 50
1. Depois (50 cabe na folha: nada é dividido)
Passo 6 — Inserir 60 (Inserção, caso 2)

60 vai para [30, 40, 50], que estoura. A 2ª chave (40) sobe; sobram [30] e [50, 60]. O pai [20] tinha espaço e vira [20, 40]: a divisão para aí.

20 40 10 30 50 60
1. Depois: Divide a folha: 40 sobe para o pai
Passo 7 — Inserir 70 (Inserção, caso 1)

70 vai para [50, 60], que tem espaço: [50, 60, 70].

20 40 10 30 50 60 70
1. Depois (70 cabe na folha: nada é dividido)
Passo 8 — Inserir 80 (Inserção, caso 2)

80 estoura [50, 60, 70, 80]. Sobe 60; ficam [50] e [70, 80]. A raiz vira [20, 40, 60], cheia mas válida. A árvore tem agora exatamente 5 nós.

20 40 60 10 30 50 70 80
1. Depois: Divide a folha: 60 sobe para o pai
Passo 9 — Inserir 90 (Inserção, caso 1)

90 cabe em [70, 80, 90]. Esta é a árvore base de 5 nós da seção 1.2 (inserir as chaves em ordem crescente produz uma árvore perfeitamente balanceada, ao contrário da ABP).

20 40 60 10 30 50 70 80 90
1. Depois (90 cabe na folha: nada é dividido)
Passo 10 — Inserir 85 (Inserção, caso 3)

85 vai para [70, 80, 90], que estoura; sobe 80. A raiz [20, 40, 60] recebe 80 e também estoura; sobe 40, que forma a nova raiz. A divisão se propagou até o topo e a altura passou de 1 para 2.

40 20 10 30 60 80 50 70 85 90
1. Depois: Divide a raiz: 40 sobe e forma a nova raiz (altura + 1)
Passo 11 — Inserir 95 (Inserção, caso 1)

95 desce por [40] → [60, 80] até a folha [85, 90], que tem espaço.

40 20 10 30 60 80 50 70 85 90 95
1. Depois (95 cabe na folha: nada é dividido)
Passo 12 — Inserir 50 (Inserção, caso 4)

A chave 50 já existe na folha [50]: nada muda.

40 20 10 30 60 80 50 70 85 90 95
1. A chave 50 já existe: nada muda
Passo 13 — Buscar 80 (Busca, caso 1)

Raiz [40]: 80 > 40, vai para [60, 80], onde encontra 80. Foram 2 leituras de nó, sem descer até as folhas.

40 20 10 30 60 80 50 70 85 90 95
1. Busca por 80: encontrada
Passo 14 — Buscar 30 (Busca, caso 2)

Raiz [40]: 30 < 40 → [20]: 30 > 20 → folha [30]: encontrada (3 leituras).

40 20 10 30 60 80 50 70 85 90 95
1. Busca por 30: encontrada
Passo 15 — Buscar 55 (Busca, caso 3)

Raiz [40]: 55 > 40 → [60, 80]: 55 < 60 → folha [50]. Não há 55, e uma folha não tem para onde descer: não existe.

40 20 10 30 60 80 50 70 85 90 95
1. Busca por 55: não encontrada
Passo 16 — Remover 99 (Remoção, caso 1)

99 > 40, 99 > 80: a busca chega em [85, 90, 95] sem achar 99. Nada muda.

40 20 10 30 60 80 50 70 85 90 95
1. A chave 99 não existe: nada muda
Passo 17 — Remover 90 (Remoção, caso 2)

90 está numa folha com 3 chaves. Depois de removê-la sobram 2 (≥ 1, o mínimo): fim.

40 20 10 30 60 80 50 70 85 90 95
1. Antes (caminho em verde; chave a remover em vermelho)
40 20 10 30 60 80 50 70 85 95
2. Depois (90 removido da folha, que continua com chaves suficientes)
Passo 18 — Remover 80 (Remoção, caso 3)

80 está no nó interno [60, 80]. Troca-se 80 pelo seu sucessor (85, a menor chave da subárvore da direita), que está numa folha com folga ([85, 95]). Remove-se 85 da folha, que continua com 1 chave.

40 20 10 30 60 80 50 70 85 95
1. Antes (caminho em verde; chave a remover em vermelho)
40 20 10 30 60 85 50 70 95
2. Depois (85 removido da folha, que continua com chaves suficientes)
Passo 19 — Remover 10 (Remoção, caso 6a)

Remover 10 esvazia a folha [10]; o irmão [30] está no mínimo, então fundem-se com o separador 20: [20, 30]. O pai [20] fica sem chaves (abaixo do mínimo), mas o irmão dele, [60, 85], tem folga e empresta por rotação: 40 desce da raiz para o pai e 60 sobe do irmão para a raiz. O filho [50], que estava à esquerda da chave 60 no irmão, passa a ser filho do nó [40]. A altura não muda.

40 20 10 30 60 85 50 70 95
1. Antes (caminho em verde; chave a remover em vermelho)
60 40 20 30 50 85 70 95
2. Depois: Empréstimo do irmão direito (rotação): 40 desce do pai e 60 sobe do irmão
Passo 20 — Remover 70 (Remoção, caso 6b)

Remover 70 esvazia a folha [70]; o irmão [95] está no mínimo, então fundem-se com o separador 85: [85, 95]. O pai [85] fica sem chaves e o irmão dele, [40], também está no mínimo: fundem-se com o separador 60 que desce da raiz, formando [40, 60]. A raiz [60] ficou vazia, então seu único filho vira a raiz: a altura volta de 2 para 1.

60 40 20 30 50 85 70 95
1. Antes (caminho em verde; chave a remover em vermelho)
40 60 20 30 50 85 95
2. Depois: Funde os dois nós com o separador 60 que desce do pai; o único filho da raiz vazia vira a nova raiz (altura − 1)
Passo 21 — Remover 50 (Remoção, caso 4)

50 é a única chave da folha [50]. O irmão esquerdo [20, 30] tem folga e empresta por rotação: 40 desce do pai para a folha ([40]) e 30 sobe do irmão para a raiz, que vira [30, 60].

40 60 20 30 50 85 95
1. Antes (caminho em verde; chave a remover em vermelho)
30 60 20 40 85 95
2. Depois: Empréstimo do irmão esquerdo (rotação): 40 desce do pai e 30 sobe do irmão
Passo 22 — Remover 20 (Remoção, caso 5)

20 é a única chave da folha [20] e o irmão da direita, [40], está no mínimo: fundem-se com o separador 30 que desce da raiz, formando [30, 40]. A raiz [30, 60] vira [60], que ainda tem o mínimo de 1 chave: fim.

30 60 20 40 85 95
1. Antes (caminho em verde; chave a remover em vermelho)
60 30 40 85 95
2. Depois: Funde os dois nós com o separador 30 que desce do pai

1.7 Complexidade

n = número de chaves armazenadas; t = grau mínimo (cada nó tem de t−1 a 2t−1 chaves); h = altura da árvore (número de arestas da raiz até uma folha). Como cada nó é um bloco de disco, medimos o custo principal em leituras de disco (uma por nó visitado); o custo de CPU dentro do nó vem à parte.

Pior caso = o maior custo que pode acontecer, para o pior conjunto de chaves e a pior ordem de operações. Caso médio = o custo típico, com chaves em ordem aleatória.

Operação Pior caso (disco) Caso médio (disco) Em uma frase
BuscaO(logt n)O(logt n)uma leitura por nível, da raiz até a folha
InserçãoO(logt n)O(logt n) leituras, em geral 1 escritadesce até a folha; o split é raro, mas pode subir até a raiz
RemoçãoO(logt n)O(logt n) leituras, em geral 1 escritadesce até a folha; fusões em cascata só quando todos estão no mínimo
EspaçoO(n)O(n)todo nó, exceto a raiz, está pelo menos meio cheio

Por que a altura é O(logt n)?

Intuição: cada nó interno tem pelo menos t filhos (a raiz, pelo menos 2). Então, mesmo no pior caso (todos os nós no mínimo), o número de nós multiplica por t a cada nível: 1 nó na raiz, pelo menos 2 no nível 1, pelo menos 2t no nível 2, 2t² no nível 3, e assim por diante. Contando as chaves, uma árvore de altura h tem no mínimo 2th − 1 chaves. Logo n ≥ 2th − 1, ou seja, h ≤ logt((n+1)/2). Para t = 170 e n = 109, h ≤ 3,6: no máximo 4 níveis (4 leituras de disco), contra cerca de 30 numa AVL. O caso melhor é a árvore com todos os nós cheios, de altura ≈ log2t n; na prática os nós ficam em torno de 70% cheios (para chaves inseridas em ordem aleatória), e a altura fica entre os dois extremos, sempre O(logt n).

  • Busca: uma leitura por nível, no máximo h + 1. Dentro do nó, com varredura linear, o custo de CPU é O(t) por nó, ou O(t · logt n) no total; com busca binária no nó cai para O(log2 t) por nó, o que dá O(log n) no total, igual à AVL (a vantagem da B-Tree não está na CPU, e sim no número de leituras de disco).
  • Inserção: desce h leituras até a folha e insere. Um split custa O(t) (copiar metade das chaves) e cria dois nós pela metade: a mesma folha só voltará a estourar depois de pelo menos t novas inserções nela. Por isso, em média, quase toda inserção termina na folha, sem dividir nada. No pior caso, o split sobe por todos os h níveis (como no caso 3), o que custa O(t · logt n) em CPU.
  • Remoção: também desce até uma folha (h leituras) e conserta no caminho de volta. Uma fusão só se propaga quando o irmão e o pai estão no mínimo; no caso comum, a folha tem folga (caso 2) ou o empréstimo resolve (caso 4). O pior caso (como o do passo 20) percorre os h níveis, com custo O(t · logt n) em CPU.
  • Espaço: como todo nó (menos a raiz) tem pelo menos metade das chaves possíveis, o espaço desperdiçado é no máximo cerca de metade.

1.8 Estrutura e funções em C (TAD simples)

Uma forma simples de representar a B-Tree em C: cada nó guarda um vetor de chaves, um vetor de filhos (ignorado se for folha) e um contador de quantas chaves estão em uso. O grau mínimo t fixa os tamanhos máximos: até 2t−1 chaves e 2t filhos por nó.

#define T 2   /* grau mínimo da árvore */

typedef struct NodoB {
    int  n;                       /* número de chaves em uso */
    int  chaves[2*T - 1];          /* chaves em ordem crescente */
    struct NodoB *filhos[2*T];      /* filhos (não usados se for folha) */
    int  folha;                    /* 1 se for folha, 0 caso contrário */
} NodoB;

NodoB *criarNodoB(int folha);                  /* cria um nó vazio (folha ou interno) */
NodoB *buscarB(NodoB *raiz, int chave);           /* devolve o nó onde a chave está, ou NULL */
NodoB *inserirB(NodoB *raiz, int chave);          /* insere a chave; devolve a (nova) raiz */
NodoB *removerB(NodoB *raiz, int chave);          /* remove a chave; devolve a (nova) raiz */
void   destruirB(NodoB *raiz);                   /* libera todos os nós */

main de demonstração:

int main(void) {
    NodoB *raiz = criarNodoB(1);                 /* raiz começa como folha vazia */

    int chaves[] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
    for (int i = 0; i < 9; i++)
        raiz = inserirB(raiz, chaves[i]);

    printf("buscarB(70) = %s\n", buscarB(raiz, 70) ? "achou" : "não achou");  /* achou */
    printf("buscarB(75) = %s\n", buscarB(raiz, 75) ? "achou" : "não achou");  /* não achou */

    raiz = removerB(raiz, 60);
    destruirB(raiz);
    return 0;
}

2. B+-Tree (Árvore B+)

2.1 A diferença fundamental

Na B-Tree, os dados (valores dos registros) podem estar em qualquer nó — interno ou folha. Na B+-Tree, há uma separação rigorosa:

B+-Tree:
• Nós internos guardam apenas chaves de navegação — servem exclusivamente para guiar a busca.
• Folhas guardam todos os dados (pares chave/valor ou ponteiros para os registros).
• As folhas são ligadas em uma lista duplamente encadeada → percurso sequencial sem subir na árvore.

Por que isso é melhor para bancos de dados?

  • Nós internos mais densos: sem dados, os nós internos cabem mais chaves por bloco → árvore mais larga → altura menor → menos leituras de disco.
  • Consultas por intervalo eficientes: para buscar todos os registros com chave entre A e B, navegamos até a folha com A e escaneamos a lista encadeada de folhas até passar B. Isso é O(logt n + k), onde k é o número de resultados. Na B-Tree comum, cada resultado poderia estar num nó interno diferente, exigindo várias subidas e descidas na árvore.
  • Varredura sequencial: percorrer todos os registros em ordem = percorrer a lista encadeada de folhas, sem tocar nos nós internos.

2.2 Estrutura e exemplo usado na seção

Mantemos t = 2. Os nós internos têm de 1 a 3 chaves de navegação (os separadores) e de 2 a 4 filhos. As folhas têm de t a 2t−1 chaves (2 ou 3): assim, uma folha que estoura (4 chaves) se divide em duas folhas de exatamente 2. Um nó interno com separadores s1 < s2 < ... leva ao filho i as chaves k com si ≤ k < si+1: uma chave igual a um separador vai para a direita. As setas ↔ entre as folhas são a lista encadeada.

Exemplo usado em toda a seção: uma B+-Tree de 5 nós com as mesmas chaves 10, 20, ..., 90 (raiz interna e 4 folhas). Nos desenhos, nós internos em cinza e folhas em azul.

30 50 70 10 20 30 40 50 60 70 80 90

Observe que 30, 50 e 70 aparecem na raiz e nas folhas. Isso é intencional: na B+-Tree, as chaves dos nós internos são cópias que servem apenas como separadores. Todos os dados reais estão nas folhas. Por serem só placas de sinalização, um separador pode continuar na árvore mesmo depois de a chave ter sido removida da folha (veja a remoção).

Verde = caminho percorrido (verde forte = encontrado); vermelho = chave a remover, ou nó com problema (estourado ou abaixo do mínimo, tracejado); amarelo = nó novo ou alterado; amarelo forte = chave inserida, copiada ou que mudou de lugar.

2.3 Inserção

Inserção em uma B+-Tree:

  1. Localizar a folha onde a nova chave deve ser inserida (chave igual a um separador vai para a direita).
  2. Inserir a chave na posição correta, mantendo a ordenação.
  3. Se a folha ultrapassar o máximo de chaves (2t−1), realizar um split: duas folhas com t chaves cada.
  4. Copiar a menor chave da folha da direita para o nó pai (ela continua na folha, porque o dado só existe lá).
  5. Se o pai também ultrapassar o máximo, dividir o nó interno: a chave do meio sobe, sem cópia. Repetir até a raiz, podendo criar uma nova raiz.

Ideia principal: inserir na folha e dividir quando necessário; na divisão de folha a chave é copiada para o pai, e na divisão de nó interno ela é promovida. Os casos:

Caso 1 — A folha tem espaço

Ver exemplo: Inserir 25 na árvore base

Raiz [30, 50, 70]: 25 < 30, desce para [10, 20], que vira [10, 20, 25]. Os separadores não mudam.

30 50 70 10 20 30 40 50 60 70 80 90
1. Antes (caminho percorrido em verde)
30 50 70 10 20 25 30 40 50 60 70 80 90
2. Depois (25 cabe na folha: nada é dividido)

Caso 2 — A folha estoura e o pai tem espaço

Divide-se a folha em duas e copia-se a menor chave da direita para o pai. (Se a folha for a raiz, a cópia forma uma nova raiz.)

Ver exemplo: Inserir 60 numa árvore com raiz [30] e folhas [10, 20] e [30, 40, 50]

Essa é a árvore do passo 5 do exemplo completo (a raiz da árvore base está cheia). 60 vai para [30, 40, 50], que estoura: dividem-se em [30, 40] e [50, 60] e 50 é copiado para o pai, que vira [30, 50].

30 10 20 30 40 50
30 10 20 30 40 50
1. Antes (caminho percorrido em verde)
30 10 20 30 40 50 60
2. Insere 60 na folha: ela estoura (4 chaves; o máximo é 3)
30 50 10 20 30 40 50 60
3. Depois: Divide a folha: 50 é copiado para o pai

Caso 3 — A divisão se propaga: o pai também estoura

O nó interno que estoura (4 separadores) é dividido: a 2.ª chave sobe (sem cópia) e as outras ficam repartidas. Se for a raiz, nasce uma nova raiz e a altura aumenta em 1.

Ver exemplo: Inserir 85 na árvore base

85 vai para [70, 80, 90], que estoura: dividem-se em [70, 80] e [85, 90], e 85 é copiado para a raiz, que passa a [30, 50, 70, 85] e também estoura. A 2.ª chave (50) sobe e forma a nova raiz; sobram [30] e [70, 85].

30 50 70 10 20 30 40 50 60 70 80 90
1. Antes (caminho percorrido em verde)
30 50 70 10 20 30 40 50 60 70 80 85 90
2. Insere 85 na folha: ela estoura (4 chaves; o máximo é 3)
30 50 70 85 10 20 30 40 50 60 70 80 85 90
3. Divide a folha: 85 é copiado para o pai; o pai estoura (4 chaves)
50 30 10 20 30 40 70 85 50 60 70 80 85 90
4. Depois: Divide a raiz: 50 sobe e forma a nova raiz (altura + 1)

Caso 4 — A chave já existe

Ver exemplo: Inserir 40 na árvore base

A busca chega à folha [30, 40] e a chave já está lá: nada muda.

30 50 70 10 20 30 40 50 60 70 80 90
1. A chave 40 já existe: nada muda

2.4 Busca

Busca em uma B+-Tree:

  1. Começar pela raiz.
  2. Em cada nó interno, comparar a chave com os separadores e descer pelo filho certo (chave igual a um separador vai para a direita).
  3. Repetir até chegar a uma folha: a busca sempre desce até ela, mesmo que a chave apareça como separador.
  4. Procurar a chave na folha: se estiver lá, foi encontrada; senão, não existe.

Ideia principal: os dados só ficam nas folhas, então toda busca percorre o mesmo número de nós. Os casos:

Caso 1 — A chave é igual a um separador

A busca não termina no nó interno: segue pelo filho da direita e encontra a chave na folha.

Ver exemplo: Buscar 50

A raiz [30, 50, 70] tem 50 como separador, mas 50 ≥ 50 manda a busca para o 3.º filho. A folha [50, 60] contém 50: encontrada, com 2 leituras.

30 50 70 10 20 30 40 50 60 70 80 90
1. Busca por 50: encontrada

Caso 2 — A chave está numa folha (e não é separador)

Ver exemplo: Buscar 80

Raiz [30, 50, 70]: 80 ≥ 70, desce pelo último filho. A folha [70, 80, 90] contém 80.

30 50 70 10 20 30 40 50 60 70 80 90
1. Busca por 80: encontrada

Caso 3 — Não encontrada

A folha em que a chave deveria estar não a contém.

Ver exemplo: Buscar 45

Raiz [30, 50, 70]: 30 ≤ 45 < 50, desce para a folha [30, 40], que não tem 45.

30 50 70 10 20 30 40 50 60 70 80 90
1. Busca por 45: não encontrada

Busca por intervalo

É aqui que a B+-Tree brilha. Desce-se uma única vez até a folha que contém (ou conteria) o início do intervalo e, daí em diante, percorre-se a lista encadeada de folhas coletando as chaves até passar do fim, sem voltar aos nós internos.

Ver exemplo: Buscar todas as chaves entre 35 e 75
  1. Raiz [30, 50, 70]: 30 ≤ 35 < 50, desce para a folha [30, 40]. As chaves ≥ 35 dela: 40.
  2. Segue o ponteiro para a folha [50, 60]: coleta 50 e 60.
  3. Segue o ponteiro para [70, 80, 90]: coleta 70; 80 > 75, então para.

Resultado: {40, 50, 60, 70}. Custo: 1 descida + 3 folhas = O(logt n + k).

30 50 70 10 20 30 40 50 60 70 80 90
1. Busca por intervalo [35, 75]

Compare com uma ABP balanceada: cada um dos k resultados custaria uma nova busca, O(k log n); e numa B-Tree comum os k resultados podem estar em nós internos e folhas misturados, exigindo subidas e descidas.

2.5 Remoção

Remoção em uma B+-Tree: toda chave está numa folha, então a remoção sempre começa nela (não há troca por predecessor ou sucessor). Os separadores dos nós internos não precisam ser apagados, pois continuam sendo guias válidos. Depois de remover da folha, existem três situações:

  1. Remoção simples: a folha continua com pelo menos t chaves; nada mais a fazer.
  2. Empréstimo (redistribuição): a folha ficou com poucas chaves e um irmão adjacente tem folga; ele empresta uma chave e o separador do pai é atualizado.
  3. Fusão (merge): nenhum irmão pode emprestar; juntam-se as duas folhas e o separador do pai é descartado. O pai perde uma chave e pode ficar com poucas, repetindo o problema um nível acima (nos nós internos, empréstimo e fusão funcionam como na B-Tree: o separador passa pelo pai ou desce).

Se o problema chegar à raiz e ela ficar vazia, seu único filho vira a nova raiz (a altura diminui).

Ideia principal: manter todos os nós dentro dos limites de ocupação e preservar o balanceamento da árvore, com os dados sempre nas folhas. Os casos:

Caso 1 — A chave não existe

Ver exemplo: Remover 45 da árvore base

A busca chega à folha [30, 40], que não tem 45. Nada muda.

30 50 70 10 20 30 40 50 60 70 80 90
1. A chave 45 não existe: nada muda

Caso 2 — A folha tem folga

A folha tem mais que t chaves: remove-se a chave. Mesmo que ela seja um separador, o separador permanece no nó interno.

Ver exemplo: Remover 70 da árvore base

A folha [70, 80, 90] vira [80, 90], com 2 chaves (o mínimo). O 70 continua na raiz como separador: tudo que é ≥ 70 ainda vai para essa folha e tudo que é < 70 vai para a anterior, então a busca continua correta.

30 50 70 10 20 30 40 50 60 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
30 50 70 10 20 30 40 50 60 80 90
2. Depois (70 removido da folha, que continua com chaves suficientes)

Caso 3 — A folha fica abaixo do mínimo e um irmão pode emprestar

A folha recebe a chave do irmão mais próxima dela e o separador do pai é atualizado para a nova menor chave da folha da direita.

Ver exemplo: Remover 60 da árvore base

[50, 60] vira [50], abaixo do mínimo (2). O irmão da esquerda, [30, 40], está no mínimo; o da direita, [70, 80, 90], tem folga e empresta 70. A folha vira [50, 70] e o separador entre as duas, que era 70, passa a ser 80.

30 50 70 10 20 30 40 50 60 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
30 50 70 10 20 30 40 50 70 80 90
2. Remove 60 da folha: ela fica abaixo do mínimo (mínimo: 2)
30 50 80 10 20 30 40 50 70 80 90
3. Depois: Empréstimo do irmão direito: 70 passa para a folha e o separador do pai vira 80

Caso 4 — A folha fica abaixo do mínimo, nenhum irmão pode emprestar e o pai tem folga

Fundem-se as duas folhas (cabem: 2 + 1 ≤ 3) e o separador do pai que as dividia é descartado (diferente da B-Tree, em que a chave desce).

Ver exemplo: Remover 10 da árvore base

[10, 20] vira [20] e seu irmão [30, 40] está no mínimo. Fundem-se em [20, 30, 40] e o separador 30 sai da raiz, que vira [50, 70].

30 50 70 10 20 30 40 50 60 70 80 90
1. Antes (caminho em verde; chave a remover em vermelho)
30 50 70 20 30 40 50 60 70 80 90
2. Remove 10 da folha: ela fica abaixo do mínimo (mínimo: 2)
50 70 20 30 40 50 60 70 80 90
3. Depois: Funde as duas folhas; o separador 30 é descartado do pai

Caso 5 — A fusão se propaga: o pai também fica abaixo do mínimo

O nó interno que perdeu o separador fica sem chaves. Ele pede emprestado ao irmão (5a, por rotação pelo pai) ou se funde com o irmão, com o separador do avô descendo (5b). Se a raiz esvaziar, a árvore perde um nível.

Ver exemplo 5a: Remover 10 da árvore de 3 níveis obtida ao inserir 85 (caso 3)

As folhas [20] e [30, 40] se fundem em [20, 30, 40] e o separador 30 sai do pai, que fica sem chaves. O irmão [70, 85] tem folga e empresta por rotação: 50 desce da raiz e 70 sobe para a raiz; a folha [50, 60] passa a ser filha do nó [50].

50 30 10 20 30 40 70 85 50 60 70 80 85 90
1. Antes (caminho em verde; chave a remover em vermelho)
50 30 20 30 40 70 85 50 60 70 80 85 90
2. Remove 10 da folha: ela fica abaixo do mínimo (mínimo: 2)
50 20 30 40 70 85 50 60 70 80 85 90
3. Funde as duas folhas; o separador 30 é descartado do pai; o pai fica abaixo do mínimo
70 50 20 30 40 50 60 85 70 80 85 90
4. Depois: Empréstimo do irmão direito (rotação): 50 desce do pai e 70 sobe do irmão
Ver exemplo 5b: Remover 10 numa B+-Tree de 3 níveis com todos os nós no mínimo

Nenhum nó pode emprestar. As folhas se fundem e o pai [30] fica vazio; os nós internos [ ] e [70] se fundem com o separador 50 que desce da raiz, e a raiz esvazia. A altura cai de 2 para 1.

50 30 10 20 30 40 70 50 60 70 80
1. Antes (caminho em verde; chave a remover em vermelho)
50 30 20 30 40 70 50 60 70 80
2. Remove 10 da folha: ela fica abaixo do mínimo (mínimo: 2)
50 20 30 40 70 50 60 70 80
3. Funde as duas folhas; o separador 30 é descartado do pai; o pai fica abaixo do mínimo
50 70 20 30 40 50 60 70 80
4. Funde os dois nós com o separador 50 que desce do pai; a raiz fica sem chaves
50 70 20 30 40 50 60 70 80
5. Depois: Funde os dois nós com o separador 50 que desce do pai; o único filho da raiz vazia vira a nova raiz (altura − 1)

2.6 Exemplo completo: todos os casos em sequência

Exemplos retráteis: os exemplos desta aula ficam recolhidos (clique em "Ver exemplo" ou no título do passo). Expandir todos | Recolher todos

Uma sequência de operações sobre uma única B+-Tree (t = 2), partindo da árvore vazia. Cada passo indica o caso que exemplifica; no conjunto, os passos cobrem todos os casos, exceto o 5a da remoção (visto na seção anterior). Nos desenhos, cada passo mostra a árvore após a operação (nas remoções, antes e depois). Ao fim do passo 9 temos a árvore base de 5 nós.

OperaçãoCasoPasso(s)
InserçãoCaso 1 — a folha tem espaço1, 2, 3, 5, 7, 9, 11, 12
InserçãoCaso 2 — a folha estoura e o pai tem espaço (ou nasce a raiz)4, 6, 8
InserçãoCaso 3 — a divisão se propaga10
InserçãoCaso 4 — a chave já existe13
BuscaCaso 1 — chave igual a um separador14
BuscaCaso 2 — chave numa folha15
BuscaCaso 3 — não encontrada16
BuscaPor intervalo17
RemoçãoCaso 1 — a chave não existe18
RemoçãoCaso 2 — folha com folga19
RemoçãoCaso 3 — empréstimo do irmão20
RemoçãoCaso 4 — fusão com o pai folgado21
RemoçãoCaso 5b — fusão que se propaga até a raiz22

Verde = caminho percorrido (verde forte = encontrado); vermelho = chave a remover, ou nó com problema (estourado ou abaixo do mínimo, tracejado); amarelo = nó novo ou alterado; amarelo forte = chave inserida, copiada ou que mudou de lugar.

Passo 1 — Inserir 10 (Inserção, caso 1)

Árvore vazia: uma única folha, que é a raiz, com a chave 10.

10
1. Depois (10 cabe na folha: nada é dividido)
Passo 2 — Inserir 20 (Inserção, caso 1)

20 cabe na folha-raiz.

10 20
1. Depois (20 cabe na folha: nada é dividido)
Passo 3 — Inserir 30 (Inserção, caso 1)

30 cabe: a folha fica cheia (3 chaves = 2t−1).

10 20 30
1. Depois (30 cabe na folha: nada é dividido)
Passo 4 — Inserir 40 (Inserção, caso 2)

40 deixaria a folha com 4 chaves: ela estoura e se divide em duas folhas de 2 chaves, [10, 20] e [30, 40]. O separador é uma cópia de 30 (a menor chave da folha da direita), colocada numa nova raiz; 30 continua na folha. A altura passa de 0 para 1.

30 10 20 30 40
1. Depois: Divide a folha (que era a raiz): 30 é copiado para uma nova raiz (altura + 1)
Passo 5 — Inserir 50 (Inserção, caso 1)

50 ≥ 30: vai para a folha da direita, que tem espaço.

30 10 20 30 40 50
1. Depois (50 cabe na folha: nada é dividido)
Passo 6 — Inserir 60 (Inserção, caso 2)

60 estoura [30, 40, 50, 60]: dividem-se em [30, 40] e [50, 60], e 50 é copiado para o pai, que vira [30, 50].

30 50 10 20 30 40 50 60
1. Depois: Divide a folha: 50 é copiado para o pai
Passo 7 — Inserir 70 (Inserção, caso 1)

70 vai para a folha [50, 60], que tem espaço.

30 50 10 20 30 40 50 60 70
1. Depois (70 cabe na folha: nada é dividido)
Passo 8 — Inserir 80 (Inserção, caso 2)

80 estoura [50, 60, 70, 80]: [50, 60] e [70, 80], com 70 copiado para o pai, que vira [30, 50, 70] (cheio). A árvore tem 5 nós.

30 50 70 10 20 30 40 50 60 70 80
1. Depois: Divide a folha: 70 é copiado para o pai
Passo 9 — Inserir 90 (Inserção, caso 1)

90 cabe em [70, 80, 90]. Esta é a árvore base de 5 nós da seção 2.2.

30 50 70 10 20 30 40 50 60 70 80 90
1. Depois (90 cabe na folha: nada é dividido)
Passo 10 — Inserir 85 (Inserção, caso 3)

85 estoura a folha [70, 80, 85, 90]: dividem-se em [70, 80] e [85, 90], e 85 é copiado para a raiz, que estoura ([30, 50, 70, 85]). Ao dividir um nó interno a chave do meio (50) sobe, sem cópia, e forma a nova raiz.

50 30 10 20 30 40 70 85 50 60 70 80 85 90
1. Depois: Divide a raiz: 50 sobe e forma a nova raiz (altura + 1)
Passo 11 — Inserir 35 (Inserção, caso 1)

35 desce até a folha [30, 40], que tem espaço.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90
1. Depois (35 cabe na folha: nada é dividido)
Passo 12 — Inserir 95 (Inserção, caso 1)

95 vai para a folha [85, 90], que tem espaço.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. Depois (95 cabe na folha: nada é dividido)
Passo 13 — Inserir 40 (Inserção, caso 4)

A chave 40 já existe na folha [30, 35, 40]: nada muda.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. A chave 40 já existe: nada muda
Passo 14 — Buscar 50 (Busca, caso 1)

50 é igual ao separador da raiz, mas separador não é dado: como 50 ≥ 50, a busca desce pelo filho da direita, depois pela folha [50, 60] e encontra a chave na folha.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. Busca por 50: encontrada
Passo 15 — Buscar 80 (Busca, caso 2)

Raiz [50]: 80 ≥ 50 → [70, 85]: 70 ≤ 80 < 85 → folha [70, 80]: encontrada.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. Busca por 80: encontrada
Passo 16 — Buscar 55 (Busca, caso 3)

55 chega à folha [50, 60], que não tem 55: não existe.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. Busca por 55: não encontrada
Passo 17 — Buscar o intervalo [35, 75] (Busca por intervalo)

Desce-se até a folha onde estaria 35 ([30, 35, 40]) e coletam-se 35 e 40. Seguindo o ponteiro, a folha [50, 60] dá 50 e 60; a próxima, [70, 80], dá 70, e 80 > 75 encerra. Não houve nenhuma volta aos nós internos.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. Busca por intervalo [35, 75]
Passo 18 — Remover 99 (Remoção, caso 1)

99 chega à folha [85, 90, 95] e não está lá: nada muda.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. A chave 99 não existe: nada muda
Passo 19 — Remover 90 (Remoção, caso 2)

90 está numa folha com 3 chaves. Sobram 2 (o mínimo): fim.

50 30 10 20 30 35 40 70 85 50 60 70 80 85 90 95
1. Antes (caminho em verde; chave a remover em vermelho)
50 30 10 20 30 35 40 70 85 50 60 70 80 85 95
2. Depois (90 removido da folha, que continua com chaves suficientes)
Passo 20 — Remover 20 (Remoção, caso 3)

Remover 20 deixa a folha [10] com 1 chave (mínimo 2). O irmão da direita, [30, 35, 40], tem folga e empresta 30; o separador do pai passa de 30 para 35 (a nova menor chave da folha da direita).

50 30 10 20 30 35 40 70 85 50 60 70 80 85 95
1. Antes (caminho em verde; chave a remover em vermelho)
50 35 10 30 35 40 70 85 50 60 70 80 85 95
2. Depois: Empréstimo do irmão direito: 30 passa para a folha e o separador do pai vira 35
Passo 21 — Remover 70 (Remoção, caso 4)

Remover 70 deixa [80] abaixo do mínimo; os irmãos [50, 60] e [85, 95] estão no mínimo. Fundem-se as folhas [50, 60] e [80] em [50, 60, 80] e o separador 70 é descartado do pai, que vira [85] (ainda com o mínimo de 1).

50 35 10 30 35 40 70 85 50 60 70 80 85 95
1. Antes (caminho em verde; chave a remover em vermelho)
50 35 10 30 35 40 85 50 60 80 85 95
2. Depois: Funde as duas folhas; o separador 70 é descartado do pai
Passo 22 — Remover 10 (Remoção, caso 5b)

Remover 10 deixa [30] abaixo do mínimo; o irmão [35, 40] está no mínimo. Fundem-se em [30, 35, 40] e o separador 35 é descartado, deixando o pai sem chaves. O irmão do pai, [85], também está no mínimo: fundem-se os dois nós internos, com o separador 50 descendo da raiz: [50, 85]. A raiz esvaziou, então seu único filho vira a raiz: a altura volta de 2 para 1.

50 35 10 30 35 40 85 50 60 80 85 95
1. Antes (caminho em verde; chave a remover em vermelho)
50 85 30 35 40 50 60 80 85 95
2. Depois: Funde os dois nós com o separador 50 que desce do pai; o único filho da raiz vazia vira a nova raiz (altura − 1)

2.7 Complexidade

n = número de chaves; t = grau mínimo; k = número de resultados de uma busca por intervalo. Como na B-Tree, o custo principal é medido em leituras de disco (um nó por leitura).

Operação Pior caso (disco) Caso médio (disco) Em uma frase
Busca exataO(logt n)O(logt n)sempre desce até a folha: custo igual em todas as chaves
InserçãoO(logt n)O(logt n) leituras, em geral 1 escritasplit raro; só em cascata quando os nós estão cheios
RemoçãoO(logt n)O(logt n) leituras, em geral 1 escritasem troca por sucessor; fusões em cascata são raras
Busca por intervaloO(logt n + k)O(logt n + k)desce uma vez e percorre a lista de folhas
Varredura completaO(n)O(n)só a lista de folhas, sem tocar nos nós internos

Por que a busca custa sempre a mesma coisa?

Intuição: como os dados só ficam nas folhas e todas as folhas estão na mesma profundidade, toda busca percorre exatamente h + 1 nós, achando a chave ou não. Na B-Tree uma busca pode parar antes, num nó interno; na B+-Tree isso nunca acontece, mas em compensação os nós internos são mais densos (sem dados, cabem mais separadores por bloco), o que deixa a árvore mais larga e mais baixa: com blocos de 4KB, chaves de 4 bytes e ponteiros de 8 bytes, cada nó interno comporta cerca de 340 filhos, e 4 níveis alcançam bilhões de chaves. A altura segue h ≤ logt n (mesma dedução da B-Tree: cada nível multiplica o número de nós por pelo menos t).

Intervalo: o custo é a descida, O(logt n), mais a leitura das folhas que contêm os k resultados. Como cada folha guarda de t a 2t−1 chaves, são no máximo ⌈k/t⌉ + 1 folhas: no total, O(logt n + k/t) leituras (e O(k) comparações), contra O(k · logt n) da B-Tree.

Inserção e remoção: descem até uma folha e consertam no caminho de volta. Como na B-Tree, a divisão de uma folha deixa duas folhas pela metade, então a mesma folha só volta a estourar depois de várias inserções: em média, quase sempre basta escrever na folha. No pior caso (todos os nós cheios na inserção, todos no mínimo na remoção), a correção sobe por todos os h níveis.

Comparação resumida com a B-Tree e com a AVL (n = 109, t = 170):

Operação B-Tree B+-Tree AVL
Busca exata (leituras de disco) ≈ 4 (pode ser menos) ≈ 4 (sempre até a folha) ≈ 30
Inserção O(logt n) leituras O(logt n) leituras O(log n)
Remoção O(logt n) leituras O(logt n) leituras O(log n)
Busca por intervalo (k resultados) O(k · logt n) O(logt n + k) O(k · log n)
Varredura sequencial completa O(n) O(n) — lista de folhas O(n)

Note que a busca por intervalo é onde a B+-Tree brilha: O(logt n + k) versus O(k · logt n) da B-Tree. Para k = 100.000 resultados, a diferença é absurda.

2.8 Estrutura e funções em C (TAD simples)

A diferença para a B-Tree aparece em dois pontos da struct: as chaves de um nó interno são só separadores (não guardam dado associado), e toda folha tem um ponteiro prox para a folha seguinte, formando a lista encadeada que liga todas as folhas da esquerda para a direita.

#define T 2   /* grau mínimo da árvore */

typedef struct NodoBMais {
    int  n;                          /* número de chaves em uso */
    int  chaves[2*T - 1];             /* separadores (interno) ou chaves reais (folha) */
    struct NodoBMais *filhos[2*T];     /* filhos (não usados se for folha) */
    int  folha;                       /* 1 se for folha, 0 caso contrário */
    struct NodoBMais *prox;           /* próxima folha, da esquerda p/ direita (só em folhas) */
} NodoBMais;

NodoBMais *criarNodoBMais(int folha);            /* cria um nó vazio (folha ou interno) */
NodoBMais *buscarBMais(NodoBMais *raiz, int chave); /* devolve a folha onde a chave está, ou NULL */
NodoBMais *inserirBMais(NodoBMais *raiz, int chave);/* insere a chave; devolve a (nova) raiz */
NodoBMais *removerBMais(NodoBMais *raiz, int chave);/* remove a chave; devolve a (nova) raiz */
void       destruirBMais(NodoBMais *raiz);               /* libera todos os nós */

main de demonstração:

int main(void) {
    NodoBMais *raiz = criarNodoBMais(1);         /* raiz começa como folha vazia */

    int chaves[] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
    for (int i = 0; i < 9; i++)
        raiz = inserirBMais(raiz, chaves[i]);

    printf("buscarBMais(70) = %s\n", buscarBMais(raiz, 70) ? "achou" : "não achou");  /* achou */

    /* busca por intervalo: desce até a 1ª folha e segue os ponteiros prox */
    NodoBMais *f = buscarBMais(raiz, 30);
    while (f != NULL) {
        /* imprimiria aqui as chaves de f, em ordem, até o fim do intervalo */
        f = f->prox;
    }

    raiz = removerBMais(raiz, 60);
    destruirBMais(raiz);
    return 0;
}

3. Aplicações em Sistemas Reais

3.1 Sistemas de arquivos

  • ext4 (Linux): usa B-Trees para indexar blocos de dados de arquivos grandes e para o diretório HTree (uma variante de B-Tree hash).
  • NTFS (Windows): o MFT (Master File Table) usa B+-Trees para indexar arquivos por nome.
  • HFS+ / APFS (macOS): catálogo de arquivos implementado como B-Tree.
  • Btrfs: um sistema de arquivos inteiramente construído sobre B-Trees copy-on-write.

3.2 Bancos de dados

  • MySQL InnoDB: todos os índices (primário e secundários) são B+-Trees. A tabela em si é organizada como uma B+-Tree com os dados nas folhas (chamada de clustered index).
  • PostgreSQL: o tipo de índice padrão é B+-Tree. É o que se cria com CREATE INDEX sem especificar tipo.
  • SQLite: a estrutura de dados central é uma B-Tree (com variante B+ para índices).
  • Oracle, SQL Server: também usam B+-Trees para índices.

3.3 Por que não AVL em banco de dados?

A AVL garante O(log n) operações, mas isso é enganoso no contexto de disco.

Para n = 10⁹ registros:
• AVL: altura ≈ 30 → 30 leituras de disco por busca. Cada leitura traz 4KB mas usa apenas ≈ 20 bytes (nó AVL). Aproveitamento: 0,5%.
• B+-Tree com t=200: altura ≈ 4 → 4 leituras de disco por busca. Cada leitura traz um nó com ~400 chaves. Aproveitamento: 100%.

O O(log n) da AVL assume que cada "passo" tem custo O(1). No disco, cada passo custa O(1 leitura de bloco) — mas a AVL usa <1% do bloco lido. A B-Tree usa 100%. Quando o custo dominante é o I/O de disco, o fator de ramificação t é crucial.

3.4 Comparação B-Tree vs B+-Tree

Característica B-Tree B+-Tree
Localização dos dados Nós internos E folhas Apenas nas folhas
Chaves em nós internos Chaves reais com dados Cópias para navegação
Densidade de nós internos Menor (dados ocupam espaço) Maior (só chaves de guia)
Busca por intervalo O(k · logt n) O(logt n + k)
Varredura sequencial Percurso em-ordem da árvore Scan da lista de folhas
Busca exata pode terminar cedo? Sim (dado pode estar em nó interno) Não (sempre chega à folha)
Aplicação típica Sistemas de arquivos, índices simples Bancos de dados, índices relacionais

Resumo

  • B-Trees e B+-Trees foram projetadas para o modelo de acesso a disco: cada nó = um bloco = uma leitura.
  • O grau mínimo t define o fator de ramificação: nós têm entre t−1 e 2t−1 chaves. Com t grande, a altura é minúscula.
  • A altura garantida O(logt n) implica O(logt n) leituras de disco por operação — tipicamente 3 a 5 para bilhões de registros.
  • A B-Tree mantém todas as folhas na mesma profundidade via divisões (inserção) e empréstimos e fusões (remoção) — balanceamento perfeito.
  • Cada operação tem poucos casos: inserção (folha com espaço, split, split em cascata), remoção (folha com folga, empréstimo, fusão, fusão em cascata) e busca (achar, não achar).
  • A B+-Tree separa navegação (nós internos) de dados (folhas) e encadeia as folhas, tornando buscas por intervalo dramaticamente mais eficientes.
  • MySQL InnoDB, PostgreSQL, ext4, NTFS e a maioria dos sistemas de armazenamento modernos usam B+-Trees como estrutura de índice central.

Exercícios

  1. Uma B-Tree tem grau mínimo t=3. Qual o número mínimo e máximo de chaves por nó não-raiz? E de filhos?

  2. Por que uma B-Tree garante que todas as folhas estão na mesma profundidade? Qual invariante assegura isso?

  3. Insira as chaves 5, 15, 25, 35, 45 em ordem numa B-Tree vazia de grau t=2. Mostre a árvore após cada inserção, incluindo splits.

  4. Qual é a altura máxima de uma B-Tree de grau t com n chaves? Expresse como fórmula e calcule para t=100 e n=10⁹.

  5. Qual é a principal diferença entre B-Tree e B+-Tree em termos de onde os dados ficam armazenados? Qual suporta melhor buscas por intervalo, e por quê?

  6. Na B+-Tree, por que as chaves aparecem tanto nos nós internos quanto nas folhas? Isso representa um problema de redundância?

  7. Um banco de dados tem 50 milhões de registros. Um índice B+-Tree com t=200 é usado. Quantas leituras de disco uma busca exata demora aproximadamente? Compare com uma AVL balanceada.

  8. Por que sistemas de arquivos e bancos de dados usam B-Trees em vez de AVL, mesmo ela garantindo O(log n)?

  9. Na B-Tree de 5 nós desta aula (t=2, raiz [20, 40, 60], folhas [10], [30], [50] e [70, 80, 90]), remova, nesta ordem, as chaves 80, 60 e 10. Mostre a árvore após cada remoção e diga qual caso da seção 1.5 foi aplicado.

  10. Na B+-Tree de 5 nós desta aula (raiz [30, 50, 70], folhas [10, 20], [30, 40], [50, 60] e [70, 80, 90]), insira a chave 85 e, em seguida, remova a chave 10. Mostre a árvore após cada operação, indicando os casos aplicados (seções 2.3 e 2.5).


Gabaritos

Sugestão de resposta — Exercício 1

Com t=3:
• Chaves mínimas: t−1 = 2.
• Chaves máximas: 2t−1 = 5.
• Filhos mínimos (nó interno): t = 3.
• Filhos máximos (nó interno): 2t = 6.

Obs.: a raiz pode ter apenas 1 chave (e 2 filhos), sem restrição de mínimo.


Sugestão de resposta — Exercício 2

A altura só cresce quando a raiz é dividida: ao inserir numa raiz cheia, ela é dividida e uma nova raiz é criada com a mediana. Ambos os filhos da nova raiz ficam na mesma profundidade. Da mesma forma, splits propagam para cima mas nunca adicionam folhas em profundidades diferentes — a divisão sempre cria dois nós no mesmo nível.

O invariante é: toda inserção e remoção percorre a árvore de cima para baixo, e qualquer reorganização (split ou merge) ocorre localmente em nós do mesmo nível. Assim, a propriedade "todas as folhas na mesma profundidade" é mantida após cada operação.


Sugestão de resposta — Exercício 3

t=2: nós têm entre 1 e 3 chaves; raiz pode ter 1 a 3.

Após inserir 5:       [5]

Após inserir 15:      [5, 15]

Após inserir 25:      [5, 15, 25]       (raiz com 3 chaves = 2t-1, ok mas cheia)

Após inserir 35:
  35 entra em [5, 15, 25] e a raiz estoura: [5, 15, 25, 35] (4 chaves). Sobe a 2ª chave (15), que forma a nova raiz:
      [15]
      /  \
    [5]  [25]
  O 35 fica na folha da direita:
      [15]
      /  \
    [5]  [25, 35]

Após inserir 45:
  Raiz [15], 45 > 15 → filho [25, 35], 45 > 35 → inserir em [25,35] → [25,35,45] (cheio, mas é folha e cabe 3 chaves).
      [15]
      /  \
    [5]  [25, 35, 45]

Nota: [25,35,45] tem 3 = 2t−1 chaves — é o máximo permitido. Se inserirmos mais uma chave nesse filho, ele estoura e precisa ser dividido.


Sugestão de resposta — Exercício 4

A altura máxima ocorre quando todos os nós têm o número mínimo de chaves (t−1 por nó não-raiz, 1 na raiz). Nesse pior caso, contando a altura h da raiz (profundidade 0) até as folhas (profundidade h):

h ≤ logt((n+1)/2)

Isso vem do fato de que uma B-Tree de altura h tem no mínimo 1 + 2·(t−1)·(1 + t + t² + ... + t^(h−1)) = 1 + 2·(t^h − 1) = 2t^h − 1 chaves (a raiz tem 1; o nível 1 tem 2 nós; o nível 2 tem 2t; e assim por diante). Logo n ≥ 2t^h − 1, e resolvendo: h ≤ logt((n+1)/2).

Para t=100 e n=10⁹:

h ≤ log100((10⁹+1)/2) ≈ log100(5 · 10⁸) = (8 + log 5) / 2 ≈ 8,7 / 2 ≈ 4,35

Como h é inteiro, h ≤ 4: a árvore tem no máximo 5 níveis (raiz + 4) — no máximo 5 leituras de disco para qualquer busca num banco de dados de 1 bilhão de registros.


Sugestão de resposta — Exercício 5

B-Tree: dados podem ser armazenados em qualquer nó — interno ou folha. Uma busca pode terminar antes de chegar às folhas se a chave for encontrada num nó interno.

B+-Tree: dados estão exclusivamente nas folhas. Os nós internos contêm apenas cópias de chaves para guiar a navegação. As folhas são encadeadas em ordem.

A B+-Tree suporta melhor buscas por intervalo porque: após encontrar a primeira chave do intervalo numa folha (O(logt n)), basta percorrer os ponteiros da lista encadeada de folhas para coletar os k resultados (O(k)). Total: O(logt n + k). Na B-Tree, os k resultados podem estar em nós internos espalhados pela árvore, e cada um exige uma travessia separada — O(k · logt n).


Sugestão de resposta — Exercício 6

As chaves nos nós internos são cópias — separadores que guiam a navegação. Como os dados reais só existem nas folhas, a chave precisa aparecer na folha para ser acessível ao usuário. Mas para saber em qual filho procurar, os nós internos precisam de referências a essas mesmas chaves.

Isso não é um problema na prática, pelas seguintes razões:

  • Os nós internos armazenam apenas a chave (por exemplo, um inteiro de 4 bytes), não o dado completo (que pode ser um registro de kilobytes). A redundância é mínima em tamanho.
  • A separação navegação/dados é justamente o que torna a B+-Tree eficiente: nós internos mais compactos permitem maior fator de ramificação (mais chaves por bloco), reduzindo a altura da árvore.
  • Conceitualmente, as chaves nos nós internos são apenas guias, não duplicatas dos registros.

Sugestão de resposta — Exercício 7

B+-Tree com t=200, n=50.000.000:

h = ⌈log200(50.000.000)⌉ = ⌈log(5×10⁷) / log(200)⌉ = ⌈7,699 / 2,301⌉ = ⌈3,35⌉ = 4 leituras de disco

AVL, n=50.000.000:

h ≈ 1,44 · log₂(50.000.000) ≈ 1,44 · 25,6 ≈ 37 leituras de disco

A B+-Tree é ~9× mais rápida em número de acessos ao disco. E cada acesso AVL desperdiça 99%+ do bloco lido, enquanto cada acesso B+ usa o bloco inteiro. Na prática, a diferença real de tempo pode ser 50× a 100× em favor da B+-Tree.


Sugestão de resposta — Exercício 8

O O(log n) da AVL esconde uma premissa implícita: cada "passo" (acesso a um nó) custa O(1). Isso é verdade na RAM, mas falso no disco.

No disco, o custo de um acesso é o mesmo independente de quantos bytes são lidos — um bloco inteiro (4KB a 64KB) é carregado. Uma AVL usa ~20 bytes de um bloco de 4KB: aproveitamento de 0,5%. Uma B+-Tree preenche o bloco com 400+ chaves: aproveitamento de ~100%.

Portanto, o modelo de custo correto para disco é: número de blocos lidos, não número de operações. Neste modelo:

  • AVL: O(log₂ n) = O(log n) blocos lidos. Para n=10⁸: ≈ 27 blocos.
  • B+-Tree (t=200): O(log₂₀₀ n) = O(log n / log 200) blocos lidos. Para n=10⁸: ≈ 3 blocos.

A B+-Tree vence em disco justamente porque foi projetada para o modelo de acesso a disco: 1 nó = 1 bloco = 1 acesso. A AVL foi projetada para RAM, onde a premissa de O(1) por acesso é válida.


Sugestão de resposta — Exercício 9
Árvore inicial:  [20, 40, 60]  com folhas  [10] [30] [50] [70, 80, 90]

Remover 80  (caso 2: a chave está numa folha com folga)
  [70, 80, 90] vira [70, 90] (2 chaves, mais que o mínimo).
  [20, 40, 60]  com folhas  [10] [30] [50] [70, 90]

Remover 60  (caso 3: a chave está num nó interno)
  Predecessor: 50, numa folha no mínimo. Sucessor: 70, numa folha com folga.
  Troca 60 por 70 e remove 70 da folha, que vira [90].
  [20, 40, 70]  com folhas  [10] [30] [50] [90]

Remover 10  (caso 5: fusão, com o pai folgado)
  A folha [10] fica vazia; seu único irmão, [30], está no mínimo.
  Funde [ ] + 20 (desce da raiz) + [30] = [20, 30]. A raiz perde uma chave.
  [40, 70]  com folhas  [20, 30] [50] [90]

Sugestão de resposta — Exercício 10
Árvore inicial:  raiz [30, 50, 70]  com folhas  [10, 20] [30, 40] [50, 60] [70, 80, 90]

Inserir 85  (inserção, caso 3: a divisão se propaga)
  85 vai para [70, 80, 90], que estoura: [70, 80] e [85, 90]; 85 é copiado para a raiz.
  A raiz [30, 50, 70, 85] estoura: 50 sobe e forma a nova raiz.
                        [50]
                 /               \
             [30]              [70, 85]
            /    \            /    |    \
      [10,20]  [30,40]  [50,60]  [70,80]  [85,90]

Remover 10  (remoção, caso 5a: a fusão se propaga e o pai empresta)
  [10, 20] vira [20], abaixo do mínimo (2); o irmão [30, 40] está no mínimo.
  Funde as folhas em [20, 30, 40] e descarta o separador 30; o nó [30] fica vazio.
  O irmão [70, 85] tem folga: por rotação, 50 desce da raiz e 70 sobe para a raiz.
                        [70]
                 /               \
             [50]                [85]
            /    \              /    \
  [20,30,40]  [50,60]     [70,80]  [85,90]