Você está na página 1de 17

Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

Árvores n-árias

Site: AVA - IFMG Campus Bambuí Impresso por: Letícia Moreira Leonel
Curso: Técnicas de Programação - BIBENGC.2021.1-A Data: terça, 8 nov 2022, 09:56
Livro: Árvores n-árias

1 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

Índice

1. Introdução

2. Árvore B

3. Operações Básicas em uma Árvore B


3.1. Criação da árvore
3.2. Busca por uma chave
3.3. Inserção
3.4. Remoção

4. Árvores B* e B+

5. Referências

2 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

1. Introdução

No tópico anterior, trabalhamos com o armazenamento em memória secundária utilizando arquivos de registros. Como vimos, podemos
enxergá-los como listas de dados, que são armazenados na memória secundária até que seja necessário trazê-los à memória principal.

Entretanto, a organização em listas é bastante ineficiente. São estruturas lineares, que demandam um grande número de acessos a disco
quando desejamos localizar uma informação. Cada acesso a disco tem um alto custo computacional, devido ao tempo de busca, à latência
rotacional e ao tempo de transferência dos dados.

Já vimos outras estruturas de dados que melhoram o tempo de busca de uma informação em uma coleção, como as árvores, especialmente as
árvores binárias de busca. Estas possuem como propriedade o fato dos dados serem organizados de tal forma que os descendentes de um nó à
esquerda sempre serão menores que o valor armazenado no nó, assim como todos os descendentes à direita são maiores que ele. Entretanto,
uma árvore binária de busca pode se degenerar, tornando-se desbalanceada, fazendo com que se comporte praticamente como uma lista. Para
resolver esse problema, várias árvores balanceadas foram propostas na literatura, como as árvores AVL e rubro-negras, que buscam garantir
que o tempo de busca de uma informação seja sempre O (log n).

Entretanto, árvores binárias não são adequadas para armazenamento em memória secundária. Embora os dados sejam armazenados de
forma que facilita a busca da informação, a árvore binária tende a ter uma altura (distância da raiz às folhas) considerável, quando possui muitos
dados inseridos, aumentando o número de acessos.

A Figura 1 ilustra uma árvore binária de busca, perfeitamente balanceada, com altura h=4. Para encontrar qualquer dado na árvore, serão
necessários, no máximo, 4 acessos a disco. Por exemplo, para localizar a chave "E", recuperamos a raiz ("H"), como a chave desejada é menor
que a armazenada na raiz, buscamos o filho da esquerda ("D"), que ainda não é a chave desejada. Como "E" é maior que "D", buscamos o filho
da direita, "F", que ainda não é o que desejamos, sendo necessário mais um acesso, ao filho da esquerda, que finalmente é a chave procurada.
Foram necessários, portanto, 4 acessos a disco para encontrar esse valor na árvore.

Figura 1 - Árvore binária de busca com altura h=4

Fonte: Autor, 2021.

Uma forma de reduzir o número de acessos é, em vez de armazenar cada nó da árvore de forma individual no arquivo, agruparmos em
"páginas" com um número preestabelecido de nós. Podemos observar essa organização na Figura 2, em que cada retângulo representa uma
página com três nós armazenados internamente. As operações em disco, agora, leem e escrevem páginas inteiras, de tal forma que nosso
processo de busca da mesma chave ("E") agora se reduz a apenas 2 acessos a disco, um para cada página. Note que mantemos todas as
propriedades da árvore binária.

Figura 2 - Árvore Binária organizada em páginas com 3 nós

3 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

Fonte: Autor, 2021.

Podemos perceber que, ao agrupar os nós em páginas, temos uma nova estrutura de dados, em que cada página pode ter múltiplos
descendentes. A primeira página da Figura 2 tem 3 chaves e 4 descendentes, e cada página subsequente poderá ter, portanto, até 4
descendentes, formando uma árvore 4-nária. Alterando o número de chaves que cada página pode conter, teremos árvores capazes de
armazenar qualquer quantidade de chaves e de descendentes, portanto, formando uma estrutura n-ária em que o número de descendentes será
sempre o número máximo de chaves mais um. Para simplificar a representação, podemos mostrar as árvores n-árias como na Figura 3, em que
agrupamos as chaves nas páginas de forma mais compacta e, entre as chaves, mostramos os apontadores para cada descendente. Observe
que mantemos a propriedade básica de uma árvore de busca: o filho da esquerda contém apenas valores menores que a chave, enquanto os da
direita, armazenam valores maiores.

Figura 3 - Árvore n-ária correspondente

Fonte: Autor, 2021.

Na próxima seção, vamos expandir esse conceito para as árvores denominadas Árvores B (B-Trees) e suas propriedades.

4 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

2. Árvore B

Como vimos na seção anterior, uma árvore n-ária pode reduzir, consideravelmente, a altura em relação a uma árvore binária de busca
correspondente. Isso implica em menor número de acessos a disco e, portanto, maior velocidade para acessar a informação.

Uma implementação bastante comum de árvore n-ária é a denominada Árvore B. Não se sabe se o nome se refere ao sobrenome de um de
seus criadores (Rudolf Bayer e Eward M. McCreight) ou ao local de trabalho em que a criaram (Boeing Research Labs) ou simplesmente um
nome aleatório. De qualquer forma, a origem do nome é irrelevante, diante de toda a contribuição à Ciência da Computação, desde sua criação,
em 1971. Árvores B são encontradas na implementação de sistemas gerenciadores de bancos de dados, para armazenamento e recuperação
mais eficiente de dados, e até mesmo em sistemas de arquivos de sistemas operacionais, como o NTFS no Windows e btrfs e ext4, no Linux.

Uma árvore B é uma árvore com uma raiz, que possui as seguintes propriedades (CORMEN et al., 2009):

1. Toda página x tem as seguintes propriedades:


1. x.n, que é o número de chaves atualmente armazenadas na página x;
2. as próprias x.n chaves, x.key1, x.key2,..., x.keyx.n, armazenadas em ordem crescente, de tal forma que x.key 1 ≤ x.key 2 ≤ ... ≤ x.key x.n;
3. x.leaf, um valor lógico que é verdadeiro se x é uma folha e falso se é uma página interna.
2. Cada página interna x também contém x.n + 1 ponteiros x.c1, x.c2,..., x.cx.n+1 para seus filhos. Páginas folha não possuem filhos, portanto,
seus atributos x.ci são indefinidos.
3. As chaves x.keyi separam as faixas de chaves armazenadas em cada sub-árvore: se ki é qualquer chave armazenada na sub-árvore com
raiz em x.ci, então
k1 ≤ x.key1 ≤ k2 ≤ x.key2 ≤ ... ≤ x.keyx.n ≤ kx.n+1.
4. Todas as folhas possuem a mesma profundidade, que é a altura h da árvore.
5. Páginas possuem limites inferior e superior para o número de chaves que podem conter. Expressamos esses limites em termos de um
inteiro fixo t ≥ 2, chamado grau mínimo da árvore B:
1. Cada nó, exceto a raiz, deve possuir no mínimo t - 1 chaves. Toda página interna diferente da raiz tem, portanto, no mínimo t filhos.
Se a árvore não está vazia, a raiz deve possuir, no mínimo, uma chave.
2. Toda página pode conter no máximo 2t - 1 chaves. Portanto, uma página interna pode ter, no máximo, 2t filhos. Dizemos que uma
página está cheia se ela contém exatamente 2t - 1 chaves.

A árvore B mais simples é aquela com t = 2. Todo nó interno tem 2, 3 ou 4 filhos, por isso chamamos árvore 2-3-4. Na prática, costumamos usar
valores maiores de t em aplicações, como t = 100, por exemplo. Quanto maior o valor de t, menor é a altura da árvore B.

Devemos notar que aqui estamos usando a definição de árvore B de Cormen et al. (2009), que utiliza o grau mínimo da árvore (t).
Outros autores, representam o grau da árvore como m, com definição similar (m = t - 1), e o máximo de descendentes igual a 2m, como
Ziviane (1999). Por outro lado, foram encontrados materiais em que m representa o máximo de filhos de uma página (equiparando m =
2t).  Cuidado com simulações e vídeos que utilizam o grau m, pode confundir os resultados. Utilizaremos a definição de Cormen et al.
(2009) por ser uma implementação robusta e que otimiza os acessos a disco na inserção e remoção de chaves e a maioria dos
exemplos na internet segue outras implementações!

n+1
A altura da árvore determina a quantidade de operações de acesso a disco que serão realizadas. A altura pode ser estimada por h ≤ logt 2
.

5 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

3. Operações Básicas em uma Árvore B

Como dito previamente, para todos os conceitos, utilizaremos os conceitos de Cormen et al. (2009) para definições e implementações das
operações em uma árvore B. Essa diferenciação é importante, porque Cormen et al. (2009) traz algumas alterações em relação a outros
autores.

Uma árvore B original (e de outros autores) somente realiza as operações de fusão e de divisão de páginas se, ao chegar na página onde a
inserção/remoção for realizada, as condições estruturais da árvore forem violadas (por exemplo, a página ficar com menos que 2t - 1 chaves ou
já estiver cheia), voltando à página mãe recursivamente, garantindo a integridade estrutural até a raiz.

Na implementação proposta por Cormen et al. (2009), utilizamos algoritmos de uma passagem pela árvore, da raiz até a folha, sem retornar à
página mãe. Por isso, quando encontramos uma página que pode, potencialmente, alterar a integridade estrutural da árvore, faremos a
fusão/divisão daquele nó, mesmo que ele não seja o alvo da operação. Só desceremos para a próxima página ao garantirmos que todas as
alterações foram feitas e a árvore permanecerá balanceada e dentro do intervalo de [t - 1, 2t - 1] chaves. A única página que será sempre
mantida em memória é a raiz, a partir de onde iniciaremos todos os algoritmos.

Essa premissa de uma única passagem pode parecer, em um primeiro momento, que estaremos fazendo mudanças desnecessárias na árvore
(e em uma boa parcela das operações, elas não serão realmente necessárias). Mas se considerarmos que subir e descer pelas páginas da
árvore podem significar mais acessos a disco, e/ou a necessidade de manter mais páginas carregadas na memória, temos uma economia de
recursos (tempo e memória), tornando a implementação mais eficiente.

Temos que considerar também que estamos trabalhando com conceitos de chaves, abstraindo o restante da complexidade do que é
armazenado em uma árvore B. A chave pode conter dados "satélites", relacionados àquela chave, na forma de um objeto mais complexo. Por
exemplo, uma árvore B para armazenar objetos da classe student pode utilizar o número de identificação do aluno como chave, por ser um
dado numérico e de fácil ordenação, mas junto com essa chave, outros atributos do aluno, como nome, curso, etc., podem estar associados e
armazenados em conjunto. Por isso, manter o menor número de acessos ao disco e de páginas na memória.

As operações básicas em uma árvore B são:

• Criação da árvore;
• Busca por uma chave;
• Inserção;
• Remoção.

Enquanto os dois primeiros são procedimentos muito simples, veremos que a inserção e remoção de chaves em uma árvore B requer um
número considerável de métodos auxiliares.

6 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

3.1. Criação da árvore

A criação da árvore B é o processo de alocarmos uma raiz vazia e fazermos seu armazenamento em disco. A partir da criação, podemos iniciar
o processo de inserção de novas chaves na árvore.

Seguiremos a notação adotada por Cormen et al. (2009), por ser bastante simples. Logicamente, ao fazermos uma implementação orientada a
objetos, devemos nos ater aos ajustes necessários ao acesso aos atributos da página, assim como o construtor do objeto, que está
representado pelo procedimento ALLOCATE-NODE.

B-TREE-CREATE(T)
1 x = ALLOCATE-NODE()
2 x.leaf = TRUE
3 x.n = 0
4 DISK-WRITE(x)
5 T.root = x

No método B-TREE-CREATE temos o processo de inicialização da árvore. Inicialmente, alocamos um novo nó (página) vazio, denominado x.
Como a árvore está vazia, dizemos que aquela página é folha e o número de chaves é igual a zero, e fazemos a escrita em disco dessa página.
Por fim, fazemos a raiz apontar para essa página recém criada. Veremos, no procedimento de inserção, que uma árvore B tem como
propriedade ter o aumento de sua altura sempre pela divisão da raiz, por isso é muito importante ter sempre a raiz em memória e de fácil
acesso.

Na criação da árvore, definimos o grau mínimo (t) e, a partir desse ponto, ele não é mais alterado para toda a árvore, visando garantir sua
integridade estrutural.

O procedimento de criação da árvore vazia tem complexidade O(1) para operações em disco e o tempo de CPU também é O(1).

7 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

3.2. Busca por uma chave

A busca em uma árvore B não é muito diferente da busca em uma árvore binária. A principal diferença é que enquanto uma árvore binária tem
uma única chave por nó, resultando em apenas duas possibilidades de busca (descendente à esquerda ou à direita), temos no mínimo t - 1
chaves para buscar na página e 2t descendentes (dos quais utilizaremos apenas um para seguir a busca, se a chave não estiver na página
atual). Como apresentado nas definições, cada chave, se considerada individualmente, tem as mesmas propriedades da árvore de busca: o filho
da esquerda contém chaves com valor menor que a chave atual, enquanto o descendente da direita contém chaves maiores.

Primeiro, verificamos se a chave desejada (k) encontra-se na página atual. Se não estiver, verificamos em qual sub-árvore estaria e começamos
a descida para os descendentes, até chegar a um nó folha. Se o valor for encontrado nesse caminho, retornamos a página em que ela está e o
índice da chave. Como estamos tratando de linguagem que não permite fazer retorno de dois valores diferentes, uma possível adaptação é
passar um ponteiro para a página e retornar o valor do índice. Se o valor não estiver na folha, retornamos NIL (nulo, ou NULL).

Outro detalhe importante é que não faremos modificações nos algoritmos que foram escritos por Cormen et al. (2009), sendo necessário fazer
os ajustes, inclusive, nos índices: Cormen considera que o primeiro índice é 1 e não 0, como nas linguagens C/C++.

B-TREE-SEARCH(x, k)
1 i = 1
2 while i ≤ x.n and k > x.keyi
3 i = i + 1
4 if i ≤ x.n and k == x.keyi
5 return (x, i)
6 elseif x.leaf
7 return NIL
8 else DISK-READ(x.ci)
9 return BTREE-SEARCH(x.ci, k)

As linhas 1-3 fazem uma busca linear na página atual (começando pela raiz, obviamente), para verificar se a chave k está presente. Se
encontrarmos uma chave que seja maior à que buscamos, esse laço para, nos permitindo analisar se é a chave desejada ou se precisaremos
continuar em um dos descendentes. As linhas 4-5 tratam do caso da chave estar presente na página, em que retornamos a página e o índice
onde k foi encontrada.

As linhas 6-9 se encarregam da situação em que a chave k não está na página atual. Se a página for folha, vamos retornar NIL para indicar que
a chave não existe na árvore. Se não for folha, vamos selecionar o i-ésimo filho de x (x.ci), buscá-lo no disco e recomeçar o procedimento a
partir desse filho, de forma recursiva.

A Figura 4 ilustra uma busca pela chave k = "J" na árvore. Começando pela raiz, comparamos as chaves, observando que as chaves "D" e "H"
são menores que a desejada. Quando analisamos a chave "L" (i = 3), saímos do laço e sabemos que não adianta mais procurar dali em diante
na página. Como não é uma folha, buscamos o filho x.c3 da página no disco, que é o filho da direita de "H", e repetimos o processo
recursivamente, de tal forma que x.c3 é nossa nova página de busca (x), até descobrir que a chave k = "J" ocupa o índice 2 das chaves (x.key 2),
então retornamos a página x e o índice i = 2 para indicar que ela foi encontrada.

Figura 4 - Busca em árvore B

Fonte: Autor, 2021.

Repita o processo de busca na árvore da Figura 4, buscando os valores "D", "F" e "P".

Quanto à complexidade, dependerá da altura da árvore. O número de acessos a disco depende da altura, sendo, portanto, O(h) = O (log t n),
sendo n o número de chaves armazenadas, ou seja, no pior cenário, faremos h acessos a disco para buscar a chave, caso ela esteja em uma
folha ou não esteja presente na árvore. Quanto ao tempo de CPU, considerando que cada página tem, no máximo, 2t - 1 chaves, o laço das
linhas 2-3 tem um tempo O(t) dentro de cada página, sendo que para a altura completa da árvore teremos O(th) = O(t log t n) no total.

8 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

3.3. Inserção

A inserção em uma árvore B requer um pouco mais de cuidado que nas árvores binárias de busca. Embora em ambas as inserções sejam feitas
nas folhas, a árvore B tem restrições mais rígidas que as árvores binárias. Numa árvore AVL, fazemos a inserção e, caso haja um
desbalanceamento maior que 1 nível, somos obrigados a fazer rotações para que a diferença da altura das duas sub-árvores seja igual a zero
ou um. Uma árvore B exige que todas as folhas tenham a mesma altura, então o processo de "rotação" implica em dividir uma página cheia em
duas e ceder um elemento para a página mãe (a chave do meio).

A implementação de Cormen et al. (2009) é eficiente neste aspecto. Com o intuito de fazer uma única passada pela árvore, sem retornar para o
nível anterior, minimizamos o número de acessos a disco e tomamos decisões de divisão antes que sejam necessárias. Começamos o processo
pela raiz da árvore, se ela estiver cheia (x.n == 2t - 1), procedemos com a divisão da raiz: criamos um novo nó raiz, que receberá a chave do
centro da página a ser dividida, a antiga raiz se torna o filho da esquerda dessa chave e, em seguida, criamos uma nova página, para quem
copiamos as chaves posteriores ao elemento do meio e seus descendentes, se houver. Dessa forma, cada página filha fica com t - 1 chaves (e t
descendentes, se houver) e a raiz com apenas uma chave e dois descendentes. Como apresentado na conceituação da árvore B, a raiz é a
única página que pode ter menos de t - 1 chaves. Podemos também observar que a altura da árvore B somente aumenta pela divisão da
raiz, que acaba sendo um processo lento, pois temos que fazer subir chaves a partir da inserção nas folhas, dividindo as páginas, até que a raiz
definitivamente fique cheia.

Depois de tratarmos a raiz (ou caso ela não esteja cheia), buscamos o filho x.ci (sendo x a raiz) em que a chave deverá ficar. Trazemos esse
filho do disco para a memória, antes de iniciar a recursão, para verificar se ele está cheio. Se estiver cheio, o que pode acontecer se a raiz não
foi recentemente dividida, fazemos a divisão desse filho, subindo uma chave e criando dois descendentes com t - 1 chaves. Como a página mãe
ainda está na memória, é fácil subir uma chave sem ter que fazer novas operações em disco. Seguimos esse procedimento, descendo até
chegarmos na folha que irá comportar a nova chave, onde faremos a inserção, mantendo a ordem crescente dentro da página.

O processo de divisão das páginas visa manter espaço livre, em cada uma que esteja no caminho, para o caso de termos que dividir alguma
página para fazer a inserção. Na maioria das vezes, esse processo será feito e nenhuma chave subirá, mas garantimos que sempre teremos
espaço livre na árvore para novas divisões. Se não fizermos essa divisão preventiva, eventualmente teremos que dividir uma página filha,
estando a página mãe cheia, fazendo com que tenhamos que voltar, recursivamente, vários níveis da árvore para fazer os ajustes necessários,
aumentando o número de acesso a disco desnecessariamente, além de aumentar o consumo de memória para manter páginas carregadas para
fazermos as alterações.

Antes de analisarmos o pseudocódigo dos métodos de inserção (por conveniência, vamos dividir a inserção em métodos auxiliares), vamos
analisar um processo de inserção em uma árvore, começando por uma raiz vazia.

A Figura 5 ilustra o processo de inserção das chaves "E", "C", "H", "F", "G" e "D", nesta ordem, partindo de uma árvore recém-criada (vazia). O
grau mínimo t = 3, de tal forma que as páginas que não sejam a raiz podem ter no mínimo t - 1 = 2 chaves e t = 3 descendentes e, no máximo, 2t
- 1 = 5 chaves e 2t = 6 descendentes.  As setas pontilhadas em vermelho mostram o fluxo dos procedimentos, as setas contínuas em preto
mostram quem são as páginas filhas.

Até inserirmos a chave "G", temos uma situação bastante simples: a página contém espaço disponível e fazemos a inserção movendo as
chaves uma posição para a direita, até liberar o espaço onde devem ocupar (indicado pela chave em vermelho). Quando inserimos a chave "G",
nossa raiz da árvore ficha cheia, mas ainda não é necessário fazer nenhuma alteração. Quando vamos inserir a chave "D", começamos pela raiz
e já nos deparamos com ela cheia, então temos que criar uma nova raiz, vazia, para onde movemos a chave que está no meio da página sendo
dividida ("F"). Criamos uma nova página, no mesmo nível que a antiga raiz, para onde copiamos todas as chaves que estavam após o meio.
Fazemos a antiga raiz ser a filha da esquerda da chave "F" e a nova página se torna a filha da direita (possui chaves maiores. Agora que a raiz
não está mais cheia, reiniciamos o procedimento, localizando qual página filha deve conter a nova chave, que determinamos que é a da
esquerda, onde finalmente fazemos a inserção da chave "D".

Figura 5 - Inserção de chaves em uma árvore B

9 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

Fonte: Autor, 2021.

Como pudemos perceber, a árvore aumentou um nível quando a raiz estava cheia, sendo criada uma nova raiz, com espaço para inserção de
novas chaves. Todas as inserções em árvore B ocorrem nas folhas, portanto, a raiz só recebe, quando já tiver descendentes, chaves que
subam de futuras divisões.

Vamos analisar o pseudocódigo, proposto por Cormen et al. (2009), para inserção em uma árvore B.

B-TREE-INSERT(T, k)
1 r = T.root
2 if r.n == 2t - 1
3 s = ALLOCATE-NODE()
4 T.root = s
5 s.leaf = FALSE
6 s.n = 0
7 s.c1 = r
8 B-TREE-SPLIT-CHILD(s, 1)
9 B-TREE-INSERT-NONFULL(s, k)
10 else B-TREE-INSERT-NONFULL(r, k)

O método B-TREE-INSERT recebe a representação da árvore T (lembre-se que esse pseudocódigo não utiliza totalmente os conceitos de
orientação a objetos) e a chave a ser inserida (k). Primeiro, identificamos a raiz e verificamos se não está cheia (linhas 1-2). As linhas 3-7 se
encarregam, no caso da raiz estar cheia, de criar uma nova raiz (s), dizer que ela não é folha e tem nenhuma chave e tornar a antiga raiz (r) sua
filha da esquerda. A linha 8, após essa reorganização, se encarrega de pedir a divisão (B-TREE-SPLIT-CHILD) do filho 1 (o mais à esquerda) de
s, para que seja feita a divisão corretamente e a chave da posição mediana seja inserida em s. Depois da divisão, a linha 9 se encarrega de
chamar o procedimento auxiliar B-TREE-INSERT-NONFULL, a partir da nova raiz, para que a inserção seja efetivada. Se a raiz não estiver
cheia, na linha 2, pulamos diretamente para a linha 10, na qual faremos a inserção sem necessidade de divisão da raiz.

Vamos analisar cada um dos métodos auxiliares da inserção, começando pelo B-TREE-SPLIT-CHILD.

B-TREE-SPLIT-CHILD(x, i)
1 z = ALLOCATE-NODE()
2 y = x.ci
3 z.leaf = y.leaf
4 z.n = t - 1
5 for j = 1 to t - 1
6 z.keyi = y.keyj+t
7 if not y.leaf
8 for j = 1 to t
9 z.cj = y.cj+t
10 y.n = t - 1
11 for j = x.n + 1 downto i + 1
12 x.cj+1 = x.cj
13 x.ci+1 = z
14 for j = x.n downto i
15 x.keyj+1 = x.keyj
16 x.keyi = y.keyt
17 x.n = x.n + 1
18 DISK-WRITE(y)
19 DISK-WRITE(z)
20 DISK-WRITE(x)

A divisão é simples e direta. Temos 3 páginas: x, y e z, onde x é a página mãe da página que será dividida (y) e z é uma nova página, no mesmo
nível que y, para onde copiaremos as chaves e descendentes que sejam maiores que a chave da posição mediana, que será inserida em x. Ao
final do processo, x terá mais uma chave e as páginas y e z terão exatamente t - 1 páginas.

As linhas 1-4 se encarregam da criação de uma nova página, para conter as chaves que são maiores que a mediana da página sendo dividida.
Se a página sendo dividida (y), que é o i-ésimo filho de x (linha 2), for folha, z também será, pois estão no mesmo nível. As linhas 5-6 copiam as
chaves acima da mediana de y para z, para que cada uma fique com exatamente t - 1 chaves. Se for uma página intermediária, que tenha
descendentes, procedemos com a cópia para y nas linhas 7-9.

As linhas 11-12 organizam a página mãe para receber a chave que vai subir do filho y, garantindo que a chave seja inserida na ordem correta
(linha 13). Da mesma forma, os descendentes da página mãe são reorganizados, para que o novo filho (z) seja inserido na posição correta
(linhas14-16).

Por fim, todas as páginas são gravadas em disco, para que as mudanças se tornem permanentes e nossa árvore continue com sua integridade
estrutural garantida.

A complexidade do método B-TREE-SPLIT-CHILD é determinada pelos laços das linhas 5-6 e 8-9, sendo dada por O(t) e Θ(t), pois o tempo
depende do grau mínimo da árvore (t). A complexidade do procedimento em relação a acesso a discos é O(1), pois fazemos exatamente 3
gravações em disco, para cada uma das páginas.

Vejamos, finalmente, o procedimento B-TREE-INSERT-NONFULL, que é o responsável por efetivamente fazer a inserção na página folha

10 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

correta, descendo pela árvore e fazendo todas as divisões de páginas cheias que encontrar pelo caminho. Em seguida, analisaremos a
complexidade de todo o processo de inserção.

B-TREE-INSERT-NONFULL(x, k)
1 i = x.n
2 if x.leaf
3 while i ≥ 1 and k < x.keyi
4 x.keyi+1 = x.keyi
5 i = i - 1
6 x.keyi+1 = k
7 x.n = x.n + 1
8 DISK-WRITE(x)
9 else while i ≥ 1 and k < x.keyi
10 i = i - 1
11 i = i + 1
12 DISK-READ(x.ci)
13 if x.ci.n == 2t - 1
14 B-TREE-SPLIT-CHILD(x, i)
15 if k > x.keyi
16 i = i + 1
17 B-TREE-INSERT-NONFULL(x.ci, k)

As linhas 3-8 cuidam da situação mais simples da inserção, que é a inserção em uma página folha (linha 2), constituindo nosso caso base para
finalizar a recursão pela árvore. Quando chegamos em uma folha pronta para a inserção, todas as divisões já foram feitas e a árvore está
perfeitamente balanceada.

Se a página não for folha, temos que encontrar em qual sub-árvore, a partir da página atual (x), deverá ser feita a inserção (linhas 9-11) de forma
recursiva. Recuperamos a página filha correspondente (x.ci), na linha 12 e verificamos, na linha 13, se esse descendente está cheio. Se estiver
cheio, temos que fazer a divisão antes dessa página antes de continuarmos a recursão, para garantir uma única passada pela árvore (linha 14)
e, após dividir, determinamos em qual das duas páginas após a divisão a inserção deverá continuar (linhas 15-16). Em seguida, seguimos
recursivamente para essa página (linha 17), até chegarmos à folha em que a inserção deverá ocorrer e encerrarmos a recursão.

Para uma árvore B de altura h, a chamada de B-TREE-INSERT executa O(h) acessos a disco, uma vez que somente executamos operações
DISK-READ e DISK-WRITE com complexidade O(1) nas chamadas recursivas de B-TREE-INSERT-NONFULL. O tempo total de CPU é O(th),
sendo h dependente do número de chaves, temos O(th) = O(t logt n).

Segundo Cormen et al. (2009), como o método B-TREE-INSERT-NONFULL tem recursão de cauda, podemos implementá-lo sem recursão,
utilizando um laço de repetição, reduzindo a complexidade espacial (número de páginas na memória a qualquer tempo) para O(1), ou seja, uso
constante da memória principal.

??? Questões para Reflexão


1. A partir do estado final da árvore mostrada na Figura 5, insira, em ordem, as chaves "B", "A", "M", "O", "R", "S", "N", "I", "L", "J", "T", "Z", "U",
"W", "K", "L" e "P".

2. Mostre os resultados da inserção das chaves "F", "S", "Q", "K", "C", "L", "H", "T", "V", "W", "M", "R", "N", "P", "A", "B", "X", "Y", "D", "Z", "E" em
ordem em uma árvore B vazia com grau mínimo 2. Desenhe somente as configurações da árvore exatamente antes de um nó precisar ser
dividido, e também desenhe a configuração final. (Cormen et al., 2009, tradução nossa).

11 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

3.4. Remoção

A remoção de chaves de uma árvore B é similar ao processo de inserção, mas um pouco mais complicada, porque pode ser feita em qualquer
página, não somente nas folhas, de tal forma que teremos que reorganizar os filhos quando uma chave é removida. Assim como na inserção,
temos também que garantir que as propriedades da árvore não sejam violadas e, dessa forma, ela mantenha sua integridade estrutural.

Vamos garantir que nenhuma página tenha menos que o mínimo (t - 1) chaves. Na inserção, dividíamos páginas cheias para garantir que as
operações pudessem ocorrer sem precisar fazer backtracking na árvore, logo, teremos o inverso na remoção: vamos fundir páginas ou pegar
chaves emprestadas de outras, para garantir que, no caminho da remoção, as páginas tenham, pelo menos, t chaves, ou seja, 1 chave a mais
que o mínimo, para casos em que essas movimentações sejam necessárias e garantir que nenhuma fique abaixo do mínimo exigido.

Outra propriedade importante da remoção é que, caso a raiz tenha apenas uma chave e seja necessário fazer a fusão dos seus dois filhos, esta
chave ocupará a posição mediana do filho da esquerda, todas as chaves (e eventuais descendentes) do filho da direita serão copiados para
depois da mediana e essa página se tornará a nova raiz. Assim, a altura da árvore diminui em 1 unidade quando fazemos a fusão da raiz com
seus dois descendentes, mantendo a propriedade de que o crescimento ou retração da árvore ocorre sempre na raiz.

Cormen et al. (2009) não estabelece um pseudocódigo para a remoção, mas prescreve um protocolo a ser seguido para a remoção. A partir
desse protocolo, a produção do algoritmo que faz a exclusão recursivamente, em uma única passagem pela árvore, é simples. A única
exceção para não passar mais de uma vez pela árvore ocorre quando é necessário inserir o predecessor ou o sucessor de uma chave em
seu lugar.

A remoção ocorrerá da seguinte forma:

1. Se a chave k está em uma página folha x, remova k de x.


2. Se a chave k está em uma página interna x, faça:
a) Se o filho y que precede k na página x tem, no mínimo, t chaves, então encontre o predecessor k' de k na sub-árvore enraizada em y (k'
é o maior dos menores). Recursivamente, remova k' e troque k por k' em x (podemos encontrar e remover k' em uma única passagem pela
árvore).
b) Se y tem menos que t chaves, então examine, de forma simétrica, o filho z que segue k na página x. Se z tem pelo menos t chaves,
então encontre o sucessor k' de k na sub-árvore enraizada em z (k' é o menor dos maiores). Recursivamente, remova k' e troque k por k'
em x (podemos encontrar e remover k' em uma única passagem pela árvore).
c) Caso contrário, se ambos y e z possuírem apenas t - 1 chaves, faça a fusão de k e todo o conteúdo de z em y, de tal forma que x perde
a chave k e o ponteiro para z, e y agora passa a conter 2t - 1 chaves. Então desaloque z e recursivamente remova k de y.
3. Se a chave k não está presente na página interna x, determine a raiz x.ci da sub-árvore apropriada que deve conter k, se k existir na
árvore. Se x.ci tem apenas t - 1 chaves, execute o passo 3a ou 3b conforme necessário para garantir que vamos descer para uma página
contendo, no mínimo, t chaves. Então finalize fazendo a recursão no filho apropriado de x.
a) Se x.ci tem apenas t - 1 chaves mas tem um irmão imediato com, no mínimo, t chaves, dê a x.c i uma chave extra trocando uma chave
de x para x.ci, e movendo a chave do irmão de x.ci imediatamente à esquerda ou à direita para x, e movendo o ponteiro apropriado dos
descendentes desse irmão x.ci para x.ci. Inicie pelo irmão da esquerda, caso ele não tenha como emprestar essa chave, analise o irmão
da direita.
b) Se x.ci e ambos os irmãos imediatos possuem t - 1 chaves, faça a fusão de x.ci com um dos irmãos (preferencialmente o da esquerda),
movendo uma chave de x para o nó em que as chaves serão agrupadas, para se tornar a mediana daquele nó, e todos os ponteiros dos
descendentes, se houver.

Considerando que uma árvore B tem mais chaves nas folhas, podemos esperar que ocorram mais remoções nas folhas que nas páginas
internas da árvore. Embora façamos apenas uma passagem pela árvore, pode ocorrer a necessidade de retornar para um nó de onde uma
chave foi deletada para substituí-la por seu predecessor ou sucessor (casos 2a e 2b).

Embora o procedimento pareça complicado, ele envolve apenas O(h) operações em disco, sendo h a altura da árvore, desde que as operações
de leitura e escrita em disco feitas entre as chamadas recursivas sejam O(1). O tempo total de CPU é dado por O(th) = O(t log t n).

Vamos analisar o processo de remoção de algumas chaves a partir de uma árvore B, para compreendermos melhor os passos do processo de
remoção. A Figura 6 ilustra a árvore inicial e, nas figuras subsequentes, procederemos com as remoções. A árvore tem grau mínimo t = 3, mas
não serão representados todos os espaços para chaves para reduzir o espaço da representação, podemos adicioná-los e removê-los quando
houver alteração no número de chaves de cada página.

Figura 6 - Árvore B inicial para remoção

12 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

Fonte: Adaptado de Cormen et al. (2009)

Considerando a árvore da Figura 6, vamos proceder com a remoção da chave "F". Começamos pela raiz e verificamos que a chave não está na
página e ela não é folha. Determinamos que "F" é menor que "P", portanto, devemos fazer a remoção a partir do filho da esquerda dessa chave.
Recuperamos a página e observamos que ela não é folha e não tem a chave, e que a chave pode estar no filho da direita "C". Como a página
tem t chaves, podemos recomeçar a recursão a partir desse filho. Buscamos a página em disco e verificamos que ela tem t chaves, então
podemos fazer mais uma recursão nesse nível, de tal forma que percebemos que a página é folha e tem a chave, sendo seguro fazer a remoção
porque não ficará com menos de t - 1 chaves. A Figura 7 ilustra a árvore após essa remoção, indicando a página em que foi feita a alteração.

Figura 7 - Remoção de F (caso 1)

Fonte: Adaptado de Cormen et al., 2009

A partir desse nova configuração da árvore, vamos proceder com a remoção da chave "M". Começamos novamente pela raiz, que não tem a
chave desejada. Determinamos que a chave deve estar no filho da direita de "P", buscamos essa página e verificamos que ela tem pelo menos t
chaves, sendo seguro fazer a recursão. Reiniciada a recursão, vemos que a chave está na página e ela não é folha. Nesse caso, entramos nas
opções do item 2 do algoritmo e vamos verificar os filhos. O filho da esquerda contém t  chaves ("J", "K" e "L"), então observamos o filho da
direita, que tem o mínimo de t chaves, então vamos buscar, a partir dessa página filha, o predecessor de "M" (ou seja, a maior chave à
esquerda), que é "L". O predecessor está em uma página folha com t chaves, então é seguro removê-la e colocar no lugar de "M", tomando seu
lugar e efetivamente removendo-a da árvore. O procedimento para buscar o predecessor ou o sucessor pode ser feito buscando a maior chave
e fazendo uma chamada para o método de remoção desse valor, que é ineficiente, ou podemos unificar em um único procedimento, que busca e
remove essa chave para realizar a troca (garantindo a integridade da árvore como um todo, assim como a remoção). A Figura 8 ilustra a árvore
atualizada e as páginas que foram alteradas no processo.

Figura 8 - Remoção de M (caso 2a)

Fonte: Adaptado de Cormen et al., 2009.

Agora vamos remover a chave "G" da árvore. Começamos novamente pela raiz, que não é folha e não contém a chave. determinamos que a
chave "G", se existir, deve estar na sub-árvore enraizada no filho da esquerda de "P", buscamos essa página e verificamos que ela é uma
página interna e possui a chave que desejamos remover. Como ela possui t chaves, podemos continuar a remoção a partir dessa página,
recursivamente. Como a página é interna e tem a chave, mas a remoção vai fazer com que fique com apenas t - 1 chaves (que é o mínimo, mas
a remoção garante que as páginas tenham pelo menos t chaves), vamos analisar os descendentes dessa chave. O filho da esquerda tem
apenas t - 1chaves ("D" e "E"), então temos que olhar o da direita, que também tem t - 1 chaves ("J" e "K"). Nesse caso, como ambos possuem t
- 1 chaves, vamos descer a chave "G" para o filho da esquerda e copiar as chaves do filho da direita para o filho da esquerda, logo após "G".
Como são páginas folha, não há descendentes para copiarmos e podemos remover o filho da direita da posição em que "G" estava na página
mãe, movendo as chaves e seus descendentes para que a árvore fique correta. Agora chamamos a recursão para a página que foi recém
fundida, como é folha e tem a chave, simplesmente removemos "G" dessa página. A Figura 9 mostra o resultado dessas alterações.

Figura 9 - Remoção de G (caso 2c)

13 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

Fonte: Adaptado de Cormen et al., 2009.

Agora, vamos remover a chave "D". Teremos uma situação especial, pois a chave não está na raiz, que não é folha. Olhamos o filho da
esquerda, que possui apenas t - 1 chaves ("C" e "L"), temos que conferir o da direita, que está na mesma situação ("T" e "X"). Nesse caso,
temos que fazer a fusão das páginas, trazendo para o filho da esquerda a chave "P", que está na raiz, e as chaves do irmão da direita e todos os
descendentes desse irmão. Como a raiz ficou vazia, removemos e dizemos que esse único descendente é a nova raiz, fazendo nossa árvore
reduzir sua altura em 1 unidade. Agora recomeçamos o processo de remoção a partir dessa nova raiz: a chave não está na página e ela não é
folha, determinamos que poderá estar no filho da esquerda de "L" (que é, ao mesmo tempo, o filho da direita de "C") e buscamos essa página
para analisar. Como ela tem mais de t chaves, é seguro chamar a recursão para essa página. Nesse novo passo recursivo, determinamos que a
página é folha e possui a chave, bastando fazer a remoção. A Figura 10 ilustra as transformações da árvore nesse procedimento, mostrando a
fusão da raiz e, posteriormente, a remoção da chave (a seta pontilhada, em vermelho, indica a mudança dos estados da árvore, não
descendência de páginas).

Figura 9 - Remoção de D (caso 3b)

Fonte: Adaptado de Cormen et al., 2009.

Vamos, por fim, a um último cenário, removendo a chave "B" da árvore. Começamos pela raiz, detectando que não é folha e não contém a
chave que desejamos remover, e também que possui mais que t chaves. Determinamos que a chave deve estar, se presente na árvore, no filho
da esquerda da chave "C" e o trazemos para a memória. Ao analisá-lo, já percebemos que não tem t chaves e precisaremos preparar a árvore
para a remoção. Observamos que não há um irmão à esquerda para analisar, então verificamos o irmão da direita, que tem exatamente t
chaves. Faremos um empréstimo de chave do irmão da direita, movendo a chave da página mãe para a página da esquerda, o sucessor de "B"
(que corresponde à menor chave na sub-árvore da direita, "E") para o lugar da chave na página mãe. Terminada essa rotação, a árvore está
preparada, uma vez que no caminho da remoção temos pelo menos t chaves (no filho da esquerda). Entramos na recursão nessa página,
observamos que a chave está presente e é uma folha, então basta fazer a remoção. A Figura 10 ilustra o estado final da árvore após as
remoções.

Figura 10 - Remoção de B (caso 3a)

Fonte: Adaptado de Cormen et al., 2009.

??? Questões para Reflexão


1. Remova as seguintes chaves, nesta ordem: "C", "P", "V", "J", "O", "Y", "R", "X", "T".
2. Escreva o pseudocódigo da remoção e dos métodos auxiliares, ou seja, a fusão, a busca do sucessor, a busca do predecessor, etc.,

14 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

devem ser executadas em funções menores e dedicadas a esses subproblemas.


3. Para evitar fazer primeiro uma busca para determinar se a chave existe na árvore, para depois fazer a remoção, podemos modificar o
processo de remoção para que ele sinalize se foi feita alguma remoção ou a chave não existia. Como?

15 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

4. Árvores B* e B+

Existem variantes da árvore B, buscando otimizar algumas de suas características. Vamos verificar duas delas, as árvores B* e B+.

Uma árvore B* é como uma árvore B, em termos de estrutura física, com armazenamento em páginas internas e folhas. A diferença é que,
enquanto a taxa de ocupação das páginas numa árvore B é de 50%, pois o mínimo é de t - 1 chaves, e as operações de inserção e remoção
buscam manter próximo a esse nível, como já vimos; uma árvore B* terá uma taxa de ocupação de cerca de 66%, pois o limite mínimo de uma
página será de 2/3 do total. O objetivo é produzir árvores mais compactas, que façam uso mais eficiente do espaço em disco.

A inserção e a remoção são alteradas para fazer essa mudança de capacidade. Durante a inserção, redistribuímos as chaves das páginas filhas,
para que fiquem com pelo menos 2/3 da capacidade, e só fazemos divisões quando estiverem completamente cheias. Em outras palavras,
atrasamos a operação de split até que 2 páginas irmãs estejam em sua capacidade total, dividindo o conteúdo delas entre 3 páginas (as duas e
uma terceira, nova, o chamado split 2-para-3), com 2/3 da capacidade preenchida em cada uma. 

A árvore B+ mantém as capacidades de uma árvore B, mas adota uma abordagem diferente. Os dados completos são armazenados apenas
nas páginas folha, enquanto nas páginas internas só guardamos chaves que servem como índices para o conteúdo das folhas. Além dessa
diferença, adicionamos  um ponteiro em cada página folha, para que aponte para a página folha a sua direita, ligando-as de forma sequencial.
As páginas internas são chamadas index set e guardam apenas chaves, enquanto as folhas são chamadas de sequence set, com todos os
dados armazenados. Os conteúdos podem, inclusive, estar em arquivos separados. Árvores B+ são bastante utilizadas em sistemas
gerenciadores de banco de dados.

Podemos perceber a vantagem desses ponteiros extras, se consideramos a intenção de imprimir todo o conteúdo da árvore em ordem
crescente. Na árvore B, precisamos fazer o percurso in-order, que visitará várias vezes cada um dos nós intermediários, para encontrar todos os
descendentes. Numa árvore B+, não precisamos fazer o percurso in-order, basta descermos para a folha mais à esquerda e percorrer o
conteúdo daquela página, quando finalizarmos, seguimos o ponteiro para a página da direita, sem precisar voltar nas páginas intermediárias, o
que é economiza muito tempo, especialmente em acessos a disco.

A inserção e a remoção na árvore B+ não diferem muito da árvore B original, observada a restrição de termos dados apenas nas folhas. No
index set, fazemos os ajustes para que as chaves possam orientar o processo de localização dos dados, sendo mais simples que na árvore B.

16 of 17 08/11/2022 09:57
Árvores n-árias https://ead5.ifmg.edu.br/bambui/mod/book/tool/print/inde...

5. Referências

CORMEN, Thomas H.; LEISERSON, Charles E.; RIVEST, Ronald L.; STEIN, Clifford. Introduction to Algorithms. 3rd ed. Cambridge,
Massachusetts: The MIT Press, 2009. 1292 p. ISBN 978-0-262-53305-8.

ZIVIANI, Nívio. Projeto de algoritmos: com implementações em Pascal e C. 4. ed. São Paulo: Pioneira, 1999. ISBN 85-221-98-5286.

17 of 17 08/11/2022 09:57

Você também pode gostar