# data[].attributes.body
As árvores B são árvores de pesquisa equilibradas que foram feitas para funcionar bem em disco ou em outros dispositivos de armazenamento secundário de acesso direto. As árvores B são muito similares às árvores rubro-negras, porém são mais adequadas para minimizar as operações de E/S do disco.

A diferença relevante entre a árvore B e a árvore rubro-negra é que os nós da árvore B podem ter muitos filhos, podendo diferenciar entre dezenas a milhares. O impacto do "fator de ramificação" de uma árvore B é enorme, embora geralmente seja determinado pelas características das unidades de disco usadas. As árvores B são similares às árvores rubro-negras, pois a altura de cada árvore B com n nós é O(lg n), embora a altura de uma árvore B possa ser muito menor do que a de uma árvore rubro-negra porque seu fator de ramificação pode ser muito maior. Consequentemente, as árvores B também podem ser usadas para implementar muitas operações em coleções dinâmicas em tempo O(lg n).

As árvores B desenvolvem as árvores binárias de busca de maneira natural. A imagem no inicio é uma representação simples de uma árvore B.

# data[].attributes.body
Assim como nas árvores de busca binária e nas árvores rubro-negras, qualquer "informação de satélite" associada a uma chave é armazenada no mesmo nó que a chave. Na prática, é possível armazenar apenas um ponteiro por chave para outra página de disco contendo as informações de satélite correspondentes a essa chave. Sempre que uma chave é movida de um nó para outro, o satélite se move com a chave.  Uma árvore B T é uma árvore enraizada com as seguintes propriedades.

1. Todo nó x tem os seguintes campos:

a. n[x], o número de chaves atualmente armazenadas no nó x,
b. as próprias n[x] chaves, armazenadas em ordem não decrescente, de modo que chave₁[x] ≤ chave₂[x] <= ... <= chave n[x][x]
c. folha[x], um valor booleano que é TRUE sex é uma folha, e FALSE sex é um nó interno.

2. Cada no interno x também contém n[x] + 1 ponteiros c₁[x], C₂[x],..., Cn[x] + 1[x] para seus filhos. Os nós de folhas não têm filhos, e assim seus campos ci são indefinidos.

3. As chaves chavei[x] separam os intervalos de chaves armazenadas em cada subárvore: se
ki é qualquer chave armazenada na subárvore com raiz ci[x], então

k₁ ≤ chave₁[x] ≤ k₂ ≤ chave₂[x] ≤... ≤ chaven[x][x] ≤ kn[x] +1 .

4. Toda folha tem a mesma profundidade, que é a altura h da árvore.

5. Existem limites inferiores e superiores sobre o número de chaves que um nó pode conter.
Esses limites podem ser expressos em termos de um inteiro fixo t ≥ 2 chamado grau mínimo da árvore B:

a. Todo nó diferente da raiz deve ter pelo menos t - 1 chaves. Desse modo, todo nó interno diferente da raiz tem pelo menos t filhos. Se a árvore é não vazia, a raiz deve ter
pelo menos uma chave.
b. Todo nó pode conter no máximo 2t - 1 chaves. Então, um nó interno pode ter no
máximo 2t filhos. Dizemos que um nó é completo se ele contém exatamente 2t - 1
chaves.

A árvore B mais simples ocorre quando t = 2. Todo nó interno tem então 2, 3 ou 4 filhos, e temos uma árvore 2-3-4.

# data[].attributes.body
O número de acessos ao disco para a maioria das operações em uma árvore B é proporcional à altura da árvore B.

O teorema:
No pior caso de uma árvore B temos que n >= 1, então para qualquer árvore B T de n nós de altura h  e grau mínimo t>= 2, h <= logt (n+1/2)

Usando a Prova: Se uma árvore B tem altura h, o número de seus nós é minimizado quando a raiz contém uma chave e todos os outros nós contêm t-1 chaves.

n >= 1 + (t - 1) ∑i=1 h 2ti-1
1 + 2(t - 1)(th - 1/t - 1)
2th - 1.

Logo com álgebra obtemos th <= (n+1)/2.
Com isso vemos o uso de logaritmos de base t em ambos os lados, provando o teorema.

# data[].attributes.body
As operações básicas são as de procura, criação e inserção de elementos.

Pesquisa em uma árvore B

A pesquisa em uma árvore B é bastante semelhante a pesquisa em uma árvore de pesquisa binária, exceto pelo fato de que, em vez de tomar uma decisão de ramificação binária ou de duas vias em cada nó, tomamos uma decisão de ramificação de várias vias, de acordo com o número de filhos do nó. Precisamente, em cada nó interno x, tomamos uma decisão de ramificação de (n[x] + 1) via.

O algoritmo B-Tree-Search toma como entrada um ponteiro para o nó de raiz x de uma subárvore e uma chave k a ser pesquisada nessa subárvore. Se k está na árvore B, B-Tree-Search retorna o par ordenado (y, i) consistindo em um no y e um índice i tal que chavei[y] = k. Caso contrário, retorna Null.

Criar uma árvore B vazia

Para construir uma árvore B, primeiro criamos um nó raiz vazio,e depois inserimos novas chaves. Esses procedimentos alocam uma página de disco para ser usada como um novo nó no tempo O(1).

Inserção de uma chave em uma árvore B

Criado o nó raiz, a inserção das próximas chaves seguem o mesmo procedimento: busca-se a posição correta da chave em um nó folha e insere a chave garantindo a ordenação destas. Após feito isso, considerando a abordagem de inserção de baixo para cima (Bottom-up) na árvore B, podem ocorrer duas situações:

1. Página folha está com um número menor de chaves do que o máximo permitido (d-1):

Nesse caso apenas inserimos a chave de maneira ordenada na página

2. Página folha completa ou com o número máximo de chaves permitido (d-1):

Nesse caso ocorre o overflow da página em questão e é necessário a operação de split para manter o balanceamento da árvore.

Primeiramente escolhe-se um valor intermediário na sequência ordenada de chaves da página incluindo-se a nova chave que deveria ser inserida. Este valor é exatamente uma chave que ordenada com as chaves da página estará no meio da sequência.

Cria-se uma nova página e os valores maiores do que a chave intermediária são armazenados nessa nova página e os menores continuam na página anterior (operação de split).

Esta chave intermediária escolhida deverá ser inserida na página pai, na qual poderá também sofrer overflow ou deverá ser criada caso em que é criada uma nova página raiz. Esta série de overflows pode se propagar para toda a árvore B, o que garante o seu balanceamento na inserção de chaves.

A divisão de um nó em uma árvore B

A função do split é dividir o nó em duas partes e "subir" o valor central do nó para um nó acima ou, caso o nó que sofreu o split seja a raiz, criar uma nova raiz com um novo nó.

A operação toma como entrada um nó interno não completo x, um índice i e um nó y tal que y = ci[x] eh um filho completo de x. Então o procedimento divide esse filho em dois e ajusta x de modo que ele tenha agora um filho adicional. Para dividir uma raiz completa, primeiro transformaremos a raiz em um filho de um novo nó raiz vazia. Assim, a árvore cresce uma unidade em altura; a divisão é o único meio pela qual a árvore cresce.

# data[].attributes.body
Passos:

1. A entrada x está em uma folha?

1.1. Se sim: remova a entrada x do nó folha. Fim.

1.2. Se não: passe para o passo 2;

# data[].attributes.body
2. A entrada x não está no nó folha?

2.1. Se não estiver no nó folha:

2.1.1. Se uma entrada y imediatamente maior                                      que uma entrada x estiver em um nó                                          folha: substitua a chave x pela chave y                                      imediatamente maior que a chave x,                                          remova a chave x. Fim.

2.2. Se estiver em um nó folha: repita o passo 1.

# data[].attributes.body
Sites e Livros de onde retiramos o conteúdo:

Arvore B - Wikipedia

CORMEN, Thomas H. et al. (..). Algoritmos: teoria e prática. Rio de Janeiro: Elsevier, c2002.. xvii, 916 p. ISBN 9788535209266 (broch.).

AULA 10: Árvore B. 2021. (58 min.), MP4, son., Cor. Série Estrutura de Dados e Seus Algoritmos. Disponível em: https://www.youtube.com/watch?v=38RM8_VMIcI. Acesso em: 27 nov. 2022.