0% acharam este documento útil (0 voto)
12 visualizações494 páginas

Edf Java

O livro 'Programação Modular: Estruturas de Dados Fundamentais em Java' de Roberto S. Bigonha aborda conceitos essenciais de estruturas de dados e algoritmos em Java, incluindo análise de complexidade, tipos abstratos de dados e diversas implementações de estruturas como listas, pilhas e árvores. Publicado em 2021, o autor é um renomado professor e pesquisador na área de Ciência da Computação. O conteúdo é estruturado em capítulos que detalham desde a teoria até a prática de implementação.

Enviado por

guiestaramusic
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
12 visualizações494 páginas

Edf Java

O livro 'Programação Modular: Estruturas de Dados Fundamentais em Java' de Roberto S. Bigonha aborda conceitos essenciais de estruturas de dados e algoritmos em Java, incluindo análise de complexidade, tipos abstratos de dados e diversas implementações de estruturas como listas, pilhas e árvores. Publicado em 2021, o autor é um renomado professor e pesquisador na área de Ciência da Computação. O conteúdo é estruturado em capítulos que detalham desde a teoria até a prática de implementação.

Enviado por

guiestaramusic
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

Belo Horizonte

PROGRAMAÇÃO MODULAR
Estruturas de Dados
Fundamentais
em Java

Roberto
S.
Bigonha
PROGRAMAÇÃO MODULAR

Estruturas de Dados Fundamentais


em Java

Roberto S. Bigonha

Belo Horizonte, MG
2021
Roberto da Silva Bigonha: PhD em Ciência da Com-
putação pela Universidade da Califórnia, Los Angeles. Professor
Emérito da Universidade Federal Minas Gerais. Membro da Socie-
dade Brasileira de Computação. Áreas de interesse: Linguagens de
Programação, Programação Modular, Estruturas de Dados, Com-
piladores e Semântica Formal.
Dados Internacionais de Catalogação na Publicação
(CIP)
(Câmara Brasileira do Livro, SP, Brasil)
Bigonha, Roberto S.
PROGRAMAÇÃO MODULAR: Estruturas de
Dados Fundamentais em Java / Roberto da Silva
Bigonha — Belo Horizonte, MG, 2021, 485 p
Bibliografia.
ISBN 978-65-00-22441-2
B594 1. Estruturas de Dados
2. Complexidade de Algoritmos
3. Estruturas Lineares e Hierárquicas
4. Árvores de Pesquisa AVL, B, SBB, Patricia e
Rubro-Negra
I. Tı́tulo.
CDD: 005.1 CDU: 004.42
Índice para catálogo sistemático:
1. Estruturas de Dados
COPYRIGHT c 2021 - Roberto S. Bigonha
Todos os direitos reservados. Nenhuma parte deste livro poderá ser
reproduzida, sejam quais forem os meios empregados, sem a per-
missão por escrito do Autor. Aos infratores aplicam-se as sanções
previstas nos artigos 102, 104, 106 e 107 da Lei 9.610, de 19 feve-
reiro de 1998.
Sumário

Prefácio viii

1 Análise de Complexidade 1
1.1 Medidas via modelo matemático . . . . . . . . . . 2
1.2 Cálculo do custo . . . . . . . . . . . . . . . . . . . 3
1.3 Análise de algoritmos não-recursivos . . . . . . . . 9
1.4 Análise de algoritmos recursivos . . . . . . . . . . . 16
1.5 Complexidade assintótica . . . . . . . . . . . . . . 27
1.6 Ordens de complexidade . . . . . . . . . . . . . . . 36
1.7 Complexidade e velocidade do processador . . . . . 39
1.8 Outros limites assintóticos . . . . . . . . . . . . . . 43
1.9 Fórmulas importantes . . . . . . . . . . . . . . . . 48
1.10 Conclusão . . . . . . . . . . . . . . . . . . . . . . 51

2 Tipos Abstratos de Dados 55


2.1 Lacuna semântica . . . . . . . . . . . . . . . . . . 56
2.2 Modularidade . . . . . . . . . . . . . . . . . . . . 59
2.3 Noção de abstração de dados . . . . . . . . . . . . 61
2.4 Tipo abstrato de dados . . . . . . . . . . . . . . . 63
2.5 Implementações de tipos . . . . . . . . . . . . . . . 65
2.6 Conceito de dicionário . . . . . . . . . . . . . . . . 72
2.7 Conclusão . . . . . . . . . . . . . . . . . . . . . . 74
iii
iv SUMÁRIO

3 Tabelas 75
3.1 Implementação de tabelas . . . . . . . . . . . . . . 76
3.2 Tabelas Ordenadas . . . . . . . . . . . . . . . . . . 80
3.3 Conclusão . . . . . . . . . . . . . . . . . . . . . . 85

4 Listas 87
4.1 Listas sequenciais . . . . . . . . . . . . . . . . . . 89
4.2 Listas sequenciais com tratamento de falhas . . . . 94
4.3 Listas sequenciais genéricas . . . . . . . . . . . . . 98
4.4 Listas encadeadas . . . . . . . . . . . . . . . . . . 102
4.5 Listas máquina de estado . . . . . . . . . . . . . . 107
4.6 Listas duplamente encadeadas . . . . . . . . . . . . 114
4.7 Exercı́cio com listas encadeadas . . . . . . . . . . . 121
4.8 Conclusão . . . . . . . . . . . . . . . . . . . . . . 128

5 Pilhas 131
5.1 Pilhas sequenciais . . . . . . . . . . . . . . . . . . 132
5.2 Pilhas sequenciais heterogêneas . . . . . . . . . . . 135
5.3 Pilhas encadeadas . . . . . . . . . . . . . . . . . . 136
5.4 Pilhas genéricas . . . . . . . . . . . . . . . . . . . 140
5.5 Conclusão . . . . . . . . . . . . . . . . . . . . . . 143

6 Filas 145
6.1 Filas sequenciais . . . . . . . . . . . . . . . . . . . 146
6.2 Filas encadeadas . . . . . . . . . . . . . . . . . . . 151
6.3 Conclusão . . . . . . . . . . . . . . . . . . . . . . 155

7 Tabelas de Dispersão 157


7.1 Transformação de chaves em endereços . . . . . . . 158
7.2 Resolução de colisões com listas encadeadas . . . . 161
7.3 Resolução de colisões com endereçamento aberto . . 166
SUMÁRIO v

7.4 Conclusão . . . . . . . . . . . . . . . . . . . . . . 172

8 Árvores 175
8.1 Árvores binárias . . . . . . . . . . . . . . . . . . . 179
8.2 Caminhamentos em árvores binárias . . . . . . . . 184
8.3 Caminhamentos com iteradores . . . . . . . . . . . 189
8.4 Árvores binárias estendidas . . . . . . . . . . . . . 193
8.5 Conclusão . . . . . . . . . . . . . . . . . . . . . . 195

9 Filas de Prioridades 197


9.1 Árvore binária completa . . . . . . . . . . . . . . . 198
9.2 Conceito de heaps . . . . . . . . . . . . . . . . . . 200
9.3 Construção de filas de prioridade . . . . . . . . . . 201
9.4 Implementação de fila de prioridades . . . . . . . . 205
9.5 Uso de fila de prioridade . . . . . . . . . . . . . . . 208
9.6 Conclusão . . . . . . . . . . . . . . . . . . . . . . 210

10 Árvores Patricia 213


10.1 Árvores de pesquisa digital . . . . . . . . . . . . . 214
10.2 Árvores trie . . . . . . . . . . . . . . . . . . . . . 220
10.3 Conceito de árvore Patricia . . . . . . . . . . . . . 226
10.4 Transformação de trie em Patrı́cia . . . . . . . . . 228
10.5 Construção direta de Patricia binária . . . . . . . . 231
10.6 Implementação de Patricia I . . . . . . . . . . . . 238
10.7 Chaves de tamanhos variados . . . . . . . . . . . . 254
10.8 Implementação de Patricia II . . . . . . . . . . . . 263
10.9 Patricia m-árias . . . . . . . . . . . . . . . . . . . 267
10.10 Conclusão . . . . . . . . . . . . . . . . . . . . . . 279

11 Árvores Binárias de Pesquisa 281


11.1 Conceito de árvores binárias de pesquisa . . . . . . 282
vi SUMÁRIO

11.2 Tipo abstrato de dados Abp . . . . . . . . . . . . . 283


11.3 Análise de custo . . . . . . . . . . . . . . . . . . . 298
11.4 Conclusão . . . . . . . . . . . . . . . . . . . . . . 300

12 Árvores de Busca Balanceadas 301


12.1 Dicionário com AVL . . . . . . . . . . . . . . . . . 303
12.2 Pesquisa por um item . . . . . . . . . . . . . . . . 306
12.3 Inserção de itens . . . . . . . . . . . . . . . . . . . 307
12.4 Rebalanceamento após inserção . . . . . . . . . . . 310
12.5 Exemplo de inserções sucessivas . . . . . . . . . . . 320
12.6 Remoção de itens . . . . . . . . . . . . . . . . . . 324
12.7 Rebalanceamento após remoção . . . . . . . . . . . 328
12.8 Exemplo de remoções sucessivas . . . . . . . . . . . 340
12.9 Conclusão . . . . . . . . . . . . . . . . . . . . . . 346

13 Árvores B 349
13.1 Pesquisa por itens . . . . . . . . . . . . . . . . . . 354
13.2 Implementação da operação pesquise . . . . . . . . 355
13.3 Inserção de itens . . . . . . . . . . . . . . . . . . . 356
13.4 Implementação da operação insira . . . . . . . . . 364
13.5 Remoção de itens . . . . . . . . . . . . . . . . . . 369
13.6 Implementação da operação retire . . . . . . . . . 389
13.7 Análise dos custos das operações . . . . . . . . . . 395
13.8 Variantes de Árvores B* . . . . . . . . . . . . . . 396
13.9 Conclusão . . . . . . . . . . . . . . . . . . . . . . 400

14 Árvores SBB 401


14.1 Pesquisa por itens . . . . . . . . . . . . . . . . . . 404
14.2 Transformações de Bayer . . . . . . . . . . . . . . 405
14.3 Inserção de itens . . . . . . . . . . . . . . . . . . . 418
14.4 Implementação da inserção . . . . . . . . . . . . . 424
SUMÁRIO vii

14.5 Remoção de itens . . . . . . . . . . . . . . . . . . 427


14.6 Exemplo de remoções sucessivas . . . . . . . . . . 436
14.7 Implementação de remoção . . . . . . . . . . . . . 448
14.8 Análise de complexidade das operações . . . . . . . 451
14.9 Conclusão . . . . . . . . . . . . . . . . . . . . . . 452

15 Árvores Rubro-Negras 453


15.1 Pesquisa por itens . . . . . . . . . . . . . . . . . . 460
15.2 Rotação de nodos . . . . . . . . . . . . . . . . . . 461
15.3 Inserção de itens . . . . . . . . . . . . . . . . . . . 463
15.4 Remoção de itens . . . . . . . . . . . . . . . . . . 468
15.5 Conclusão . . . . . . . . . . . . . . . . . . . . . . 476

Bibliografia 477

Índice Remissivo 483


Prefácio
A série de livros Programação Modular destina-se a alunos de cur-
sos de graduação da área de Computação, como Ciência da Com-
putação, Análise de Sistemas, Matemática Computacional, En-
genharia de Computação, Engenharia de Sofware e Sistemas de
Informação. As técnicas apresentadas são voltadas para o desen-
volvimento de programas e complexos e de grande porte.
Este livro, Estruturas de Dados Fundamentais em Java,
mostra detalhes da implementação eficiente e modular das estrutu-
ras de dados fundamentais, usando as técnicas descritas em outros
volumes desta série.
Neste volume são estudados medidas de complexidade de al-
goritmos, tabelas sequenciais, tabelas de dispersão, listas, pilhas,
filas, filas com prioridades e diversos tipos de árvores de pesquisa.

viii
Capı́tulo 1

Análise de Complexidade

Quelli che s’innamoran di pratica


senza scienzia sono como ’l nocchieri
ch’entra in navilio sanza timone o bus-
sula, che mai ha certezza dove si vada 1
Leonardo da Vinci (1452-1519)

A determinação dos custos dos algoritmos é indispensável quando


se deseja compará-los conforme seus desempenhos, de forma a es-
colher corretamente a melhor solução.
Pode-se determinar o tempo de execução de um algoritmo me-
dindo-se, por meio de um relógio, o tempo que ele gasta para vários
conjuntos de dados de entrada em um dado computador.
Esse método, entretanto, apresenta vários pontos negativos, pois,
em primeiro lugar, o algoritmo precisa ser implementado, e, ainda
assim, pode não ser possı́vel realizar as medidas de tempo para
todas as suas possı́veis entradas. Execuções experimentais só po-
dem ser feitas para um número limitado de entradas, quando, para
ser conclusivo, dever-se-ia considerar todas as entradas possı́veis,
inclusive as intratáveis.
Há também o fato de as medidas obtidas pela medição direta do
1
Aquele que se enamora da prática sem o devido conhecimento cientı́fico é como o marinheiro que entra
em um navio sem bússola ou leme, não tem a menor ideia para onde vai.

1
2 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

tempo de execução serem dependentes do compilador usado para


implementar o algoritmo, pois compiladores podem gerar códigos
distintos para construções equivalentes, e também por que os tem-
pos medidos dependem do desempenho do hardware no qual o
algoritmo é executado. Além disso, análise de algoritmos deveria
ser realizada sem necessidade de implementá-los.
Isso sugere a necessidade de um método de análise que seja in-
dependente desses fatores, por exemplo, um que use modelos ma-
temáticos apropriados, determinados a partir da estrutura dos al-
goritmos, sem necessidade de sua implementação nem de execuções
experimentais.

1.1 Medidas via modelo matemático

O desempenho de muitos algoritmos depende do tamanho do pro-


blema a ser resolvido. Problemas pequenos, como ordenar um
arranjo contendo poucos números inteiros, são resolvidos com rapi-
dez, por pior que seja o método usado. Por outro lado, a ordenação
de um milhão de inteiros somente é eficiente com o uso de soluções
apropriadas.
O estudo da complexidade e do custo de algoritmos está dire-
tamente vinculado ao tamanho do problema a ser resolvido, que
deve ser devidamente identificado em cada caso. Algumas vezes, o
tamanho do problema, que é um inteiro, coincide com o tamanho
dos dados de entrada. Outras vezes, o tamanho tem a ver com o
esforço necessário para se chegar ao resultado desejado. Por exem-
plo, no caso da ordenação de inteiros, o tamanho do problema é o
tamanho do arranjo a ser ordenado. O tamanho de um problema
envolvendo grafos pode ser o número de nodos. Por outro lado, em
um algoritmo para listar todos os divisores de um número inteiro, o
1.2. CÁLCULO DO CUSTO 3

tamanho do problema é o valor desse inteiro. Desta forma, a análise


da complexidade de um algoritmo começa com a identificação da
variável inteira que representa o tamanho do problema.
O segundo passo no estudo da complexidade é definição do tipo
de custo que se deseja avaliar, pois há dois tipos de custos a conside-
rar: tempo de execução e consumo de memória. Esses dois aspectos
são muito importantes para a avaliação e comparação de soluções
algorı́tmicas. Entretanto, como a questão do custo de tempo da
execução surge com mais frequência no estudo de algoritmos e de
suas aplicações, ela recebe aqui mais atenção.
Define-se, assim, que Função de Custo ou Função de Com-
plexidade f (n) é a medida do tempo necessário para executar
um algoritmo para um problema de tamanho n. Note que o default
é o custo de tempo. Quando tratar-se de consumo de memória,
deve-se explicitá-lo claramente.
O terceiro passo consiste na associação de custo a cada instrução
do algoritmo e então contabilizar o custo de todas as operações que
são usadas na solução de problemas.

1.2 Cálculo do custo

Um exemplo singelo de cálculo de função de custo de tempo é o


do algoritmo clássico de pesquisa sequencial em tabelas, o qual, ou
localiza a chave na tabela ou informa que ela não está presente.
No algoritmo de pesquisa, definido na Fig. 1.1, a é um arranjo
contendo n inteiros, indexado de 1 a n. A posição a[0] nunca
é ocupada, sendo usada apenas como sentinela de pesquisa, para
acelerar o método de busca.
A função pesquise retorna um inteiro que, se positivo, é o
ı́ndice da chave encontrada, senão retorna −1 para indicar que
4 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Algoritmo Custos
1 int pesquise(int ch, int[] a, int n){ melhor pior
2 int i = n; c2 c2
3 a[0] = ch; c3 c3
4 while(!(a[i] == ch)) c4 (n + 1)c4
5 i--; 0 nc5
6 if (i != 0) c6 c6
7 return i; c7 = c8 c7 = c8
8 else return -1;
9 }

Figura 1.1 Pesquisa em Tabela I

a chave não está na tabela. A pesquisa sequencial inicia-se na


posição n da tabela, e, a seguir, testam-se os elementos nas posições
inferiores até que o elemento procurado seja encontrado ou que
toda a tabela tenha sido pesquisada. Assim, do ponto de vista de
custo, há pelo menos três casos a considerar nessa análise:

1. O elemento procurado está na posição n da tabela, a primeira


posição inspecionada, sendo encontrado imediatamente, com
apenas uma comparação de chaves, constituindo uma situação
de melhor caso.
2. O elemento procurado não se encontra na tabela, a qual então
será percorrida em toda sua extensão, isto é, serão feitas n + 1
comparações, o que configura o pior caso.
3. O elemento procurado está na posição k, para algum k no
intervalo [1, n − 1], sendo o custo da busca um valor entre o
de melhor e o de pior casos, dito um custo médio. Nesse caso,
alguma suposição deve ser feita a respeito da distribuição dos
dados no arranjo a e das probabilidades de ocorrências das
chaves procuradas. Ambas as distibuições não são facilmente
determináveis, sendo assim o custo médio é de difı́cil cálculo.
1.2. CÁLCULO DO CUSTO 5

No algoritmo da Fig. 1.1, ci, para i no intervalo [2, 8], denota o


custo de execução da instrução i associada.
No algoritmo acima estão anotados em cada linha, para o melhor
e o pior casos, os custos de execução das instruções associadas. Por
exemplo, a instrução i--; da linha (5) tem custo 0 no melhor
caso, e nc5, no pior caso. Ressalta-se que as instruções (7) e (8)
são executadas de forma excludente, mas ambas têm o mesmo
custo, i.e., c7 = c8, o que permite dizer que, nesse algoritmo, o
custo total de ambas é c7, conforme anotado.
O custo do algoritmo deve ser calculado iniciando-se pelo corpo
do comando de repetição mais interno, que na Fig. 1.1 é a linha
(5). Assim, no caso de a chave não estar na tabela, essa linha,
de custo c5, é executada n vezes, onde n é o número de entradas
válidas na tabela, e a linha (4) é executada n + 1 vezes. Esse é o
pior caso.
No melhor caso, linha (5) nunca será executada, sendo 0 seu
custo, e a linha (4) é executada uma única vez. As demais operações
de pesquise são executadas uma única vez, observada a ressalva
acima em relação às linhas (7) e (8).
Em resumo, no melhor caso, o custo de execução do algoritmo
pesquise é:

f (n) = c2 + c3 + c4 + c6 + c7 (1.1)

que pode ser re-escrita como

f (n) = K (1.2)

onde K = c2 + c3 + c4 + c6 + c7 é uma constante dependente da


plataforma em que o algoritmo for executado.
E no pior caso, a condição do comando while da linha (4) tem
custo (n + 1)c4, e o do comando de repetição é (n + 1)c4 + nc5, e
6 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

o custo do algoritmo completo, nesse caso, é


f (n) = c2 + c3 + (n + 1)c4 + nc5 + c6 + c7 (1.3)
que reduz-se a
f (n) = K1n + K2 (1.4)
onde K1 = c4 + c5 e K2 = c2 + c3 + c4 + c6 + c7
Os valores exatos de K, K1 e K2 dependem da plataforma
em que uma implementação do algoritmo pesquise for execu-
tada, portanto, aparentemente, contraria a premissa de que o custo
do algoritmo deve ser avaliado independentemente da sua imple-
mentação. Entretanto, a influência de K da Eq. (1.2) e de K1 e
K2 da Eq. (1.4) no custo final torna-se insignificante para valores
elevados de n.
Por outro lado, como o custo de cada instrução não é mesmo
conhecido, e a influência das constantes computadas pode ser in-
significante para grandes n, pode-se simplicar o cálculo do custo,
ignorando as operações em que o número de vezes em que são exe-
cutadas não depende do tamanho do problema. Para isso, basta
contabilizar as operações que sejam efetivamente relevantes para
determinação do custo final, como exemplificado a seguir.

Instruções executadas
O que realmente interessa, para fins de comparação de algoritmos, é
exatamente a determinação do comportamento do custo em relação
ao tamanho da entrada. O relevante para o custo é o número de
vezes que cada instrução é executada em função do tamanho da en-
trada. Nesse contexto, a análise de custos fica bastante simplicada,
como mostram os cálculos anotados na Fig. 1.2.
O custo, nesse caso, é o número total de vezes que as instruções
são executadas. Note que, no melhor caso, a instrução (4) somente
1.2. CÁLCULO DO CUSTO 7

Algoritmo Custos
1 int pesquise(int ch, int[] a, int n){ melhor pior
2 int i = n; 1 1
3 a[0] = ch; 1 1
4 while (!(a[i] == ch)) 1 n+1
5 i--; 0 n
6 if (i != 0) 1 1
7 return i; 1 1
8 else return -1;
9 }

Figura 1.2 Pesquisa em Tabela II

é executada uma vez, e a instrução (5), nenhuma vez. Entretanto,


no pior caso, as frequências de execução dessas instruções são n + 1
e n, respectivamente.
Assim, contabilizando a frequência de execução de todas as ins-
truções de pesquise, chega-se rapidamente às funções finais de
custo, que são:
f (n) = 5 melhor caso
f (n) = 2n + 5 pior caso

É possı́vel simplificar ainda mais o processo de cálculo do custo,


sem perda de generalidade, observando-se que o número de vezes
que as instruções (2), (3), (6), (7) e (8) são executadas independe
de n, e, portanto, têm pouca influência no custo de problemas de
grande porte, podendo ser ignoradas na análise de custo.
Além disso, as instruções (4) e (5) são executadas praticamente
o mesmo número de vezes, i.e., n + 1 e n vezes, respectivamente,
portanto, basta contar apenas uma deles, e.g, a linha (4), como
mostra Fig. 1.3.
Essas simplificações permitem inferir imediatamente que:
• melhor caso: f (n) = 1, que ocorre quando o registro procurado
8 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Algoritmo Custos
1 int pesquise(int ch, int[] a, int n){ melhor pior
2 int i = n;
3 a[0] = ch;
4 while (!(a[i] == ch)) 1 n+1
5 i--;
6 if (i != 0)
7 return i;
8 else return -1;
9 }

Figura 1.3 Pesquisa em Tabela III

é o primeiro consultado.

• pior caso: f (n) = n + 1, que ocorre quando o registro procu-


rado não está presente na tabela.
Em resumo, no processo simplificado de determinação do custo
do algoritmo em função do tamanho n do problema, ignora-se o
custo das operações que têm pouca ou nenhuma influência no custo
final para grandes n e consideram-se apenas as operações mais
significativas.

Custo médio
Mesmo com o método simplificado de análise de custos, o custo
médio do pesquise pode ser ainda complexo, pois seu cálculo
depende de outros fatores, como a distribuição probabilı́stica das
chaves procuradas.
Para ilustrar esse processo, suponha que toda pesquisa com o
algoritmo da Fig. 1.3 recupere um registro e que não exista pesquisa
sem sucesso.
Suponha também que pi seja a probabilidade de o i-ésimo re-
gistro, para i no intervalo [1, n], ser procurado, e considerando que
1.3. ANÁLISE DE ALGORITMOS NÃO-RECURSIVOS 9

para recuperar o i-ésimo registro são necessárias n − i + 1 com-


parações, então:
1 · pn + 2 · pn−1 + 3 · pn−2 + · · · + n · p1
f (n) = Pn (1.5)
p
i=1 i
Se cada registro tiver a mesma probabilidade de ser acessado que
Pn
todos os outros, então pi = 1/n, para 1 ≤ i ≤ n, e i=1 pi = 1.
Nesse caso, obtém-se o custo médio f (n):
1 1 n(n + 1) n+1
f (n) = (1 + 2 + 3 + · · · + n) = ( )= (1.6)
n n 2 2
Ou seja, a análise do caso esperado, nesse exemplo, com a su-
posição arbitrária de que todos os registros têm a mesma pro-
babilidade de ser acessado, revela que uma pesquisa com sucesso
examina, em média, aproximadamente metade dos registros. En-
tretanto, isso é somente válido no caso de as probabilidades de
acesso aos elementos serem iguais. Em um caso real, pode não ser
possı́vel garantir isso.

1.3 Análise de algoritmos não-recursivos

No cálculo dos custos de um programa que possua procedimentos,


o tempo de execução de cada procedimento deve ser computado
separadamente, um a um, iniciando-se com os procedimentos que
não chamam outros procedimentos. A seguir, devem ser avaliados
os procedimentos que chamam os procedimentos cujos tempos já
foram avaliados. Esse processo deve ser repetido até se chegar ao
programa principal.
Para cálculo de custo de cada um dos procedimentos deve-se:
• identificar a variável que representa o tamanho do problema;
• determinar as instruções consideradas relevantes para o cálculo
do custo;
10 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

• contabilizar o número de vezes em que essas instruções são


executadas em função do tamanho do problema.
Dorante, o custo de um algoritmo será visto como o número de
vezes que suas instruções relevantes são executadas.
O processo de determinação do custo de cada instrução que
contenha operação destacada como relevante para o custo obedece
as seguintes regras:
• As operações ditas não relevantes têm custo 0 e podem ser
ignoradas no cálculo do custo.
• O custo de um comando de atribuição ou de entrada e saı́da
que contenham operação relevante normalmente pode ser con-
siderado 1, exceto quando neles houver chamadas de função,
pois, nesse caso, os custos das chamadas devem ser considera-
dos.
• Se o comando de atribuição dito relevante envolver vetores de
tamanho arbitrariamente grande, deve-se considerar os custos
do processamento desses vetores.
• O tempo de execução de uma expressão relevante é normal-
mente 1, considerando a aplicação dos operadores envolvidos
mais os tempos de avaliação de operandos que sejam cons-
tantes ou variáveis. Entretanto, caso operandos contenham
chamadas de funções, seus custos devem ser considerados.
• O custo de execução de uma sequência de comandos é a soma
dos custos dos comandos relevantes.
• O custo de execução de um comando de decisão é composto
pelo custo de execução dos comandos relevantes executados
dentro do comando condicional, mais o tempo para avaliar a
condição, se ela contiver operação relevante para o custo.
1.3. ANÁLISE DE ALGORITMOS NÃO-RECURSIVOS 11

• O custo de um comando de repetição é o somatório do custo


de execução de cada iteração conforme o número de repetições
definido, acrescido do custo do teste de término do laço. O
custo de uma iteração é o custo da condição acrescida do custo
do corpo do comando.
• O custo de uma chamada de procedimento é o custo desse
procedimento para um problema cujo tamanho é o definido
por seus parâmetros implı́citos ou explı́citos.

Análise da ordenação por inserção


A seguir, o algoritmo de ordenação de um arranjo de inteiros, in-
dexados de 1 a n, pelo método de inserção da Fig. 1.4 é usado para
ilustrar a aplicação do método de cálculo de custo para o pior caso.
Nesse algoritmo, a instrução destacada como relevante é a de
comparação de chaves x < a[j] , que ocorre na linha (5), e o
tamanho do problema é n, o número de elementos no arranjo.
Algoritmo ——– pior caso ———
1 void ordena(int[ ] a, int n){ - -
2 int i, j, x; - -

for (int i = 2; i < n; i++) {

3  


x = a[i]; j = i-1; a[0] = x;  

4  

 

5 while (x < a[j]) 
 P
n−1
i=2 i

6 a[j+1] = a[j]; j--; i 
}
 

7 
 

 
a[j+1] = x;

 
8  


9 } -
10 } - -
Figura 1.4 Ordenação por Inserção

No comando while da linha (5) da Fig. 1.4, a instrução de com-


paração de chaves x < a[j] , no pior caso, é feita uma vez para
12 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

cada j no intervalo [0, i − 1], portanto o custo dessa comparação


de chaves é i, para i ∈ [2, n − 1].
O corpo do comando for da linha (3) tem o custo da única
operação relevante nele contida, i.e., seu custo é i, pois o custo da
demais operação não precisa ser considerado. Assim, o custo do
comando for é n−1
P
i=2 i.
n
X n[n + 1]
Dado que i= , tem-se que
i=1
2
n−2
X n−2
X n−2
X
Pn−1
i=2 i= (k + 1) = k+ 1 (1.7)
k=1 k=1 k=1
ou
Pn−1 (n − 2)[(n − 2) + 1]
i=2 i= + (n − 2) (1.8)
2
Portanto,
Pn−1 n2 n
i=2 i = − −1 (1.9)
2 2
Assim, o custo no pior caso do algoritmo de ordenação de n
inteiros pelo método da inserção é:
n2 n
f (n) = − −1 (1.10)
2 2
O melhor caso desse algoritmo ocorre quando o arranjo dado já
estiver ordenado, pois a comparação x < a[j] somente será feita
uma vez para cada valor de i, produzindo assim o custo:
n−1
X
f (n) = 1=n−2 (1.11)
i=2

Análise da ordenação por seleção


A tı́tulo de ilustração, no algoritmo a seguir será considerado o
número de comparações de elementos do arranjo a como custo re-
1.3. ANÁLISE DE ALGORITMOS NÃO-RECURSIVOS 13

levante, e, na sequência, considerar-se-á o número de trocas de


posição dos elementos do arranjo como a operação relevante deter-
minante do custo.
Algoritmo ——– pior caso ——–
1 void ordena(int[ ] a, int n){ - -

2 for (i = 1; i <= n-1; i++) { - 



3 min = i; -
 



for (j = i+1; j <= n; j++) {  
P
4
n−1
n−i i=1 (n−i)
5 if (A[j] < A[min]) min = j;  
}


6 


7 x=A[min]; A[min]=A[i]; A[i]=x; - 



8 } -
9 } - -

Figura 1.5 Ordenação por Seleção I

O custo do algoritmo de ordenação por seleção da Fig. 1.5, no


pior caso, quando considera-se somente o custo da comparação da
chave de pesquisa com o elemento do arranjo, é:
n−1
X n(n − 1) n2 n
f (n) = (n − i) = = − (1.12)
i=1
2 2 2
Alternativamente, se número de trocas for considerado como a
medida de custo relevante, tem-se a situação descrita na Fig. 1.6,
que mostra que o programa ordenação por seleção realiza, tanto
no melhor como no pior caso, exatamente n − 1 trocas:
f (n) = n−1
P
i=1 1 = n − 1 (1.13)

Análise da pesquisa binária


Muitas vezes, a metodologia de contabilizar a operação eleita como
relevante não é tão óbvia quanto a empregada nos exemplos apre-
sentados, sendo mais apropriado o uso de abordagens especı́ficas.
14 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Algoritmo ——– pior caso ——–


1 void ordena(int[ ] a, int n){ - -

2 for (i = 1; i <= n-1; i++) { - 



3 min = i; - 



4 for (j = i+1; j <= n; j++) { - 
P
n−1
5 if (A[j] < A[min]) min = j; - i=1 1

}


6  


7 x=A[min]; A[min]=A[i]; A[i]=x; 1




8 } -
9 } - -

Figura 1.6 Ordenação por Seleção II

Para ilustrar o uso de metodologias especiais em casos particu-


lares, considere o cálculo da função de custo do clássico algoritmo
de pesquisa binária, que é uma técnica considerada eficiente para
busca de chaves em tabelas ordenadas.
1 int pesquise(int ch, int[ ] a, int n) {
2 int i, esq, dir, cmp;
3 if (n <= 0) return 0;
4 esq = 1; dir = n;
5 do { i = (esq + dir)/2;
6 cmp = a[i].compareTo(ch);
7 if (cmp == 0) return i;
8 else if (cmp < 0) esq = i + 1;
9 else dir = i - 1;
10 while (esq <= dir);
11 return 0;
12 }

Listagem 1.7 Pesquisa Binária

O algoritmo a ser analisado é o da Listagem 1.7, o qual, para fins


de simplificação, pesquisa um inteiro ch em um arranjo a ordenado
de n inteiros.
1.3. ANÁLISE DE ALGORITMOS NÃO-RECURSIVOS 15

Para fins de cálculo do custo, suponha que n seja uma potência


de 2. Isso facilita os cálculos e não representa perda alguma de
generalidade, pois o que interessa é que n seja um valor grande.
A função de custo do algoritmo é f (n) = k, onde k é o número
de vezes que operação de comparação a[i].compareTo(ch); da linha
(6) é executada no pior caso, que é quando o valor procurado não
se encontra no arranjo.
A atividade central do algoritmo é o comando de repetição
do-while, no qual, a cada iteração, o tamanho da parte da ta-
bela a pesquisar é dividido ao meio.
Assim, inicialmente, quando a primeira comparação é realizada,
o tamanho do arranjo a ser pesquisado é n. Na segunda iteração,
esse tamanho torna-se n/2, e assim sucessivamente, até o elemento
procurado ser encontrado ou o tamanho for reduzido a 1, como
ilustra o quadro da Fig. 1.8.

#teste Tamanho da Tabela


1 n
2 n/2
3 n/4
4 n/8
··· ···
k n/2k−1 = 1

Figura 1.8 Tamanho da Tabela em Análise

Note que, no pior caso, quando o elemento procurado não estiver


no arranjo, a pesquisa somente para quando o tamanho do arranjo
n
a ser pesquisado reduz-se a 1, isto é, quando 2k−1 = 1.
Logo, o número de vezes k que o tamanho da parte do arranjo
a ser pesquisada é dividido ao meio, no pior caso, é lg n + 1.
Portanto, f (n) = lg n + 1
16 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

1.4 Análise de algoritmos recursivos

No caso de procedimento recursivo, deve-se quebrar a circu-


laridade associando ao procedimento uma função de complexidade
f (n) desconhecida, onde n mede o tamanho dos argumentos do
procedimento, e assim gerar as equações de custo recorrentes, cu-
jas soluções fornecem os custos desejados.
O custo da chamada recursiva é então definido em termos dessa
função de complexidade desconhecida.
Assim, o problema da análise de custo de algoritmos recursivos
compreende os seguintes passos:

1. determinar, a partir dos parâmetros do algoritmo, a variável


n que denota o seu tamanho;
2. identificar a operação significativa a ser contada pela função
de custo;
3. associar a cada procedimento recursivo uma função de com-
plexidade f (n) desconhecida, onde n mede o tamanho dos
argumentos do procedimento;
4. postular que se a função de custo para o procedimento com
parâmetros de tamanho n for f (n), então o custo do mesmo
procedimento com parâmetros de tamanho g(n) é f (g(n));
5. obter, a partir do algoritmo, uma equação de recorrência para
f (n);
6. resolver a equação de recorrência para obter a fórmula direta
do custo.

Os principais procedimentos recursivos diretos, i.e., aqueles que


chamam diretamente a si mesmo, podem ser estruturados de acordo
1.4. ANÁLISE DE ALGORITMOS RECURSIVOS 17

com um pequeno grupo de sete esquemas, cada qual gerando um


tipo de equações de recorrência.
Nos esquemas apresentados a seguir, void p(n) representa a
declaração do cabeçalho de um procedimento de nome p que re-
cebe uma lista de parâmetros que caracterizam um problema de
tamanho n, os quais não são explicitados, pois seu conhecimento é
irrelevante para a análise de custo.
Em todos os esquemas apresentados a seguir, a operação re-
levante, determinante do custo, é processa um elemento, que
tem custo 1, ou então processo x elementos, de custo x.

Algoritmos recursivos tipo I


A primeira classe de procedimentos recursivos são aqueles que ter-
minam quando a entrada tem tamanho 0, e, com uma entrada com
tamanho n > 0, processam um elemento e depois chamam-se re-
cursivamente com parâmetros de tamanho n − 1, conforme mostra
Listagem 1.9.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa um elemento; p(n-1);}
4 }

Listagem 1.9 Recursivo Tipo I

Seja f (n) o custo postulado de p(n), definido pelo número de


elementos processados. A operação processa um elemento tem
custo 1, a chamada recursiva p(n-1) tem custo f (n − 1), e o com-
portamento desse algoritmo é descrito pelo sistema de equações:

f (n) = 1 + f (n − 1), para n > 0
f (n) = 0, para n ≤ 0
18 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Para resolver esse sistema de equações, pode-se usar a própria


recorrência
f (n) = 1 + f (n − 1) (1.14)
para calcular o termo f (n − 1) do lado direito, produzindo

f (n − 1) = 1 + f (n − 2)

E novamente usar Eq. (1.14) para calcular

f (n − 2) = 1 + f (n − 3)

E assim, concluir que se assim fosse feito sucessivamente, para um


dado n, chegar-se-ia a

f (1) = 1 + f (0)

Agrupando essas equações, tem-se:



f (n) = 1 + f (n − 1) 


f (n − 1) = 1 + f (n − 2)

n equações
··· = ··· 


f (1) = 1 + f (0) 

Adicionando as equações acima lado a lado:

f (n) = 1+ f(n−1)





f (n − 1) = 1+ f(n−2)
 

 

··· = ···
f
 (1)


= 1 + f (0)
Xn
f (n) = 1 + f (0)
i=1

Portanto, f (n) = n
1.4. ANÁLISE DE ALGORITMOS RECURSIVOS 19

Algoritmos recursivos tipo II


A segunda classe de procedimentos recursivos é formada por aque-
les que, para um problema de tamanho 0, terminam imediata-
mente, e com problemas de tamanho n > 0, processam n elementos
e chamam-se recursivamente com parâmetros de tamanho n − 1,
conforme mostra Listagem 1.10.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa n elementos; P(n - 1);}
4 }

Listagem 1.10 Recursivo Tipo II

Supondo que p(n) tenha custo f (n), o custo desse algoritmo é


descrito pelas equações:

f (n) = n + f (n − 1), para n > 0
f (n) = 0, para n ≤ 0
Usando o método apresentado anteriormente, pode-se gerar as
equações, que são adicionadas lado a lado:

f (n) = n+ f −1)
(n



f (n − 1) = n − 1 + f (n − 2)
  
 
    
  

f (n − 2) = n − 2 + (n − 3)
  
 

    
 
(n
 −3) = n − 3 + 


f(n−4)


k + 1 equações

··· = ··· 



0 

z }| { 
f (n − k) = n − k + f ( n − k − 1)
   
 

X k
f (n) = (n − i) + f (0)
i=0
Para obter a função de custo desejada, basta computar o so-
matório presente na definição de f (n), isto é:
20 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

k
X
f (n) = (n − i) + f (0) e n − k − 1 = 0
i=0

k
X k
X
f (n) = n+ i+0
i=0 i=0

k(k + 1)
f (n) = (k + 1)n + (veja Seção 1.9)
2
n+n−k
f (n) = ( )(k + 1) + 0, onde k = n − 1
2
n+n−n+1
f (n) = ( )(n − 1 + 1)
2
n2 + n
Assim, tem-se a expressão final de custo: f (n) =
2

Algoritmos recursivos tipo III


A terceira classe de procedimentos recursivos é formada pelos que,
com uma entrada de tamanho 0, terminam, e, com entrada de
tamanho n > 0, processam um elemento e depois chamam-se re-
cursivamente com parâmetros de tamanho n/2, conforme Lista-
gem 1.11.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa um elemento; p(n/2);}
4 }

Listagem 1.11 Recursivo Tipo III

Esses algoritmos têm natureza logarı́tmica, pois dividem o espaço


1.4. ANÁLISE DE ALGORITMOS RECURSIVOS 21

de trabalho em duas metades e processam recursivamente um ele-


mento em cada metade até que o espaço tenha tamanho 1.
Para o cálculo de sua complexidade, supõe-se que n seja um
inteiro potência de 2, que p(n) tenha custo f (n), e, assim, o
comportamento desse algoritmo seja descrito pelas equações de
recorrência:

 f (n) = 1 + f (n/2), para n > 1
f (1) = 1
f (n) = 0, para n ≤ 0

Repetindo o processo realizado com o esquema Tipo I, usa-se a


equação de recorrência
f (n) = 1 + f (n/2) (1.15)
para calcular o termo f (n/2) do lado direito
f (n/2) = 1 + f (n/22) (1.16)
E assim, sucessivamente, até que se possa deduzir a última equação,
onde supõe-se que n seja uma potência de 2:

f (n/2k−1) = 1 + f (n/2k ) (1.17)


onde n/2k = 1 e, consequentemente, k = lg n
Agrupando essas equações, tem-se:

f (n) = 1 + f (n/2)  
2 

f (n/2) = 1 + f (n/2 )  

2 3 
f (n/2 ) = 1 + f (n/2 )
k equações
f (n/23) = 1 + f (n/24)  
··· = ···




k−1 k 

f (n/2 ) = 1 + f (n/2 )
22 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Adicionando-as lado a lado:


f (n) = 1+ f(n/2)



2
f
 (n/2)
 = 1+ f(n/2
 )
f  2 )
(n/2 = 1+ f  3 )
(n/2
  

f  3 )
(n/2 = 1+ f  4 )
(n/2
 

··· = ···
k−1
1 + f (n/2k )

f
(n/2
  ) =
Xk
f (n) = 1 + f (n/2k )
|{z}
i=1 1
f (n) = k + f (1), onde k = lg n e f (1) = 1
Assim, obtém-se a expressão final de custo: f (n) = lg n + 1

Algoritmos recursivos tipo IV


A quarta classe de procedimentos recursivos são aqueles que, com
uma entrada de tamanho 0, terminam imediatamente, e, com
entrada com tamanho n > 0, processam n elementos e depois
chamam-se recursivamente com parâmetros de tamanho n/2. ou
seja, dividem o espaço de trabalho em duas metades, e proces-
sam n/2 elementos em uma das metades, conforme mostra Lista-
gem 1.12.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa n elementos; P(n/2);}
4 }

Listagem 1.12 Recursivo Tipo IV

Assim, supondo que n seja um inteiro potência de 2, o custo


f (n) desse algoritmo no pior caso é descrito pelas equações de
recorrência:
1.4. ANÁLISE DE ALGORITMOS RECURSIVOS 23

 f (n) = n + f (n/2), para n > 1
f (1) = 1
f (n) = 0, para n ≤ 0

A solução desse sistema é obtida via uma conveniente mudança de


variável: no lugar de f (n) = n + f (n/2), usa-se f (2k ) = 2k +
f (2k−1), trocando-se n por 2k .
A solução da nova equação de recorrência fica assim:
f (2k ) = 2k + 
f(2k−1 )
 

f(2k−1 ) = 2k−1 + f(2k−2 )


 


f(2k−2 ) = 2k−2 + f(2k−3 )


 


··· = ···
1
f
 (2
 ) = 21 + f (20)
Xk
f (2k ) = 2i + f (1)
i=1

f (2k ) = 2k+1 − 2 + 1
f (2k ) = 2.2k − 1
Portanto, f (n) = 2n − 1

Algoritmos recursivos tipo V


A quinta classe de procedimentos recursivos é formada por aqueles
que, com uma entrada de tamanho 0, terminam imediatamente,
e, com entrada com tamanho n > 0, processam um elemento e
depois chamam-se recursivamente duas vezes com parâmetros de
tamanho n/2, conforme apresentado na Listagem 1.13.
Supondo que p(n) tenha custo f (n), o comportamento desse
algoritmo é descrito pelas equações:
24 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

 f (n) = 1 + 2f (n/2), para n > 1
f (1) = 1
f (n) = 0, para n ≤ 0

1 void p(n) {
2 if (n <= 0) termina;
3 else {processa 1 elemento; P(n/2); P(n/2);}
4 }

Listagem 1.13 Recursivo Tipo V

Supondo que n seja uma potência inteira de 2, a expansão dos


termos da equação de recorrência e subsequente adição desses ter-
mos lado a lado produzem:

f (n) = 1+ 2f(n/2)


 


2f(n/2)



= 2+ 4f(n/4)



4f(n/4) = 4+ 8f(n/8)


 
 


8f(n/8) = 8+ 16f
(n/16)
 
 

··· = ···
k−1((((( k−1
2k−1 + 2k f (n/2k )
(((
2
( ( ( f (n/2 ) =
k−1
X
f (n) = 2i + 2k f (n/2k )
|{z}
i=0 1

Assim,
k−1
X
f (n) = 2i + 2 k
i=0
f (n) = 2 − 1 + 2k , onde 2k = n
k

f (n) = n − 1 + n

Portanto, f (n) = 2n − 1
1.4. ANÁLISE DE ALGORITMOS RECURSIVOS 25

Algoritmos recursivos tipo VI


A sexta classe de procedimentos recursivos é a dos que, com uma
entrada de tamanho 0, terminam, e, com entrada com tamanho
n > 0, processam n elementos e depois chamam-se recursiva-
mente duas vezes com parâmetros de tamanho n/2, conforme Lis-
tagem 1.14.

1 void p(n) {
2 if (n <= 0) termina
3 else {processa n elementos; P(n/2); P(n/2);}
4 }

Listagem 1.14 Recursivo Tipo VI

O custo desse algoritmo é descrito pelas equações:



 f (n) = n + 2f (n/2), para n > 1
f (1) = 1
f (n) = 0, para n ≤ 0

Cuja solução é obtida via uma conveniente mudança de variável:


no lugar de f (n) = n + 2f (n/2), usa-se f (2k ) = 2k + 2f (2k−1),
onde n foi trocado por 2k .
A solução da nova equação de recorrência fica assim:

k k k−1 
f (2 ) = 2 + 2f(2 )
 
 

k−1  k k−2  

2f(2 ) = 2 + 4f(2 )
 
  
 
k−2  k k−3 
4f(2 ) = 2 + 8f(2 ) k equações
     
  

··· = ···




k−1 1 k k 0 
2 f (2 ) = 2 + 2 f (2 )
  
 


X k
k
f (2 ) = 2k + 2k f (1)
i=1
Donde, deduz-se que:
26 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

f (2k ) = k2k − 2k
f (2k ) = (k − 1).2k , onde k = lg n
f (n) = (lg n − 1)2lg n
Portanto, f (n) = n lg n − n

Algoritmos recursivos tipo VII


A sétima classe de procedimentos recursivos compreende aqueles
que, com uma entrada de tamanho 0, terminam imediatamente,
e, com entrada com tamanho n > 0, processam um elemento e
depois chamam-se recursivamente duas vezes com parâmetros de
tamanho n−1, conforme Listagem 1.15.
1 void p(n) {
2 if (n <= 0) termina;
3 else {processa 1 elemento; P(n-1); P(n-1);}
4 }

Listagem 1.15 Recursivo Tipo VII

O custo f (n) do algoritmo pode ser descrito pelas equações:



f (n) = 1 + 2f (n − 1)
f (0) = 0
Cuja solução advém dos cálculos:
−

f (n) = 1+ 2f(n 
1)
− −

 
2f
  (n
 
1) = 2 + 4f
 (n
 
2)
− 2) −

 

4f
  (n

 
= 4 + 8f
 (n
 3)

− −


8f
  (n
  3) = 8 + 16f
 (n 4)
··· (
= ···
(
k−1 ((((((
f (n − (k − 1)) = 2k−1 + 2k f (n − k)
( (
2
((((
k−1
X
f (n) = 2i + 2k f (n − k})
| {z
i=0 0
1.5. COMPLEXIDADE ASSINTÓTICA 27

n−1
X
Assim, f (n) = 2i e, portanto, f (n) = 2n − 1
i=0

Conclusão
Os algoritmos recursivos mais comuns têm um dos sete formatos
apresentados ou alguma variação deles. Evidentemente, outras
estruturas são perfeitamente possı́veis, e a metodologia utilizada
deve ser adaptada para tratar esses casos não previstos.

1.5 Complexidade assintótica

As complexidades de tempo e de espaço são medidas como função


do tamanho do problema ou da entrada do algoritmo. As técnicas
descritas permitem definir o comportamento dos algoritmos à me-
dida que a entrada cresce. Entretanto, toda formulação apresen-
tada é bastante imprecisa, porque muitas aproximações são feitas
durante a análise dos algoritmos, tais como:
• os cálculos são feitos sem que o algoritmo seja executado em
qualquer máquina e, portanto, os custos reais das operação são
ignorados;
• supõe-se que toda instrução tenha o mesmo custo, contabili-
zando apenas sua frequência de execução;
• o custo da maioria das instruções dos algoritmos analisados é
ignorada, sendo contabilizado somente os das operações iden-
tificadas como relevantes;
• instruções relevantes são supostas serem executadas em toda
iteração dos comandos de repetição que as contêm, mesmo
quando ocorrem em comandos condicionais;
28 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

• a análise de custo médio é geralmente muito difı́cil, prevale-


cendo quase sempre a do pior caso.

Quando trabalha-se com problemas de pequeno porte, as de-


cisões de conduta de análise listadas acima podem invalidar to-
talmente o custo apurado. Entretanto, quando o tamanho do pro-
blema assume altas proporções, os comportamentos dos algoritmos
tendem a se diferenciarem, separando-se em classes de complexi-
dade.
O comportamento limite da complexidade quando a entrada
cresce é chamado de complexidade assintótica de tempo ou de
espaço, ou simplesmente, complexidade assintótica, a qual define
o limite de tamanho dos problemas que um dado algoritmo pode
resolver.
Ressalta-se que independentemente do aumento da velocidade
de cálculo dos computadores, o que realmente determina o limite
do tamanho dos problemas que podem ser resolvidos com um dado
algoritmo é a sua complexidade.
Para facilitar o tratamento das aproximações listadas e permitir
a eliminação de detalhes que são irrelevantes para a análise do custo
de algoritmos, usa-se a notação O(g(n)) (leia-se ordem de g(n) ou
ordem de no máximo g(n)), a qual denota uma quantidade que
não é explicitamente conhecida, mas que define com clareza seu
comportamento.
Isto é, a notação O(g(n)) propõe substituir a real expressão da
função de custo, que frequentemente não é conhecida, por uma
regra de comportamento bem definida e mais simples, que permite
fazer comparações dos custos de algoritmos.
Diz-se que um algoritmo tem custo O(g(n)), mas deve-se estar
ciente de que as técnicas de análise de custo de algoritmos apre-
sentadas permitem calcular a função g(n), a qual serve para inferir
1.5. COMPLEXIDADE ASSINTÓTICA 29

o comportamento da real e desconhecida função de custo f (n), e


nunca, sua exata expressão.

Dominação assintótica
Sejam f (n) a real função de custo desconhecida de um algoritmo,
e g(n), sua função de custo computada, considerando n o tamanho
do problema.
Definição 1.1 O(g(n)) denota um conjunto de funções f (n)
tais que |f (n)| ≤ c|g(n)|, para c constante.
A notação O(g(n)) permite definir o chamado comportamento
assintótico de outras funções, como a função de custo f (n), pois,
representa o comportamento do custo quando tamanho n cresce.
Trata-se de definir a taxa de crescimento ou ordem de cres-
cimento da função de custo.
Definição 1.2 f (n) = O(g(n)) ou f (n) ∈ O(g(n)), se ∃ cons-
tantes positivas c e n0 tais que para n ≥ n0, o valor de f (n) é
menor ou igual a cg(n).
Note que a definição 1.2 estabelece que O(g(n)) é um conjunto
de funções, e a notação f (n) = O(g(n)) é apenas uma nova forma
de dizer que f (n) ∈ O(g(n)), portanto, o sinal “=”, nesse contexto,
tem significado distinto do da álgebra.
De acordo com essa definição, o tempo T de execução do algo-
ritmo de custo f (n) = O(g(n)), isto é, f (n) da ordem de g(n),
em uma máquina M e n suficientemente grande, é limitado supe-
riormente por um valor proporcional a g(n), isto é, T ≤ K.g(n),
onde K é uma constante que depende das caracterı́sticas de M.
Definição 1.3 Uma função g(n) domina assintoticamente ou-
tra função f (n), se existem duas constantes n0 e c, tais que
para n ≥ n0, temos |f (n)| ≤ c.|g(n)|.
30 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

O gráfico da Fig. 1.16, onde a curva em vermelho seria a função


de custo real e desconhecida, mostra que g(n) é um limite as-
sintótico superior para f (n).

Figura 1.16 Dominação Assintótica

Para uma melhor compreensão do conceito de dominação as-


sintótica, considere as funções:

f (n) = n − 2
g(n) = n2 − 4

Note que |n − 2| ≤ |c(n2 − 4)| para n ≥ 2 e c ≥ 1/4, portanto,


g(n) domina assintoticamente f (n), i.e., f (n) = O(g(n)).
Por outro lado, f (n) não domina g(n), porque 6 ∃ c > 0 tal que
|n2 − 4| ≤ c|n − 2|, para todo n > 2, i.e., g(n) 6∈ O(f (n))
A dominação assintótica pode ser uma relação unidirecional,
como no exemplo acima, mas pode também ser bidirecional, como
mostra o seguinte exemplo:
f (n) = 10n2


g(n) = 20n2

no qual, para todo n > 1, tem-se que |10n2| ≤ |20n2|, portanto,


g(n) domina assintoticamente f (n), e que |20n2| ≤ 2|10n2|, signi-
ficando que f (n) também domina g(n).
1.5. COMPLEXIDADE ASSINTÓTICA 31

Se as funções de custo f e g dominam assintoticamente uma a


outra, então os algoritmos associados têm custos assintoticamente
equivalentes. Nesses casos, o comportamento assintótico não serve
para compará-los.

Obtenção da ordem
A vantagem do uso de O(f (n)) é que ordem de crescimento per-
mite uma caracterização mais simples da função de custo. A sim-
plificação, sem perda de precisão, da expressão de custo O(f (n))
é amparada pelas seguintes regras:
• se f (n) for uma soma de vários fatores, pode-se descartar todos
exceto o que tiver maior taxa de crescimento.
• se o único termo resultante da simplificação acima contiver
coeficientes constantes, pode-se trocá-los pela constante 1.
Para exemplificar a aplicação dessas regras, considere a expressão
O(5n4 − 10n3 + 4n2 + n + 1000), na qual claramente o termo 5n4 é
o que tem a maior taxa de crescimento. Assim, pelas regras acima,
essa expressão reduz-se a O(n4).
Por essas mesmas regras, o algoritmo de ordenação pelo método
de inserção, cujo custo no pior caso é f (n) = n2/2 − n/2 − 1,
tem uma ordem de crescimento de sua complexidade assintótica
definida por O(n2).
Note que seria um abuso dizer que o tempo de execução do
algoritmo de ordenação por inserção é O(n2), haja vista que tudo
que se sabe é que o tempo real de execução é apenas limitado
superiormente por cn2, para algum c. O que se quer dizer quando
se fala que o tempo de execução é O(n2) é que no pior caso o
tempo de execução é proporcional a n2, para n suficientemente
grande.
32 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Ressalta-se, contudo, que para n pequeno, outros fatores têm


maior influência na comparação de desempenho de algoritmos, nes-
ses casos, outros modelos de apuração de custo devem ser usados.

Expressões de complexidade
No processo de análise da complexidade de um algoritmo, custos
de trechos do algoritmo em análise devem ser combinados para
compor o seu custo final. Nesse processo, expressões com a notação
O devem ser combinadas e avaliadas, segundo as regras próprias,
que estão listadas abaixo, onde, c é uma constante, n denota o
tamanho do problema, f (n), uma função de custo, h(n), outra
função de custo, e t(n, i), uma função intermediária de custo que,
além de n, depende do valor de uma variável i do algoritmo.
As regras básicas de operação com a notação O são:

• f (n) = O(f (n)):


um trecho do algoritmo de custo f (n) pode ser considerado de
custo assintótico O(f (n)).
• f (n) = O(1):
o custo de operações que não dependem do tamanho n do
problema, i.e., quando f (n) = c, tem custo O(1).
• f (n) = O(c + g(n)) então f (n) = O(g(n)):
a combinação de dois trechos de algoritmo de custos c e O(g(n)),
respectivamente, tem custo O(g(n)).
• f (n) = O(g(n))+O(h(n)), então f (n) = O(max(g(n), h(n))):
a combinação de dois trechos de algoritmo de custos O(g(n))
e O(h(n)), respectivamente, tem custo O(max(g(n), h(n))),
onde max é uma função que retorna o termo de maior custo
conforme a ordem de complexidade definida na Fig. 1.20.
1.5. COMPLEXIDADE ASSINTÓTICA 33

• f (n) = O(g(n)) + O(g(n)), então f (n) = O(g(n)):


a combinação de dois trechos de algoritmo de custos individu-
ais O(g(n)) tem o custo combinado O(g(n)) + O(g(n)), que é
o mesmo que O(g(n)).
• f (n) = c.O(g(n)), então f (n) = O(g(n)):
a execução c vezes de um trecho de algoritmo de custo O(g(n))
tem custo final c.O(cg(n)), que reduz-se à O(g(n)).
• f (n) = bi=a(O(t(n, i)), então f (n) = O( bi=a t(n, i)):
P P

a execução repetida i vezes, para i no intervalo [a, b], de um tre-


cho de algoritmo de custo O(t(n, i)) tem custo O( bi=a t(n, i)).
P

• f (n) = O(g(n)).O(h(n)), então f (n) = O(g(n).h(n)):


a execução O(g(n)) vezes de um trecho de custo O(h(n)) tem
custo final O(g(n).h(n)).
• f (n) = g(n).O(h(n)), então f (n) = O(g(n).h(n)):
a execução g(n) vezes de um trecho de custo O(h(n)) tem
custo O(g(n).h(n)).

Essas transformações são muito úteis quando se combinam os


custos de trechos de um algoritmo, e deseja-se manter as expressões
de custo o mais simples possı́vel.

Cálculo da complexidade assintótica


Para ilustrar as operações com a notação O(n), considere o cálculo
do custo do algoritmo de ordenação de um arranjo de n inteiros
pelo método da seleção, mostrado na Fig. 1.17.
A análise usada contabiliza todas as instruções que são execu-
tadas, e supõe-se que n seja o tamanho da entrada de dados e que
toda instrução tenha custo 1.
34 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

1 void ordena(int[ ] a, int n){


2 for (i = 1; i <= n-1; i++) {
3 min = i;
4 for (j = i+1; j <= n; j++) {
5 if (A[j] < A[min])
6 min = j;
7 }
8 x=A[min]; A[min]=A[i]; A[i]=x;
9 }
10 }

Figura 1.17 Ordenação por Seleção I

Inicia-se a análise pelo comando de repetição mais interno, desta-


cado na Fig. 1.18, que contém um comando de decisão que contém
apenas um comando de atribuição, que leva um tempo constante
para ser executado, assim como a avaliação da sua condição.

4 for (j = i+1; j <= n; j++) {


5 if (A[j] < A[min])
6 min = j;
7 }

Figura 1.18 Loop Interno de Seleção I

Como não se sabe se o corpo do comando de decisão será execu-


tado ou não, então deve-se considerar o pior caso, ou seja, assumir
que a linha (6) será sempre executada. Assume-se também que o
tempo para incrementar a variável de controle da repetição e ava-
liar sua condição de terminação seja 1. Assim, o tempo combinado
para executar uma vez o corpo da repetição composta pelas linhas
(5) e (6) é 3, portanto O(3), que é o mesmo que O(1), o qual
simplifica-se para 1.
1.5. COMPLEXIDADE ASSINTÓTICA 35

Como o número de iterações da repetição é n − i, o tempo


total gasto nesse comando interno de repetição é (n − i) × 1 + 1,
considerando o teste final de saı́da. Portanto, o custo do comando
for interno acima é 1+(n−i), que pode ser simplicado para (n−i),
pois O(1 + (n − i)) é mesmo que O(n − i), e (n − i) ∈ O(n − i).

2 for (i = 1; i <= n-1; i++) {


3 min = i;
4,5,6,7 -----------------> custo n-i
8 x = A[min]; A[min] = A[i]; A[i] = x;
9 }

Figura 1.19 Loop Externo de Seleção I

O corpo do for mais externo, destacado na Fig. 1.19, contém o


for interno e os comandos de atribuição nas linhas (3) e (8). Logo,
o tempo de execução das linhas (3) a (9) é:
1 + (n − i) + 1 + 1 + 1 = (n − i) + 4
que pode ser simplificado para (n − i), pois, O((n − i) + 4) é o
mesmo que O(n − i), e (n − i) ∈ O(n − i).
A linha (2), que tem a condição do for mais externo, é execu-
tada n − 1 vezes, e cada iteração do for tem custo n − i, calculado
acima. Assim, o tempo total f (n) para executar o programa é:
n−1
X n(n − 1) n2 n
f (n) = (n − i) = = −
i=1
2 2 2

Portanto, n2 n
f (n) = O( − )
2 2

f (n) = O(n2)
36 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

1.6 Ordens de complexidade

As classes de comportamento assintóticos comumente associadas


a algoritmos são classificadas, conforme apresentado na Fig. 1.20,
pela taxa de crescimento de seus custos. As funções de cresci-
mento mais lento são classificadas como de menor ordem do que
as de crescimento mais rápido. Essa ordenação das expressões de
complexidade provê uma dimensão para comparar algoritmos.
A complexidade de menor ordem é a constante O(1), que não
depende do tamanho n do problema, e a maior delas é a exponen-
cial O(cn), que define o comportamento de problemas intratáveis,
de crescimento muito rápido.

Ordem Complexidade Nome


1 O(1) complexidade constante
2 O(lg n) complexidade logarı́tmica
3 O(n) complexidade linear
4 O(n lg n) complexidade linear logarı́tmica
5 O(n2 ) complexidade quadrática
6 O(n3 ) complexidade cúbica
7 O(2n ) complexidade exponencial
8 O(cn ) complexidade exponencial (c > 2)

Figura 1.20 Ordem de Custos das Classes de Complexidade

Complexidade constante: f (n) = O(1)

Algoritmos de complexidade O(1) são ditos de complexidade cons-


tante. Seu custo de execução independe do tamanho de n, e, assim,
seu tempo de execução f (n) é limitado por uma constante K, cujo
valor é fixado a partir da velocidade do processador, pois as ins-
truções do algoritmo são executadas um número fixo de vezes.
1.6. ORDENS DE COMPLEXIDADE 37

Complexidade logarı́tmica: f (n) = O(lg n)

Um algoritmo de complexidade O(lg n) é dito ter complexidade


logarı́tmica, sendo seu tempo de execução f (n) ≤ K lg n, onde
K é uma constante de tempo definido para cada processador.
Esse tempo de execução ocorre em algoritmos que resolvem um
problema pela sua divisão iterativa em duas partes e buscando a
solução em uma das partes.
Esses algoritmos são muito eficientes, pois é pequena a influência
do tamanho da entrada no custo final. Observe o efeito no custo
do crescimento do tamanho n:
• quando n é 1.000, lg n ≈ 10
• quando n é 32.000, lg n ≈ 16
• quando n é um milhão, lg n ≈ 20

Complexidade linear: f (n) = O(n)

Um algoritmo de complexidade O(n) é dito ter complexidade li-


near. Seu tempo de execução f (n) ≤ K.n, onde K é a constante
de tempo do processador. Em geral, nessa classe de algoritmos,
todos elementos da entrada são processados uma única vez. Note
que cada vez que n dobra de tamanho, o tempo de execução apenas
dobra.

Complexidade linear logarı́tmica f (n) = O(n lg n)

Algoritmos de complexidade O(n lg n) são ditos de complexidade


linear logarı́tmica. Tempos de execução dessa classe ocorrem em al-
goritmos que abordam um problema quebrando-o, recursivamente,
em problemas menores, os quais são resolvidos de forma indepen-
dente, e depois as soluções dos subproblemas são combinadas. Seu
38 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

tempo de execução f (n) ≤ Kn lg n, onde K é a constante do pro-


cessador. Observe que quando n é um milhão, n lg n é cerca de 20
milhões. e quando n é dois milhões, n lg n é cerca de 42 milhões,
apenas um pouco mais do que o dobro.

Complexidade quadrática: f (n) = O(n2)

Um algoritmo de complexidade O(n2) é dito ter complexidade


quadrática. Seu tempo de execução f (n) ≤ Kn2, onde K é a
constante do processador. Algoritmos dessa complexidade execu-
tam as operações mais significativas para o custo dentro de coman-
dos de repetição aninhados em dois nı́veis. Observe que nesse caso,
um custo cresce com o quadrado do tamanho, o que limita algorit-
mos desse tipo à solução de problemas de tamanhos relativamente
pequenos.

Complexidade cúbica: f (n) = O(n3)

Um algoritmo de complexidade O(n3) é dito ter complexidade


cúbica. Seu tempo de execução f (n) ≤ Kn3, onde K é a constante
do processador. Os custos de algoritmos dessa ordem de complexi-
dade crescem muito rapidamente, sendo, assim, úteis apenas para
resolver problemas pequenos. Observe que sempre que n é multi-
plicado por um fator k, o tempo de execução fica multiplicado por
k 3, atingido valores muito altos rapidamente.

Complexidade polinomial: f (n) = O(nd)

Um algoritmo cuja função de complexidade é O(p(n)), tal que


p(n) = di=0 aini, onde a0, a1, a2, · · · , ad são coeficientes e ad > 0,
P

é chamado de algoritmo polinomial no tempo de execução. Como


1.7. COMPLEXIDADE E VELOCIDADE DO PROCESSADOR 39

o termo de maior crescimento adnd sempre prevalece sobre os de-


mais, no lugar de O(p(n)) escreve-se apenas O(nd), para d ≥ 0.
Essa classe de complexidade inclui as ordens de complexidade para
polinômios de grau zero [O(1)], grau um [O(n)], grau dois [O(n2)]
e grau três [O(n3)].
Os algoritmos polinomiais geralmente resultam de um entendi-
mento mais profundo da estrutura do problema, que evita o uso de
força bruta na busca de sua solução. De um ponto de vista teórico,
um problema é considerado intratável se para ele não existe um
algoritmo polinomial capaz de resolvê-lo.
Dito de outra forma, um problema é considerado bem resolvido
quando existe um algoritmo polinomial para resolvê-lo. Ou então
que um algoritmo só pode se considerado eficiente se ele executa
em tempo polinomial.

Complexidade exponencial: f (n) = O(cn)


Um algoritmo de complexidade O(cn), para c ≥ 2 é dito ter com-
plexidade exponencial. Seu tempo de execução f (n) ≤ [Link], onde
K é a constante do processador. Algoritmos dessa ordem de com-
plexidade geralmente não são úteis sob o ponto de vista prático, a
não ser para valores de n muito pequenos. Eles ocorrem na solução
de problemas na qual se usa força bruta ou busca exaustiva
para resolvê-los. Observe que para c = 2, quando n é vinte, o
tempo de execução é cerca de um milhão, e, quando n dobra, o
tempo de execução fica elevado ao quadrado.

1.7 Complexidade e velocidade do processador

Na solução de problemas de grande porte, isto é, problemas cu-


jos tamanhos sejam consideráveis, a classe de complexidade do
40 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

algoritmo usado tem maior impacto no tamanho máximo dos pro-


blemas que podem ser resolvidos em um dado tempo de execução
do que a velocidade do processor utilizado.
Considere, a tı́tulo de argumento, o efeito no tamanho do pro-
blema que pode ser resolvido em um dado tempo por algumas
classes de algoritmos quando se aumenta a velocidade do proces-
sador.
Para esse efeito, sejam KA o fator de custo do processador A,
KB = KA/16, o fator de custo de um processador B, que seja 16 ve-
zes mais rápido, e sejam t, o tempo máximo admitido no execução
algoritmo, nA, o tamanho do problema que um algoritmo conse-
gue resolver no tempo t admitido, quando executado na máquina
A, nB , o tamanho máximo do mesmo problema na máquina B, e
f (n), a função de complexidade de custo do algoritmo.

Caso logaritmo: algoritmo A1 – O(lg n)


Se f (n) = O(lg n), então, no pior caso, têm-se

t = KA. lg nA
t = KB . lg nB
Portanto,
KA. lg nA = KB . lg nB
KA. lg nA = K16A . lg nB
lg nB = 16. lg nA
Donde, tem-se que nB = n16
A

Caso linear: algoritmo A2 – O(n)


Se f (n) = O(n), então, no pior caso, têm-se
1.7. COMPLEXIDADE E VELOCIDADE DO PROCESSADOR 41

t = [Link]
t = KB .nB
Portanto,
[Link] = KB .nB
[Link] = K16A .nB
Donde, tem-se que nB = 16nA

Caso linear logarı́tmica: algoritmo A3 – O(n lg n)

Se f (n) = O(n lg n), então, no pior caso, têm-se



t = [Link]. lg nA
t = KB .nB . lg nB
Portanto,
[Link]. lg nA = KB .nB . lg nB
[Link]. lg nA = K16A .nB . lg nB
nB . lg nB = (16 lg nA).nA
lg nA
nB = 16( ).nA
lg nB
Considerando que nB é ligeiramente maior que nA, haja vista os
resultados de algoritmos com f (n) = O(n), e que a curva dos lo-
garitmos é de lento crescimento, pode-se concluir que nB ≈ 16nA .

Caso quadrática: algoritmo A4 – O(n2)

Se f (n) = O(n2), então, no pior caso, têm-se

t = KA.n2A


t = KB .n2B
42 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Portanto,

KA.n2A = KB .n2B
KA.n2A = K16A .n2B
n2B = 16.n2A
Donde, tem-se que nB = 4nA

Caso cúbica: algoritmo A5 – O(n3)


Sef (n) = O(n3), então, no pior caso, têm-se
t = KA.n3A
t = KB .n3B
Portanto,
KA.n3A = KB .n3B
KA.n3A = K16A .n3B
n3B = 16.n3A
Donde tem-se que nB = 2, 51nA

Caso exponencial: algoritmo A6 – O(2n)


Se f (n) = O(2n), então, no pior caso, têm-se

t = KA.2nA


t = KB .2nB
Portanto,
KA.2nA = KB .2nB
KA.2nA = K16A .2nB
2n B = 16.2nA
2n B = 24+nA
Donde tem-se que nB = nA + 4
1.8. OUTROS LIMITES ASSINTÓTICOS 43

Resumo
Tabela 1.21 resume a influência do velocidade processador no ta-
manho do problema que um dado algoritmo pode resolver, quando
fixado o tempo máximo de execução. Claramente, vê-se a comple-
xidade é o fator determinante do tamanho máximo do problema
que se pode resolver.
Tamanho máximo Tamanho máximo
Complexidade para uma dada para velocidade
Algoritmo de Tempo Velocidade 16 vezes maior
A1 lg n N N 16
A2 n N 16N
A3 n lg n N ≈ 16N
A4 n2 N 4N
3
A5 n N 2, 51N
A6 2n N N +4

Figura 1.21 Influência da Velocidade do Processador

1.8 Outros limites assintóticos

Além da popular notação-O, há outras que provêem informações


adicionais sobre o comportamento assintótico de algoritmos. As
principais são Θ, Ω, o e ω.

Notação Θ
Diz-se que f (n) = Θ(g(n)), que se lê f (n) é da ordem exata de
g(n), se existirem constantes positivas c1, c2 e n0 tais que, para
n ≥ n0, o valor de f (n) estiver sempre entre c1g(n) e c2g(n).
Se f (n) = Θ(g(n)), a função g(n) é um limite assintótico firme
ou restrito (asymptotically tight bound) para f (n), pois, g(n)
limita f (n) acima e abaixo.
44 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Assim, f (n) = Θ(g(n)) significa que f (n) cresce assintotica-


mente tão rapidamente quanto g(n), como ilustra a Fig. 1.22.
Enquanto a notação-O trata do limite superior de funções de
custo no pior caso, a notação-Θ está ligada aos limites inferior e
superior do custo médio.

Figura 1.22 Limite Assintótico Firme

Formalmente, define-se a notação Θ como:


Definição 1.4 f (n) = Θ(g(n)), ∃ c1 > 0, c2 > 0 e n0 ≥ 0 ,
tais que 0 ≤ c1g(n) ≤ f (n) ≤ c2g(n), ∀ n ≥ n0.

Notação o
A notação o é definida como:
Definição 1.5 f (n) = o(g(n)), ∀ c > 0 e n0 ≥ 0, tais que
0 ≤ f (n) < cg(n), ∀ n ≥ n0.
Intuitivamente, pela Definição 1.5, a função f (n) tem um cres-
cimento muito menor que g(n) quando n tende para infinito. Isto
pode ser expresso da seguinte forma:
f (n)
lim =0
n→∞ g(n)

que pode ser vista como uma definição alternativa da notação o.


1.8. OUTROS LIMITES ASSINTÓTICOS 45

Embora as definições das notações O (O grande) e o (o pequeno)


sejam similares, há uma diferença expressa no fato de a expressão
0 ≤ f (n) ≤ cg(n) ser válida para todas as constantes c > 0 no
caso de f (n) = o(g(n)), e, no caso de O, requer-se apenas que
exista uma constante c que garanta a validade dessa expressão.
As notações O e o definem limites assintóticos que não são neces-
sariamente firmes, pois ambas não especificam limites inferiores.

Notação Ω
Diz-se que f (n) = Ω(g(n)), se existirem constantes positivas c e n0
tais que para n ≥ n0, o valor de f (n) é maior ou igual a cg(n). Por-
tanto, g(n) é um limite assintótico inferior (asymptotically lower
bound) para f (n), como sugere a Fig. 1.23. Formalmente, tem-se:

Definição 1.6 f (n) = Ω(g(n)), se ∃ constantes c > 0 e n0 > 0,


tais que 0 ≤ cg(n) ≤ f (n), ∀ n ≥ n0.
Observe que quando a notação Ω é usada para expressar o tempo
de execução de um algoritmo no melhor caso, define-se também o
limite inferior do tempo de execução para todas as entradas.

Figura 1.23 Limite Assintótico Inferior

Quando se fala que o tempo de execução é Ω(g(n)) quer-se


46 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

dizer que o tempo de execução desse algoritmo é de ordem de no


mı́nimo uma constante vezes g(n) para grandes valores de n. Por
exemplo, o algoritmo de ordenação por inserção é Ω(n) no melhor
caso, então, seu tempo de execução é no mı́nimo Ω(n).

Notação ω
Por analogia, a notação ω está relacionada com a notação Ω da
mesma forma que a notação o está relacionada com a notação O.
Formalmente, a notação ω é definida da seguinte forma:
Definição 1.7 f (n) = ω(g(n)), se ∀ constante c > 0,
∃ n0 > 0 tal que 0 ≤ cg(n) < f (n), ∀ n ≥ n0
Embora as definições das notações Ω e ω sejam similares, há uma
diferença importante que é que, em f (n) = ω(g(n)), a expressão
0 ≤ cg(n) < f (n) é válida para todas as constantes c > 0.
A relação f (n) = ω(g(n)) implica em
f (n)
lim = ∞,
n→∞ g(n)

se o limite existir.

Relação entre O, o, Ω, Θ, ω
Com base nas definições apresentadas, pode-se dizer que:
• f (n) = O(g(n)) significa que f (n) cresce assintoticamente
mais lentamente que c.g(n), para alguma constante c
• f (n) = o(g(n)) significa que f (n) cresce assintoticamente mais
lentamente que c.g(n), para toda constante c
• f (n) = Θ(g(n)) significa que f (n) cresce assintoticamente tão
rapidamente quanto g(n)
1.8. OUTROS LIMITES ASSINTÓTICOS 47

• f (n) = Ω(g(n)) significa que f (n) cresce assintoticamente


mais rapidamente que c.g(n), para alguma constante c
• f (n) = ω(g(n)) significa que f (n) cresce assintoticamente
mais rapidamente que c.g(n), para toda constante c
Os comportamentos descritos pelas notações Θ, Ω e O(n) estão
relacionados conforme o Teorema 1.1.
Teorema 1.1 f (n) = Θ(g(n)) se e somente se, para duas
funções quaisquer f (n) e g(n), f (n) = O(g(n)) e f (n) = Ω(g(n)).
A aplicação das notações de complexidade nas expressões de
custo dos algoritmos de ordenação analisados permite afirmar que:
• para o algoritmo de ordenação por inserção, o melhor caso tem
custo Ω(n), o pior caso O(n2), e o custo do caso médio é de
difı́cil cálculo, e, com certeza, não é Θ(n2).
• para o algoritmo de ordenação por seleção, o melhor caso tem
custo Ω(n2), o pior caso, O(n2), e o caso médio, Θ(n2).

Operações com Aproximações


Durante o processo de determinação do custos de algoritmos, as
notações de complexidade assintótica ajudam a simplificar os cálcu-
los, mas é preciso cuidado ao operar as expressões de custos obtidas,
haja vista que o sı́mbolo “=”, que ocorrem em f (n) = Θ(g(n)),
f (n) = O(g(n)), f (n) = Ω(g(n)), f (n) = o(g(n)) e f (n) =
ω(g(n)), tem um significado especial de pertinência a conjunto, que
não é seu significado usual na álgebra tradicional. Por exemplo, se
dado que f (n) = O(g(n)), não faz sentido escrever a comutação
O(g(n)) = f (n).
Entretanto, algumas das operações tradicionais da igualdade,
como transitividade, reflexidade e simetria, são preservadas.
48 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

Transitividade
1. f (n) = Θ(g(n)) ∧ g(n) = Θ(h(n)) ⇒ f (n) = Θ(h(n))
2. f (n) = O(g(n)) ∧ g(n) = O(h(n)) ⇒ f (n) = O(h(n))
3. f (n) = Ω(g(n)) ∧ g(n) = Ω(h(n)) ⇒ f (n) = Ω(h(n))
4. f (n) = o(g(n)) ∧ g(n) = o(h(n)) ⇒ f (n) = o(h(n))
5. f (n) = ω(g(n)) ∧ g(n) = ω(h(n)) ⇒ f (n) = ω(h(n))

Reflexividade
1. f (n) = Θ(f (n))
2. f (n) = O(f (n))
3. f (n) = Ω(f (n))

Simetria
1. f (n) = Θ(g(n)) se e somente se g(n) = Θ(f (n))

Simetria de Transposição
1. f (n) = O(g(n)) se e somente se g(n) = Ω(f (n))
2. f (n) = o(g(n)) se e somente se g(n) = ω(f (n))

1.9 Fórmulas importantes

Os seguintes somatórios e produtos são frequentemente encontra-


dos no processo de cálculo da função de custo de comandos de re-
petição de muitos algoritmos. Para facilitar sua resolução, apresen-
tamos suas fórmulas de avaliação, nas quais n, a, b, c, i, aj ∀j ≥ 1,
k, x e q são valores inteiros positivos, x, y, z são números reais
positivos, e lg, logaritmo na base 2.
1.9. FÓRMULAS IMPORTANTES 49

Logaritmos
• logb(xy) = logb x + logb y

• logb(x/y) = logb x − logb y

• logb(xy ) = y logb x

• logb x = (logc x)(logb c)

• y logc x = xlogc y

• blogb x = x

Produtos
• Produto dos n termos a1, a2, · · · , an de uma Progressão Geomé-
trica de razão q:
√ n+1 ×n
Pn = an1 × ann ou Pn = an+1 1 × q 2

Somatórios
• Soma dos n termos a1, a2, · · · , an de uma Progressão Aritmética:
n
X a1 + an
ai = ×n
i=1
2
• Soma dos n termos a1, a2, · · · , an de uma Progressão Geométrica
de razão q:
n
X a1(q n − 1)
ai = (q > 1)
i=1
q − 1
50 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

n−1
X n(n − 1)
• (n − i) =
i=1
2
n
X n(n − 1)
• (n − i) =
i=1
2
n
X n(n + 1)
• i=
i=1
2
n
X n(n + 1)(2n + 1)
• i2 =
i=1
6
n
X
k nk+1
• i = + termo de menor ordem
i=1
k+1
n
X
i 1 − xn+1
• ax = a × (x > 1)
i=0
1−x
n
X 1 1
• = 2 −
i=0
2i 2n
n
X
i an+1 − 1
• a = (a > 1)
i=0
a−1
n
X
• 2i = 2n+1 − 1
i=0
n−1
X i n+1
• = 2 −
i=0
2i 2n−1
n
X i 2n − 1
• i
= 2 − n−1
i=1
2 2
1.10. CONCLUSÃO 51

n−1
X
• i2i = 2 + (n − 2)2n
i=0
n
X
i x − nxn + (n − 1)xn+1
• ix = (x > 1)
i=0
(1 − x)2
n
X
• lg i = lg n!
i=1

1.10 Conclusão

Neste capı́tulo, as principais técnicas para análise e avaliação do


custo de algoritmos e determinação de sua complexidade assintótica
estão detalhadas de forma simples e intuitivas.
O domı́nio dessas técnicas é fundamental para orientar a for-
mulação de algoritmos mais eficientes e para realizar de forma ob-
jetiva a avaliação e seleção de algoritmos em diversas disciplinas
da Engenharia de Software.

Exercı́cios

1. Explique o significado das seguintes expressões:

(a) f (n) é O(1)


(b) f (n) é O(n)
(c) f (n) é O(n log n)

2. Sejam f1(n) e f2(n) as funções de custo dos algoritmos A1 e


A2, respectivamente. Qual é o significado de cada uma das
seguintes afirmativas:
52 CAPÍTULO 1. ANÁLISE DE COMPLEXIDADE

a) o tempo de execução de A1 é O(n log n) e o tempo de


execução de A2 é O(n3).
b) o tempo de execução de A1 é aproximadamente n log n e o
tempo de execução de A2 é O(n log n).
3. Seja o problema de determinar todas as soluções da equação
x2 + y 2 = z 2 + w2 em um intervalo de 1 a n.

1 private static Hashtable solucao1(int n) {


2 Hashtable solucoes = new Hashtable();
3 for (int x = 1; x <= n; x++) {
4 for (int y = 1; y <= n; y++) {
5 Long r = new Long(x*x + y*y);
6 if (![Link](r)) [Link](r,new Vector());
7 Vector v = ((Vector) [Link](r));
8 [Link](new Par(x,y));
9 }
10 }
11 return solucoes;
12 }
13
14 private static Hashtable solucao2(int n) {
15 Hashtable solucoes = new Hashtable();
16 for (int x = 1; x <= n; x++) {
17 for (int y = x; y <= n; y++) {
18 Long r = new Long(x*x + y*y);
19 if (![Link](r)) [Link](r,new Vector());
20 Vector v = ((Vector) [Link](r));
21 [Link](new Par(x,y));
22 if (x != y) [Link](new Par(y,x));
23 }
24 }
25 return solucoes;
26 }

Figura 1.24 Soluções de x2 + y 2 = z 2 + w2

Fig. 1.24 apresenta duas soluções: o método solucao1(int)


e o método solucao2(int), e Fig. 1.25 mostra o resultado da
execução dos métodos solucao1(int) e solucao2(int).
1.10. CONCLUSÃO 53

Uma solução da equação é escolher quaisquer dois pares que


estejam em um vetor de uma determinada chave da Tabela
Hash e atribuı́-los às variáveis x, y, z e w.

Figura 1.25 Exemplo de resultado dos métodos

Para a implementação dos métodos acima:

(a) calcule a função de complexidade de tempo para o método


solucao1(int)
(b) calcule a função de complexidade de tempo para o método
solucao2(int)
(c) calcule a ordem de complexidade assintótica do método
solucao1(int)
(d) calcule a ordem de complexidade assintótica do método
solucao2(int)
(e) qual é o método mais eficiente? Por quê?

Observação: considere apenas como operação relevante o cálculo


de x*x + y*y. As operações sobre Hashtable e Vector po-
dem ser desconsideradas.
54 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

Notas bibliográficas

A tradicional referência bibliográfica sobre complexidade de al-


goritmos é a série clássica de livros de Donald Knuth, The Art
of Computer Programming [6, 7], que organizou o conhecimento
dessa área e formalizou a notação que é usada desde então.
O clássico The Design and Analysis of Computer Algorithms
[2] de autoria de Alfred Aho, John Hopcroft e Jeffrey D. Ullman
é uma obra prima que aborda a análise de algoritmos de forma
bastante completa e profunda.
O livro Algoritmos [22] de Thomas H. Cormen et al. apresenta
uma abordagem moderna e bastante extensa, sendo de leitura obri-
gatória de especialistas.
Os livros Projeto de Algoritmos com Implementações em
Pascal, C, Java e C++ [14, 15] de Nivio Ziviani oferecem uma
introdução do tema.
***
Capı́tulo 2

Tipos Abstratos de Dados

Ne sutor supra crepı̌dam1


Apeles (séc IV a.C)

Uma atividade essencial da arte de escrever programas consiste


em representar na linguagem dos computadores os objetos do pro-
blema real a ser resolvido e codificar as operações aplicáveis a esses
objetos. Esse processo compreende pelo menos os seguintes passos:
• definir estruturas de dados para representar no computador as
entidades do mundo real;
• definir as transformações das informações do mundo real em
termos de sua representação definida no computador, i.e., es-
pecificar os algoritmos das operações a ser aplicadas sobre essas
representações;
• codificar esses algoritmos para manipular a representação das
informações para se atingir o efeito desejado em relação ao
mundo real.

A complexidade desse processo depende da distância conceitual


entre os objetos do mundo real e os de suas respectivas repre-
sentações dentro do programa.
1
“O sapateiro não deve subir além das sandálias”, disse o pintor grego Apeles a um sapateiro que sugeriu
mudanças na pintura da túnica de um centurião em um de seus quadros.

55
56 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

2.1 Lacuna semântica

Chama-se lacuna semântica ou gap semântico a distância concei-


tual entre os objetos do mundo real e os disponı́veis na linguagem
de programação.
A atividade de programação é mais fácil quando a lacuna semân-
tica for pequena, melhor ainda, quando houver uma correspondên-
cia um-a-um entre os objetos do mundo real e os da computação.
Por exemplo, se os valores numéricos manipulados em um pro-
blema do mundo real são diretamente implementáveis por variáveis
do tipo int da linguagem Java a representação desses valores no
programa é trivial.
Por outro lado, quanto mais dissociados forem os objetos do
mundo real dos diretamente disponı́veis na linguagem de programa-
ção em uso, mais difı́cil ou trabalhosa é a programação, pois dever-
se-á modelar esses objetos do mundo real por meio da combinação
de objetos internos do programa.
Para ilustrar o impacto do gap semântico na legibilidade de pro-
gramas, considere a escrita de um programa supostamente trivial
para ler dois valores inteiros do intervalo [-32.768, 32.767],
somá-los e imprimir o resultado. Na linguagem Java, a solução é
muito simples, como mostra o programa da Listagem 2.1.

1 public static void main(String [ ] arg) {


2 int x, y, t;
3 x = [Link](); // leitura de um inteiro do teclado
4 y = [Link]();
5 t = x + y;
6 [Link](t);
7 }

Listagem 2.1 Solução em Java


2.1. LACUNA SEMÂNTICA 57

Entretanto, para destacar a questão da lacuna semântica, supo-


nha que a linguagem de programação a ser usada não possua o tipo
int nem arranjos, disponibizando apenas o tipo de dado boolean,
dotado das tradicionais operações and (&&), or (||) e not (!).
Assim, cada inteiro do problema proposto pode ser representado
por 16 booleanos em forma de complemento 2. E para facilitar a
comparação dessa nova solução com a solução Java da Fig. 2.1, o
programa a seguir usa a sintaxe e semântica de Java.

1 public static void main(String [ ] arg) {


2 boolean x0,x1,x2,x3,x4,...,x9,x10,x11,x12,x13,x14,x15;
3 boleaan y0,y1,y2,y3,y4,...,y9,y10,y11,y12,y13,y14,y15;
4 boolean t0,t1,t2,t3,t4,...,t9,t10,t11,t12,t13,t14,t15;
5 boolean c = false;
6

7 "Leia x0,x1,x2,x3,x4,...,x9,x10,x11,x12,x13,x14,x15";
8 "Leia y0,y1,y2,y3,y4,...,y9,y10,y11,y12,y13,y14,y15";
9

10 t15 = (!x15) && ( !y15) && (c) ||


11 (!x15) && (y15) && ( !c) ||
12 (x15) && ( !y15) && ( !c) ||
13 (x15) && (y15) && (c);
14 c = (x15 && y15) || (x15 && c) || (y15 && c);
15

16 t14 = (!x14) && ( !y14) && (c) ||


17 (!x14) && (y14) && ( !c) ||
18 (x14) && ( !y14) && ( !c) ||
19 (x14) && (y14) && (c);
20 c = (x14 && y14) || (x14 && c) || (y14 && c);
21 .......................................................
22 t11 = (!x11) && ( !y11) && (c) ||
23 (!x11) && (y11) && ( !c) ||
24 (x11) && ( !y11) && ( !c) ||
25 (x11) && (y11) && (c);
26 c = (x11 && y11) || (x11 && c) || (y13 && c);
58 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

27

28 t10 = (!x10) && ( !y10) && (c) ||


29 (!x10) && (y10) && ( !c) ||
30 (x10) && ( !y10) && ( !c) ||
31 (x10) && (y10) && (c);
32 c = (x10 && y10) || (x10 && c) || (y10 && c);
33

34 t9 = (!x9) && ( !y9) && (c) ||


35 (!x9) && (y9) && ( !c) ||
36 (x9) && ( !y9) && ( !c) ||
37 (x9) && (y9) && (c);
38 c = (x9 && y9) || (x9 && c) || (y9 && c);
39 ...................................................
40 t1 = (!x1) && ( !y1) && (c) ||
41 (!x1) && (y1) && ( !c) ||
42 (x1) && ( !y1) && ( !c) ||
43 (x1) && (y1) && (c);
44 c = (x1 && y1) || (x1 && c) || (y1 && c);
45

46 t0 = (!x0) && ( !y0) && (c) ||


47 (!x0) && (y0) && ( !c) ||
48 (x0) && ( !y0) && ( !c) ||
49 (x0) && (y0) && (c);
50

51 "Imprima t0,t1,t2,t3,t4,t5,t6,...,t12,t13,t14,t15";
52 }

Esse exemplo mostra como a falta do tipo de dado apropri-


ado complica a programação e dificulta a leitura do programa. E
isso vai sempre ocorrer, pois há mais tipos de objetos no mundo
real do que tipos primitivos prontos em sua linguagem de pro-
gramação. Portanto, a modelagem de objetos do mundo real em
tipos disponı́veis na linguagem de programação é inevitável e pode
ser muitas vezes complexa.
Para minorar esse problema, é necessário recursos e técnicas
para encapsular e ocultar a complexidade de forma a tornar os
2.2. MODULARIDADE 59

programas modulares, haja vista que modularização é reconheci-


damente o elemento central da arte de escrever programas de alta
qualidade e é, provavelmente, o recurso mais poderoso disponı́vel
para se dominar a complexidade de grandes sistemas.

2.2 Modularidade

Modularidade é a chave para o desenvolvimento de software com


arquitetura flexı́vel. Ela permite identificar com clareza as par-
tes que compõem um programa, facilitando sua extensibilidade
e reusabilidade, que são atributos importantes para a produção
de programas adaptáveis a novas situações, geradas pelo apareci-
mento de novos requisitos. Essa facilidade é indispensável, pois
todo programa, mais cedo ou tarde, precisa ser adaptado devido a
inevitáveis mudanças do mundo real.
Como foi dito acima, módulo é a principal ferramenta disponı́vel
para quebrar a complexidade do software e reduzir seu custo. Define-
se módulo como um componente de software compilável separada-
mente, no sentido que ele não depende do contexto no qual está
inserido. Do ponto de vista linguı́stico, módulos são a realização
das abstrações identificadas no programa.
Uma abstração é um exercı́cio mental que nasce da percepção de
similaridades entre objetos e da decisão de se focar a atenção nessas
similaridades e de ignorar diferenças. Uma abstração ressalta as
caracterı́sticas de um objeto que o distinguem de outros tipos de
objetos e provê, segundo o ponto de vista do observador, limites
conceituais claros e bem definidos. A abstração dá a visão externa
do objeto e separa seu comportamento de sua implementação.
Abstração é um modo de pensar no qual concentra-se na idéia e
não nas manifestações especı́ficas dessa idéia. E abstrair-se significa
60 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

concentrar-se no que o programa faz e não em como ele faz, sendo


o elemento básico de construção de módulos.
O resultado desse processo depende dos objetivos de quem se
abstrai. Por exemplo, a abstração da entidade Pessoa depende de
seu uso no sistema: uma Pessoa sob a perspectiva de um médico é
uma entidade dotada de pulmão, coração, músculos, cérebro, etc, e,
para um empregador, uma Pessoa é caracterizada por nome, cpf,
competência técnica, especialidade, endereço, entre outros atribu-
tos ou funções.
Vinculado ao conceito de abstração tem-se a noção de encap-
sulação, que é o processo de compartimentalizar os elementos de
uma abstração, reunindo em um mesmo local estruturas de dados e
operações associadas. Esse processo serve para separar a interface
contratual de uma abstração de sua implementação.
Em resumo, encapsulação é um mecanismo que direciona o pro-
cesso de esconder os dados, estruturas internas e detalhes de im-
plementação de um objeto, forçando que toda interação seja via
operações da sua interface pública. Assim, encapsulação dá origem
a módulos, os quais devem ser sempre opacos e nunca deixar seus
detalhes de implementação diretamente acessı́veis em outras partes
do programa.
Uma boa linguagem de programação deve prover recursos lin-
guı́sticos para que cada tipo de abstração realizável corresponda a
um tipo de módulo. Especificamente, deve haver um mecanismo
de abstração para cada construção semanticamente relevante na
linguagem, e cada tipo de abstração deve ser implementado como
um módulo compilável separadamente.
Nesse sentido, em Java, a realização ou implementação de abs-
tração de expressões dá-se por meio de declaração de funções; a
de comandos pode ser encapsulada em métodos, e a de um con-
2.3. NOÇÃO DE ABSTRAÇÃO DE DADOS 61

junto de declarações e métodos pode ser realizada por classes, que


constituem novos tipos de dados.
Genericamente, os principais tipos de módulos realizáveis em
uma linguagem de programação imperativa são os seguintes:
• Função: conjunto encapsulado de declarações e comandos que
calcula um valor e possivelmente causa um efeito colateral no
ambiente.
• Procedimento: conjunto encapsulado de declarações e coman-
dos que deve causar um efeito colateral no ambiente.
• Abstração de dados: uma instância de um conjunto de es-
trutura de dados definida pelas suas operações e com repre-
sentação encapsulada.
• Tipo abstrato de dados: um molde que reúne um conjunto
de estruturas de dados definido pelas suas operações e com
representação encapsulada, e que pode ser usado para criar
abstrações de dados.
• Tipo abstrato de dados parametrizado: um tipo abstrato de
dados genérico obtido pela representação parametrizada e en-
capsulada de suas estruturas de dados.

O uso cuidadoso desses recursos permite quebrar a complexidade


de grandes sistemas com a estruturação de programas modulares
e legı́veis, os quais podem ser mais facilmente estendidos ou modi-
ficados em sua vida útil.

2.3 Noção de abstração de dados

O exemplo apresentado a seguir mostra o processo de modula-


rização do programa que implementa o somador da Seção 2.1 em
uma linguagem de programação que não disponibiliza o tipo int
de 16 bits, mas permita ao programador definir novos tipos, por
62 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

meio do conceito de classes, como se faz em Java. Cada inteiro


nesse exemplo é representado por uma lista de 16 booleanos, sendo
a aritmética binária em complemento de 2 completamente encapsu-
lada e abstraı́da dentro da classe MeuInt definida na Listagem 2.2.

1 class MeuInt {
2 private boolean a0, a1, ..., a15;
3 public MeuInt( ) { ... }
4 public MeuInt(boolean a0,...,boolean a15){...}
5 public void leia() { ... }
6 public void imprima() { ... }
7 public MeuInt some(MeuInt x, MeuInt y){
8 "calcule t0, ..., t15 a partir de x e y";
9 return new MeuInt(t0,...,t15);
10 }
11 }

Listagem 2.2 TAD MeuInt

E a classe Somador, da Listagem 2.3, ilustra o uso de MeuInt.

1 public class Somador {


2 public static void main(String [ ] arg) {
3 MeuInt x = new MeuInt( );
4 MeuInt y = new MeuInt( );
5 MeuInt t = new MeuInt( );
6 [Link](); [Link]();
7 t = some(x,y);
8 [Link]( );
9 }
10 }

Listagem 2.3 Usuário de MeuInt


2.4. TIPO ABSTRATO DE DADOS 63

2.4 Tipo abstrato de dados

O mecanismo de encapsulação e abstração da complexidade de


detalhes de implementação, como o apresentado na seção anterior,
é parte do conceito de Tipos Abstratos de Dados.
Objetos, sejam da vida real ou virtuais dentro em um programa
de computador, são caracterizados pelas operações que se podem
realizar com eles. A estrutura interna ou constituição dos objetos
não têm relevância em sua identificação, no sentido de que pode-se,
livremente, substituir um objeto por outro que realize as mesmas
operações e produza resultados idênticos, embora cada objeto possa
ter estruturas internas distintas.
Por exemplo, o que identifica um caixa de banco são as operações
bancárias que ele é capaz de realizar, não importando suas carac-
terı́sticas fı́sicas ou sociais.
Da mesma forma, tipos de dados em uma linguagem de pro-
gramação são caracterizados pelas operações a eles associadas e
pelo conjunto de valores que podem ser atribuı́dos a uma variável
do tipo. Se as constantes de um tipo forem consideradas operações
de zero argumentos, pode-se dizer que todo tipo é caracterizado
integralmente pelas suas operações.
Reforçando a ideia de que as operações são suficientes para ca-
racterização e uso de um tipo, a representação dos valores de um
tipo de dados devem ficar fora do alcance dos usuários do tipo,
isto é, a representação dos objetos de um tipo somente deve ser
acessı́vel às operações definidas no tipo, e nunca diretamente por
parte do usuário. Somente a interface das operações deve ser co-
nhecida externamente.
Essa separação de interesses é o cerne do conceito de abstração,
o qual preconiza que somente as informações de fato relevantes
devem estar visı́veis a cada instante.
64 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

Quando os tipos de dados criados pelo programador são tratados


abstraindo-se totalmente de sua representação, sua implementação
denomina-se Tipo Abstrato de Dados ou TAD, o qual é de-
finido como uma estrutura de programa que encapsula uma deter-
minada representação de dados e a torna conhecida somente pelas
operações que se podem realizar sobre seus elementos, sem que se
indiquem como a estrutura de dados e as operações são codificadas.
A estrutura de programa mais adequada à implementação de
tipos abstratos de dados varia de linguagem para linguagem, mas
invariavelmente ela deve prover pelo menos recursos para controle
de visibilidade, encapsulação e ocultação de informação.
No caso de Java, esse recurso é a classe, que reúne todos os
requisitos para criar novos tipos abstratos de dados.
Ressalva-se, contudo, que toda classe em Java sempre imple-
menta um novo tipo de dado, mas nem sempre o que é implemen-
tado é um tipo abstrato. Para se assegurar que de fato uma classe
implementa um TAD, deve-se observar as seguintes recomendações:

• Encapsule, dentro da classe, a representação dos valores de


instâncias do tipo e de suas operações, e exerça o devido con-
trole de acesso a cada um dos elementos dessa representação.
• Use o mecanismo de controle de visibilidade para definir cla-
ramente a interface da classe.
• Defina como privado todos os atributos definidos na classe.
• Defina como público somente as operações (métodos) que im-
plementam as operações do TAD.
• Não use informação no corpo da classe por ventura disponı́vel
no contexto em que ela é definida.
• Assegure-se que a classe implementada é um módulo com-
pilável separadamente, isto é, ela não depende do contexto
no qual está inserida.
2.5. IMPLEMENTAÇÕES DE TIPOS 65

2.5 Implementações de tipos

Nesta seção, são apresentados exemplos de implementação de ti-


pos abstratos de dados, nos quais, com o objetivo de ilustrar a
importância de se encapsular e controlar a visibilidade dos compo-
nentes do TAD, muitos dos recursos disponı́veis em Java não foram
usados. A ideia é mostrar o poder de encapsulação e de abstração
oferecido pelo conceito de classes de Java.
Inicialmente, considere a implementação de um tipo de dado
definido pelas operações de criar uma lista vazia, retirar-lhe um
elemento e nela inserir um novo elemento.
O código da Listagem 2.4 implementa o tipo ListaDeInteiros1,
que é de fato um tipo abstrato, pois a classe encapsula devidamente
os constituintes do tipo e identifica claramente sua interface for-
mada pelas operações especificadas acima.
1 class ListaDeInteiros1{
2 private int[] elementos;
3 public ListaDeInteiros1(int size) {
4 elementos = new int[size];
5 }
6 public void insere(int x) { ... }
7 public int retira() { ...}
8 }

Listagem 2.4 TAD ListaDeInteiros1

Tipo ficha de dados


A classe ListaDeInteiros2 da Listagem 2.5, diferentemente da
classe ListaDeInteiros1, apenas cria um novo tipo que não é um
tipo abstrato, pois essa classe não exerce o devido controle de visi-
bilidade sobre a representação encapsulada dos objetos declarados
66 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

com o tipo criado.


1 class ListaDeInteiros2{
2 public int[] elementos;
3 public ListaDeInteiros2(int size) {
4 elementos = new int[size]
5 }
6 public void insere(int x) { ... }
7 public int retira() { ...}
8 }

Listagem 2.5 Tipo ListaDeInteiros2

Tipos de dados, como a ListaDeInteiros2 acima, são deno-


minados Fichas de Dados, sendo uma estrutura muito útil no
processo de modelagem de dados. Sua importância decorre do pro-
cesso de modelagem de objetos do mundo real em objetos de um
programa, no qual os tipos de dados disponı́veis devem ser combi-
nados para formar estruturas mais complexas.
Nesse sentido, uma boa linguagem de programação deve ofere-
cer, no mı́nimo, estruturas primitivas para declarar valores básicos,
como int, char, float, cadeias de caracteres (String), e meca-
nismos para definir novos agregados, como arranjos e fichas de
dados, os quais permitem combinação de elementos mais simples.
Arranjos são estruturas linguı́sticas indispensáveis que permitem
o agrupamento de valores homogêneos, enquanto fichas de dados
servem para criar estruturas com componentes de diversos tipos.
Todos esses recursos são importantes, e é parte do bom estilo de
programação saber utilizá-los com qualidade.
O trecho de código da Listagem 2.6 ilustra o uso de arranjos e
da ficha de dados Aluno.
2.5. IMPLEMENTAÇÕES DE TIPOS 67

1 class Aluno {
2 public String nome;
3 public float nota;
4 }
5 public class Resultado {
6 public static void main(String args[]) {
7 Aluno [ ] r = new Aluno[50];
8 ....
9 r[k].nome = "maria da silva"; // acesso direto!
10 ....
11 for (int i = 0; i < [Link]; i++) {
12 [Link](r[i].nome + ""+ r[i].nota);
13 }
14 }
15 }

Listagem 2.6 uso da Ficha de Dados Aluno

Tipo MeuInt
A classe MeuInt da Listagem 2.7 implementa o tipo abstrato dos
inteiros de 16 bits, sugerido na Seção 2.3 a partir do tipo primitivo
boolean.
1 class MeuInt {
2 private boolean b0,b1,b2,...,b14, b15, c;
3 public MeuInt( ) { ... }
4 public MeuInt(boolean a0,...,boolean a15) { ... }
5 public MeuInt leia( ){ .... }
6 public MeuInt some(MeuInt x, MeuInt y){ ... }
7 public void imprima( ){ .... }
8 }

Listagem 2.7 Definição de MeuInt

O refinamento da operação some, onde a aritmética de com-


68 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

plemento de 2 de um número de 16 bits está esquematizado na


Listagem 2.8.

1 public MeuInt some(MeuInt x, MeuInt y){


2 boolean c = false;
3 MeuInt t = new MeuInt( );
4 t.b15 = (!x.b15) && (! y.b15) && (c)||
5 (!x.b15) && (y.b15) && (! c)||
6 (x.b15) && (! y.b15) && (! c) ||
7 (x.b15) && (y.b15) && (c);
8 c = (x.b15 && y.b15) || (x.b15 && c) || (y.b15 && c);
9 .....................................................
10 t.b9 = (!x.b9) && (! y.b9) && (c) ||
11 (!x.b9) && (y.b9) && (! c) ||
12 (x.b9) && (! y.b9) && (! c) ||
13 (x.b9) && (y.b9) && (c);
14 c = (x.b9 && y.b9) || (x.b9 && c) || (y.b9 && c);
15 t.b8 = (!x.b8) && (! y.b8) && (c) ||
16 (!x.b8) && (y.b8) && (! c) ||
17 (x.b8) && (! y.b8) && (! c) ||
18 (x.b8) && (y.b8) && (c);
19 c = (x.b8 && y.b8) || (x.b8 && c) || (y.b8 && c);
20 .....................................................
21 t.b0 = (!x.b0) && (! y.b0) && (c) ||
22 (!x.b0) && (y.b0) && (! c) ||
23 (x.b0) && (! y.b0) && (! c) ||
24 (x.b0) && (y.b0) && (c);
25 return t;
26 }

Listagem 2.8 Uso de MeuInt

A implementação das operações leia e imprima foi omitida,


pois pouco acrescentaria a mensagem pedagógica desse exemplo.
Para concluir esse exemplo, um programa usuário do tipo abs-
trato de dados MeuInt implementado poderia ser da Listagem 2.9.
2.5. IMPLEMENTAÇÕES DE TIPOS 69

1 public class Somador {


2 public static void main(String [] arg) {
3 MeuInt x = new MeuInt();
4 MeuInt y = new MeuInt();
5 MeuInt t = new MeuInt();
6 [Link] ); [Link]();
7 t = some(x,y); [Link]();
8 }
9 }

Listagem 2.9 Uso de MeuInt

Tipo GrandeInt
Um novo tipo abstrato de dados que pode vir a ser implementado
é o GrandeInt, que, como o nome sugere, implementa um novo
tipo de inteiros capaz de operar com valores de até 50 algarismos
e oferece as operações tı́picas de somar, subtrair e imprimir.
O uso do tipo GrandeInt é ilustrado no código da Listagem 2.10:

1 public classe Usuaria{


2 public static void main(String [ ] arg) {
3 GrandeInt a = new GrandeInt(2000000);
4 GrandeInt b = new GrandeInt("[Link].000.000");
5 GrandeInt c;
6 c = [Link]([Link](a)); // c = a + (b + a)
7 [Link]();
8 }
9 }

Listagem 2.10 Uso de MeuInt

que produz a saı́da: [Link].000.000

E uma implementação completa da classe GrandeInt, pode ser a


seguinte:
70 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

1 public classe GrandeInt {


2

3 private static final int Ndigits = 50;


4 private byte [] valor = new byte[Ndigits];
5

6 public GrandeInt(long d){


7 for (int i = Ndigits-1; i>=0; i--) {
8 valor[i] = (byte)(d % 10); d = d / 10;
9 }
10 }
11

12 public GrandeInt(String d){


13 char a; int k = Ndigits-1;
14 for (int i=[Link]()-1; i>=0; i--) {
15 a = [Link](i);
16 if ([Link](a))
17 valor[k--] = (byte)([Link](a,10));
18 }
19 }
20

21 public GrandeInt soma(GrandeInt x){


22 byte vaium = 0, d;
23 GrandeInt y = (GrandeInt) x;
24 GrandeInt total = new GrandeInt(0);
25 for (int i = Ndigits-1; i>=0; i--) {
26 d = (byte)(valor[i] + [Link][i] + vaium);
27 if (d > 9) {
28 [Link][i] = (byte)(d-10); vaium = 1;
29 }
30 else {
31 [Link][i] = (byte)d; vaium = 0;
32 }
33 }
34 return total;
35 }
36
2.5. IMPLEMENTAÇÕES DE TIPOS 71

37 public GrandeInt sub(GrandeInt x){...} // similar à soma


38

39 public void imprima(){


40 char a; byte b; int k = 0; int s = 0;
41 while ((k < Ndigits - 1) && (valor[k] == 0)) k++;
42 for (int i = k ; i < Ndigits; i++) {
43 if (s++ == 3) {[Link]("."); s = 0;}
44 [Link](valor[i]);
45 }
46 }
47

48 public void imprimaln(){


49 imprima(); [Link]();
50 }
51 ... Implementaç~
ao das Outras Operaç~
oes
52 }

Tipo GrandeInt alternativo


A operação de soma do tipo GrandeInt é uma função de dois
argumentos apresentados na forma c = [Link](b), onde o pri-
meiro argumento a é o objeto receptor da mensagem some, e o
segundo argumento é o b. O valor retornado por [Link] é um
objeto do tipo GrandeInt, cuja representação contém a soma dos
valores contidos em a e b.
Há contudo uma outra forma de estruturar essas operações de
uma forma mais compatı́vel com o espı́rito da Orientação por Ob-
jetos, na qual as operações são vistas como mensagens enviadas
ao objeto receptor, as quais podem alterar seu estado interno do
objeto, mas não retornam valores.
Nesse sentido, a implementação de GrandeInt da Listagem 2.11
adota esse modelo, onde as operações some e sub usam o objeto
receptor como primeiro argumento.
72 CAPÍTULO 2. TIPOS ABSTRATOS DE DADOS

1 public class GrandeInt {


2 private static final int Ndigits = 50;
3 private byte [] valor = new byte[Ndigits];
4 public GrandeInt(long d){ ... }
5 public GrandeInt(String d){ ... }
6 public void some (GrandeInt x){ ... }
7 public void sub (GrandeInt x){ ... }
8 public void imprima() { ... }
9 public void imprimaln() { ... }
10 ...
11 Outras Operaç~
oes
12

13 }

Listagem 2.11 GrandeInt Alternativo

Com essa nova abordagem, a classe usuária passa a ter a forma da


Listagem 2.12 e produz a saı́da: [Link].000.000

1 classe Usuaria{
2 public static void main(String [ ] arg) {
3 GrandeInt a = new GrandeInt(2000000);
4 GrandeInt b = new GrandeInt("[Link].000.000");
5 [Link](a); // b = b + a
6 [Link](b); // a = a + b
7 [Link]();
8 }
9 }

Listagem 2.12 Uso de GrandeInt Alternativo

2.6 Conceito de dicionário

O nome Dicionário deriva da ideia clássica de um dicionário de


uma lı́ngua, que associa informação a palavras de forma a poder
2.6. CONCEITO DE DICIONÁRIO 73

recuperá-las de forma eficiente posteriormente.


Em Computação, Dicionário é o nome comumente utilizado para
descrever uma estrutura de dados que armazena um conjunto re-
gistros sujeitos a operações de inserção, remoção e pesquisa. Ou
seja, Dicionário é um tipo abstrato de dados com pelo menos
as seguintes operações:
• criar um objeto do tipo;
• pesquisar um registro com determinada chave;
• inserir um novo registro;
• retirar um registro especı́fico dada sua chave;
• ordenar os registros de acordo com a chave.
A implementação de um Dicionário obedece ao esquema da Lis-
tagem 2.13.
1 public class Dicionário<T> {
2 private "estrutura de dados do objeto dicionário"
3 public Dicionário(...){ ... }
4 public void insira(...){ ... }
5 public void retire(...){ ... }
6 public void pesquise(...){ ... }
7 public void ordene(...){ ... }
8 }

Listagem 2.13 Uso de MeuInt

A estrutura interna do Dicionário, que é a representação encap-


sulada e protegida contra acessos diretos, tem influência direta no
nı́vel de eficiência das operações do tipo.
Há estruturas que favorecem as operações de inserção ou de
remoção e outras que priorizam a eficiência da pesquisa para uma
dada palavra au chave.
As representações mais comuns para um Dicionário são Tabelas,
Listas Lineares, Árvores de Pesquisa, Árvores Balanceadas (AVL,
74 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

SBB, B-tree, Rubro-Negras), Árvores Tries e Árvores Patricia.


Nos próximos capı́tulos cada uma dessas estruturas é conside-
rada na definição da representação interna do Dicionário e suas
operações do avaliadas caso a caso.

2.7 Conclusão

A invenção do conceito de tipos abstrato de dados foi o avanço mais


importante no sentido de modularizar de forma substantiva gran-
des sistemas de software e efetivamente reduzir lacunas semânticas.

Exercı́cios
1. Descreva como modelar, em um ambiente que não disponibiliza
o tipo String a manipulação de cadeias de caracteres.
2. Avalie importância de se impedir o acesso a representação de
um tipo de dados.

Notas bibliográficas

O conceito de tipos abstratos dados é devido a Barbara Liskov [9],


que o incorporou no projeto da linguagem CLU [10].
***
Capı́tulo 3

Tabelas

Make everything as simple


as possible, but not simpler1
Albert Einstein (1879-1955)

Uma tabela é um conjunto de registros ou fichas de dados, os


quais contêm informações, que, frequentemente incluem chaves
que os identificam. Na implementação proposta a seguir, supõe-
se que chaves, quando presentes, não sejam repetidas, devendo o
usuário do tipo Tabela assegurar-se disso, antes das operações de
inserção de novos elementos na estrutura.
Em geral, tabelas são associadas a entidades de vida curta, cri-
adas na memória interna durante a execução de um programa.
Por outro lado, arquivos estão associados a entidades de vida mais
longa e são armazenados em memória externa.
É importante considerar tabelas como tipos abstratos de dados
à semelhança do tipo Dicionário, definido na Seção 2.5, de forma
que a assegurar total abstração da representação usada, a qual é
somente acessı́vel ao usuário do tipo via as operações disponibili-
zadas.

1
Faça tudo simples, mas não simplificado

75
76 CAPÍTULO 3. TABELAS

3.1 Implementação de tabelas

A implementação de Tabela da Listagem 3.1 segue essa reco-


mendação, encapsulando as informações relevantes.
1 public class Tabela {
2 private int M; // tamanho máximo da tabela
3 private Item[] itens ; // itens[0] n~ ao armazena dados
4 private int nElementos; // número de itens na tabela
5 public static class Item { ... } // Fig.3.2
6 public Tabela(int tamanho) { ... } // Fig.3.4
7 public boolean insira(Item x) { ... } // Fig.3.6
8 public Item retire(String chave) { ... } // Fig.3.8
9 public Item pesquise(String chave) { ... } // Fig.3.7
10 }

Listagem 3.1 Tipo Abstrato Tabela

Uma tabela pode ser vista como o arranjo Item[ ] itens,


onde o registro Item é o definido na Listagem 3.2.
1 public static class Item {
2 public String chave;
3 public Item() { }
4 public Item(String chave) {[Link] = chave; }
5 }

Listagem 3.2 Elementos da Tabela

Em cada aplicação, Item deve ser estendido para incorporar


as demais informações de cada registro da tabela, como na Lista-
gem 3.3.
A função construtora de Tabela, apresentada na Listagem 3.4,
de custo C(n) = 1, promove a alocação da representação dos ob-
jetos do tipo Tabela, e efetua as inicializações necessárias, mere-
cendo destaque o fato de a posição 0 do arranjo ser reservada para
uso interno.
3.1. IMPLEMENTAÇÃO DE TABELAS 77

1 class ItemUsuário extends [Link] {


2 public String nome, endereço;
3 public ItemUsuário(){ }
4 public ItemUsuário(String chave, nome, endereço){
5 super(chave);
6 [Link] = nome; [Link]̧o = endereço;
7 }
8 }

Listagem 3.3 Definição de um Item Particular

1 public Tabela(int tamanho){


2 if (tamanho <= 0) M = 1; else M = tamanho;
3 itens = new Item[M];
4 itens[0] = new Item();
5 nElementos = 0;
6 }

Listagem 3.4 Construtora de Tabela

O programa da Listagem 3.5 cria uma tabela com 100 itens do


tipo ItemUsuário.
1 class Usuário {
2 public static void main(String[] args) {
3 Item item;
4 String chave, nome, endereço;
5 Tabela minhaTabela = new Tabela(100);
6 for (int i=1; i <= 100; i++) {
7 ... // código para obter dados do i-ésimo item
8 item = new ItemUsuário(chave, nome, endereço);
9 [Link](item);
10 }
11 }
12 }

Listagem 3.5 Um Usuáro de Tabela


78 CAPÍTULO 3. TABELAS

A operação insira, de custo C(n) = 1, inclui um novo elemento


na primeira posição livre do arranjo de itens da tabela e retorna
true se tudo correu bem, do contrário, retorna false para indicar
que houve estouro de área na tabela. Detalhes na Listagem 3.6.
1 public boolean insira(Item x) {
2 if (nElementos == M) return false;
3 else {
4 itens[++nElementos] = x;
5 return true;
6 }
7 }

Listagem 3.6 Operação insira

A operação pesquise() da Listagem 3.7 retorna o item da


tabela que contém a chave passada como parâmetro; caso a chave
não seja encontrada, o valor retornado é null.
1 public Item pesquise(String chave){
2 int i;
3 itens[0].chave = chave; // estabelece sentinela
4 i = nElementos;
5 while (!(itens[i].chave).equals(chave)) i--;
6 if (i != 0) return itens[i];
7 else return null;
8 }

Listagem 3.7 Pesquisa Sequencial Rápida

Para acelerar o processo de pesquisa, o algoritmo da Lista-


gem 3.7 usa o registro sentinela, contendo a chave de pesquisa, e
que é colocado na posição zero da tabela.
Essa técnica elimina a necessidade de testar, passo a passo, se
ainda há elementos a ser examinados, pois a sentinela assegura que
a busca pela chave sempre termina com sucesso.
3.1. IMPLEMENTAÇÃO DE TABELAS 79

Assim, após a execução do comando while da linha 5 da função


pesquise (Listagem 3.7), se o ı́ndice do elemento encontrado for
zero, significa que a pesquisa foi logicamente sem sucesso. Isso
torna o comando de repetição da função pesquise extremamente
simples.
Esta técnica de pesquisa usando sentinela é conhecida por
pesquisa sequencial rápida, pois ela elimina a necessidade de
se verificar a cada iteração do processo de busca se ainda há itens
a ser testados, desta forma eliminando um teste em cada iteração
a um custo da reserva de apenas uma posição da tabela.
Considerando o número de comparações realizadas em uma pes-
quisa com sucesso, o custo de execução de pesquise é:
• melhor caso, o primeiro item testado é o desejado: C(n) = 1
• pior caso, o último item testado é o procurado: C(n) = n
• caso médio, metade dos itens são testados : C(n) = (n + 1)/2
E no caso de pesquisa sem sucesso, o custo é C(n) = n + 1,
pois as chaves de todos os elementos da tabela de n + 1 itens são
testados.
A operação retire deve localizar no arranjo o registro com
a chave dada, e se ele for encontrado, removê-lo e trazer para a
posição que ficou vaga o último elemento para garantir a contigui-
dade de seus elementos.
E ainda, conforme mostra a Listagem 3.8, se o item desejado esti-
ver na tabela, retire deve retornar sua referência, caso contrário,
a operação retorna null.
O custo da operação retire é C(n) = n + 1, pois, no pior
caso, quando o elemento procurado não está na tabela, deve-se
percorrê-la totalmente, incluindo o teste da sentinela.
80 CAPÍTULO 3. TABELAS

1 public Item retire(String chave) {


2 int i = nElementos;
3 Item item = null;
4 itens[0].chave = chave; // estabelece sentinela
5 while (!(itens[i].chave).equals(chave)) i--;
6 if (i != 0) {
7 item = itens[i];
8 itens[i] = itens[nElementos--]; //compactaç~
ao
9 }
10 return item;
11 }

Listagem 3.8 Operação retire

3.2 Tabelas Ordenadas

Os itens da tabela apresentada na seção anterior ocupam os espaços


do arranjo interno pela ordem de inserção, não quardando qualquer
ordenação entre eles, o que encarece o custo da operação pesquise,
mas permite implementações de insira e retire de baixo custo.
Alternativamente, manter os elementos da tabela armazenados
em ordem crescente, por exemplo, reduz o custo da pesquisa, mas
tem implicação nos custos das demais operações da tabela, pois nos
casos de inserção e remoção há o custo extra de se ter que deslocar
itens para manter a tabela ordenada.
Assim, a decisão optar por manter a tabela ordenada deve con-
siderar os seguintes fatores:
• quantidade dos dados envolvidos, i.e., o tamanho da tabela
• frequência das operação de inserção
• frequência da operação de retirada
• frequência da operação de pesquisa
• restrições quanto ao tempo de pesquisa
Em suma, quando a tabela é pequena, estar ordenada ou não
3.2. TABELAS ORDENADAS 81

tem pouco impacto nos custos de execução. Em grandes massas


de dados que sejam estáveis, isto é, nas quais as operações de
inserção ou remoção são infrequentes, e se as de pesquisa forem de
uso rotineiro, pode ser vantajoso pagar pelo custo de manutenção
da ordenação, como mostram as fórmulas de custo associadas aos
algoritmos a seguir. Por outro lado, em massas de dados voláteis,
os custos de inserção e remoção por ser consideravelmente altos
podem demandar outras soluções.
A seguir, são apresentadas e avaliadas as implementações das
operações insira, retire e pesquisa para o caso de se manter
os itens armazenados em ordem crescente.
1 public boolean insira(Item x) {
2 if (nElementos == M) return false;
3 else {
4 tabela[0].chave = [Link];
5 i = nElementos;
6 while ((tabela[i].chave).compareTo([Link]) > 0 ) {
7 tabela[i+1] = tabela[i]; i--;
8 }
9 tabela[i+1] = x; ++nElementos;
10 return true;
11 }
12 }

Listagem 3.9 Operação insira

Operação insira em Tabelas Ordenadas

A operação insira percorre a tabela do seu fim para o inı́cio, des-


locando os elementos uma posição no sentido do fim, para abrir
espaço para inserção do novo elemento, como mostra a Lista-
gem 3.9.
O custo médio de insira é o de percorrer a metade da tabela,
82 CAPÍTULO 3. TABELAS

ou seja, C(n) = (n + 1)/2. O pior caso tem custo C(n) = n + 1,


e o melhor, C(n) = 1.

Operação retire em Tabelas Ordenadas

A operação retire, após localizar o item com a chave dada e


removê-lo, deve fazer a compactação do arranjo de itens para não
deixar posição vaga. Em tabelas desordenadas, a solução é mover
o último elemento para ocupar o lugar daquele que foi removido.
Em tabelas ordenadas, deslocam-se uma posição para baixo os
elementos com chaves maiores que a do item removido.
Assim, o arranjo de itens pode ter que ser percorrido duas vezes:
uma para localizar o registro a ser retirado e outra para fazer a
compactação, conforme está detalhado na Listagem 3.10.

1 public Item retire(String chave) {


2 int i = nElementos;
3 Item item = null;
4 itens[0].chave = chave;
5 while (!(itens[i].chave).equals(chave)) i--;
6 if (i != 0) {
7 item = itens[i];
8 for(int j=i; j < nElementos; j++){
9 itens[j] = itens[j+1];
10 }
11 nElementos--;
12 }
13 return item;
14 }

Listagem 3.10 Operação retire

Os custos de retire são semelhantes aos de insira, mas o


custo médio agora é o de percorrer a metade da tabela duas vezes,
3.2. TABELAS ORDENADAS 83

ou seja, C(n) = (2n + 1)/2, o do pior caso é C(n) = 2n, e o do


melhor, C(n) = 1.

Operação pesquise em Tabelas Ordenadas

Pode-se implementar a pesquisa em tabelas ordenadas de pelos


menos duas formas: pesquisa sequencial ou pesquisa binária. Na
pesquisa sequencial, faz-se uma busca, do fim para o inı́cio da ta-
bela, de forma a descobrir a situação de fracasso assim que a chave
examinada for menor que a procurada, eliminando o caminhamento
no resto da tabela, como mostrado na Listagem 3.11.
1 public Item pesquise(String chave, Item[] tabela, int n){
2 int i = n;
3 tabela[0] = new Item(chave);
4 while ((tabela[i].chave).compareTo(chave) > 0 ) i--;
5 if ((i != 0) && (tabela[i].chave).equals(chave))
6 return tabela[i];
7 else return null;
8 }

Listagem 3.11 Pesquisa Sequencial em Tabela Ordenada

O custo médio de pesquise é C(n) = (n + 1)/2. No melhor


caso, quando o elemento procurado é o primeiro testado, o custo
é C(n) = 1 e no pior caso, C(n) = n + 1, quando toda a tabela
tiver que ser percorrida.
A segunda solução, que é chamada de pesquisa binária, segue
os seguintes passos:
• compara a chave com a do item da posição do meio da tabela
• se a chave for menor, então o registro procurado pode estar na
primeira metade da tabela
• se a chave for maior, então o registro procurado pode estar
na segunda metade da tabela
84 CAPÍTULO 3. TABELAS

• repita o processo até que a chave seja encontrada, ou que a


subtabela de pesquisa tenha tamanho zero.
O método pesquise da Listagem 3.12 implementa os passos
acima e retorna o item encontrado, ou null, no caso não seja
encontrado.
1 public Item pesquise(String chave) {
2 int i, esq, dir, cmp;
3 if (nElementos == 0) return null;
4 esq = 1; dir = nElementos;
5 do {
6 i = (esq + dir) / 2;
7 cmp = (itens[i].chave).compareTo(chave);
8 if (cmp == 0) return itens[i];
9 if (cmp < 0) esq = i + 1; else dir = i - 1;
10 } while (esq <= dir);
11 return null;
12 }

Listagem 3.12 Pesquisa Binária

Em cada iteração do comando de repetição desse algoritmo, o


tamanho da parte da tabela a pesquisar é dividido ao meio, com
mostra a Fig. 3.13, onde n é o tamanho da tabela sendo pesquisada.
#testes Tamanho da Tabela
1 n
2 n/2
3 n/4
4 n/8
··· ···
k n/2k−1

Figura 3.13 Passos de Pesquisa

O custo de pesquise é o número de testes realizados, isto é,


C(n) = k, onde k é o número de vezes que o tamanho da parte da
3.3. CONCLUSÃO 85

subtabela a ser pesquisada é dividido ao meio.


No pior caso, k é o número de vezes que leva o tamanho da
subtabela a ser pesquisada a ter o tamanho 1. Nesse caso, tem-se
que n/2k−1 = 1, e, portanto, k = lg n + 1.
No melhor caso, quando o item procurado é o primeiro testado,
o custo é C(n) = 1, e o custo médio, supondo que todos os itens
tem mesma probabilidade de ser buscados, é C(n) = (lg n + 1)/2.

3.3 Conclusão

Tabelas são uma estrutura básica de programação usadas desde os


primórdios da programação de compudores.

Exercı́cios
1. Tabelas ordenadas permitem implementação eficiente de al-
goritmo de busca, mas dificulta as operações de inserção e
remoção de itens. Proponha implementações de tabelas que
privilegiam essas três operações.

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre a estru-


tura de dados, e particularmente, do conceito de tabelas, é o livro
clássico de Donald Knuth [6, 7], que dá um tratamento formal e
organizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
86 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 4

Listas

Multiplicanda non est ponenda sine necessitate1


William of Ockham (1285-1349)

Listas, diferentemente de tabelas, são estruturas de dados que


podem ser de tamanho flexı́vel e geralmente permitem operações
de baixo custo para inserção e remoção de elementos.
Listas são adequadas para aplicações onde não é possı́vel pre-
ver o número máximo de itens de um contêiner, e, assim, poder
prefixar a demanda por memória, como ocorre com as tabelas estu-
dadas no Capı́tulo 3. Em geral, listas permitem a manipulação de
quantidades imprevisı́veis de dados, e, com frequência, valoriza a
facilidade de inserir e remover elementos em detrimento dos custos
de pesquisa.

Definição 4.1 Uma lista linear é uma sequência de zero ou


mais itens x1, x2, · · · , xn, possuindo as seguintes propriedades:
• x1 é o primeiro item da lista
• xn é o último item da lista
• xi precede xi+1 para i = 1, 2, · · · , n − 1
• xi sucede xi−1 para i = 2, 3, · · · , n
1
Não se deve multiplicar sem necessidade

87
88 CAPÍTULO 4. LISTAS

Os tipos abstratos de dados apresentados neste capı́tulo são no-


meados ListaX, onde X ∈ {S, F, E, M, DE}, e são caracterizados
por incluir pelo menos as operações que permitem:
• criar uma lista linear vazia;
• inserir um novo item;
• retirar um dado item;
• pesquisar a ocorrência de um item com uma dada chave;
• verificar se houve erro na operação realizada.

Nas implementações de listas apresentadas seguir, variações des-


sas operações estão detalhadas, cada uma adaptada conforme sua
semântica especial.
Outras operações comumente implementadas em listas, mas não
tratadas aqui, podem incluir as seguintes:
• localizar o i-ésimo item;
• alterar o conteúdo de determinados itens;
• combinar duas ou mais listas lineares em uma lista única;
• dividir uma lista linear em duas ou mais listas;
• fazer uma cópia da lista linear;
• ordenar os itens da lista de acordo com os valores de seus
componentes;
• comparar duas listas.

Na implementação dos tipos abstratos da famı́lia ListaX, é pre-


ciso definir sua representação interna, a qual tem influência no
desempenho ou custo de suas operações, favorecendo algumas em
detrimento de outras, devendo ser selecionada conforme a demanda
da aplicação. Por exemplo, a decisão de facilitar inserção e remoção
de elementos pode ter impacto negativo no custo da pesquisa por
algum elemento.
4.1. LISTAS SEQUENCIAIS 89

Há três tipos de implementações de uma lista muito populares,


classificadas conforme a representação escolhida para sua estrutura
interna:
• lista sequencial: implementação com arranjos;
• lista encadeada: implementação com apontadores;
• lista máquina de estado: inplementação com cursores.
A seguir, cada uma dessas implementações são descritas e ilus-
tradas em detalhes. As estruturas descritas implementam listas de
objetos do tipo Item, e os programas de teste usados nesta seção
trabalham com listas de objetos do tipo MeuItem. Esses tipos estão
definidos na Listagem 4.1, onde destaca-se que todo objeto Item
necessariamente possui uma chave, e a classe MeuItem tem as in-
formações que o usuário da lista desejar.
1 public class Item {
2 String chave;
3 Item(String chave) {[Link] = chave;}
4 }
5

6 public class MeuItem extends Item {


7 String nome;
8 MeuItem(String chave, String nome) {
9 super(chave);
10 [Link] = nome;
11 }
12 }

Listagem 4.1 Tipos de Itens das Listas

4.1 Listas sequenciais

Ums lista sequencial pode ser caracterizada pelas operações anun-


ciadas na Listagem 4.2.
90 CAPÍTULO 4. LISTAS

1 public class ListaS {


2 private Item[] itens;
3 private int maxTam, último = -1;
4 private boolean operaç~
aoOK = true;;
5 public ListaS(int maxTam) {
6 [Link] = (maxTam <= 0)? 1: maxTam;
7 itens = new Item[[Link]];
8 }
9 public boolean vazia() {...}
10 public void insira(Item x) { ...}
11 public Item retire(int p) {...}
12 public boolean houveErro() {...}
13 public Item pesquise(String chave) {...}
14 public voi imprimaChaves() {...}
15 }

Listagem 4.2 Tipo ListaS

Nessa listagem a representação interna da lista é baseada em


um arranjo de de objetos do tipo Item. Por exemplo, o comando
new ListaS(9) aloca a seguinte estrutura:

que representa uma lista vazia com capacidade para 9 itens.


A operação vazia é a definida na Listagem 4.3.

1 public boolean vazia() {


2 operaç~
aoOK = true;
3 return (último == -1);
4 }

Listagem 4.3 Operação [Link]


4.1. LISTAS SEQUENCIAIS 91

A operação insira instala, se houver espaço, um novo item na


posição apontada por último+1, i.e., no fim da lista conforme mos-
tra a Listagem 4.4. Se não houver espaço, essa situação de erro é
anotada no campo privado operaç~aoOK, o qual pode ser consultado
pelo usuário via a operação houveErro.
1 public void insira(Item x) {
2 if (último == maxTam-1) operaç~
aoOK = false;
3 else {
4 itens[++último] = x;
5 operaç~
aoOK = true;
6 }
7 }

Listagem 4.4 Operação [Link]

A operação retire da Listagem 4.5 remove o item de ordem p,


1 <= p <= último, e faz a compactação do arranjo interno. Um
valor de p fora do espaço de itens da lista gera uma indicação de
erro. Essa operação tem custo O(n).
1 public Item retire(int p) {
2 Item x = null; p--;
3 if (vazia() || p > último || p < 0) operaç~
aoOK = false;
4 else { x = [Link][p]; ultimo--;
5 for(int i = p; i <= último; i++) {
6 itens[i] = itens[i+1];
7 }
8 operaç~
aoOK = true;
9 }
10 return x;
11 }

Listagem 4.5 Operação [Link]

As indicações de erro registradas pelas operações insira e retire


são recuperáveis via a operação houveErro, definida na Listagem 4.6.
92 CAPÍTULO 4. LISTAS

Cumpre ao usuário verificar a correção de cada operação realizada


antes que uma outra seja solicitada.

1 public boolean houveErro() {


2 return (!operaç~
aoOK);
3 }

Listagem 4.6 Operação [Link]

A operação pesquise, definida na Listagem 4.7, localiza um item


na lista sequencial dada sua chave e retorna a referência ao item
encontrado ou o valor null, caso o item não esteja na lista. Essa
operação tem custo O(n).

1 public Item pesquise(String chave) {


2 int i = 0;
3 boolean achou = false;
4 while ((i =< último) && (!achou)) {
5 if (itens[i].[Link](chave)) achou = true;
6 else i++;
7 }
8 operaç~
aoOK = true;
9 if (achou) return itens[i]; else return null;
10 }

Listagem 4.7 Operação [Link]

A operação imprimaChaves está definida na Listagem 4.8.

1 public void imprimaChaves() {


2 for (int i = 0 ; i <= último ; i++)
3 [Link](" " + itens[i].chave);
4 [Link]();
5 }

Listagem 4.8 Operação [Link]


4.1. LISTAS SEQUENCIAIS 93

O programa a seguir cria duas listas sequenciais, z1 e z2, insere


em z1 seis itens, depois transfere três itens de z1 para z2, em se-
guida pesquisa em z2 por um item de uma dada chave e termina
a execução retirando e imprimindo o todo conteúdo de z2.

1 public class UsaListaS {


2 public static void main(String[] args) {
3 ListaS z1 = new ListaS(9), z2 = new ListaS(9);
4 MeuItem x, itens[] = new MeuItem[6];
5 for (int i = 0; i < 6; i++)
6 itens[i] = new MeuItem("A" + i, "B" + i);
7 [Link]("Itens criados: ");
8 for (int i = 0; i < 6; i++) [Link](
9 "(" + itens[i].chave + "," + itens[i].nome + ")");
10 [Link]();
11 [Link]("Itens criados inseridos em z1");
12 for (int i = 0; i < 6; i++) {
13 [Link](itens[i]);
14 if ([Link]())
15 [Link]("Espaço Esgotado");
16 }
17 [Link]("z1:"); [Link]();
18 [Link]("Transfere 3 itens de z1 para z2");
19 for (int p = 1; p < 4; p++) {
20 x = (MeuItem)[Link](1);
21 if ([Link]())
22 [Link]("Item n~ao encontrado");
23 [Link](x);
24 if ([Link]())
25 [Link]("Espaço Esgotado");
26 }
27 [Link]("z1:"); [Link]();
28 [Link]("z2:"); [Link]();
29 x = (MeuItem) [Link]("A2");
30 [Link]("Item com chave A2 = " + [Link]);
31 [Link]("Itens removidos de z2:");
94 CAPÍTULO 4. LISTAS

32 while (![Link]()) {
33 x = (MeuItem)[Link](1);
34 if ([Link]())
35 [Link]("Item n~
ao encontrado");
36 [Link](" chave = "+ [Link] +
37 " nome = " + [Link]);
38 }
39 }
40 }
41

O programa acima imprime o resultado da Fig. 4.9

Itens criados:(A0,B0)(A1,B1)(A2,B2)(A3,B3)(A4,B4)(A5,B5)
Itens criados inseridos em z1
z1: A0 A1 A2 A3 A4 A5
Transfere 3 itens de z1 para z2
z1: A3 A4 A5
z2: A0 A1 A2
Item com chave A2 = B2
Itens removidos de z2:
chave = A0 nome = B0
chave = A1 nome = B1
chave = A2 nome = B2

Figura 4.9 Resultado de UsaLista

4.2 Listas sequenciais com tratamento de falhas

A implementação de listas da Seção 4.1 requer que, após as opera-


ções de inserção e remoção, verifique-se se houve ocorrência de erro,
o que pode causar poluição do código. Uma alternativa menos
poluidora é o uso dos recursos para tratamento de exceção.
4.2. LISTAS SEQUENCIAIS COM TRATAMENTO DE FALHAS 95

A Listagem 4.10 apresenta a implementação de uma lista da


famı́lia de ListaX que usa exceções para indicar ocorrências de erros,
por meio das classes de exceção Indexaç~ao e Estouro.
1 public class ListaF {
2 ao extends Exception {}
public static class Indexaç~
3 public static class Estouro extends Exception {}
4 private Item[] itens;
5 private int maxTam;
6 private int último = -1;
7 public ListaF(int maxTam) {
8 [Link] = (maxTam <= 0) ? 1 : maxTam;
9 itens = new Item[[Link]];
10 }
11 public boolean vazia() {...}
12 public void insira(Item x) throws Estouro {...}
13 ao {...}
public Item retire(int p) throws Indexaç~
14 public Item pesquise(String chave) {...}
15 public void imprimaChaves() {...}
16 }

Listagem 4.10 Lista com Tratamento de Falhas

A implementação de ListaF é muito semelhante a de ListaS.


Por exemplo, as operações vazia, pesquise e imprimaChaves das
Listagens 4.3, 4.7 e 4.8 não são afetadas pela introdução dos meca-
nismos de tratamento de exceção para sinalização de situações de
erro, pois não há previsão de ocorrência de erros nessas operações,
e, assim, elas podem ser incorporadas à ListaF sem modificações.
Por outro lado, as operações afetadas pelo uso de exceções são
insira e retire, as quais devem anunciar em suas interfaces a
possibilidade de levantamento de exceções de estouro do espaço
alocado para sua representação ou de erro de indexão do item,
respectivamente, conforme está definido na Listagem 4.11.
96 CAPÍTULO 4. LISTAS

1 public void insira(Item x) throws Estouro {


2 if (último == maxTam-1) throw new Estouro();
3 else itens[++último] = x;
4 }
5 ao {
public Item retire(int p) throws Indexaç~
6 Item x = null; p--;
7 if (vazia() || p>último || p<0) throw new Indexaç~
ao();
8 else {
9 x = [Link][p]; último--;
10 for(int i=p; i<=último; i++) itens[i] = itens[i+1];
11 }
12 return x;
13 }

Listagem 4.11 Operações [Link] e [Link]

O programa a seguir é um usuário do tipo abstrato de dados


ListaF, o qual é dotado de tratamento de falhas via levantamento
de exceções, e imprime o resultado exibido na Fig.4.9.

1 public class UsaListaF {


2 public static void main(String[] args) {
3 ListaF z1 = new ListaF(9), z2 = new ListaF(9);
4 MeuItem x, itens[] = new MeuItem[6];
5 for (int i = 0; i < 6; i++)
6 itens[i] = new MeuItem("A" + i, "B" + i);
7 [Link]("Itens criados: ");
8 for (int i = 0; i < 6; i++) [Link](
9 "(" + itens[i].chave + "," + itens[i].nome + ")");
10 [Link]();
11 try {
12 [Link]("Itens criados inseridos em z1");
13 for (int i = 0; i < 6; i++) {[Link](itens[i]);}
14 [Link]("z1:"); [Link]();
15 [Link](
16 "Transfere 3 itens de z1 para z2");
4.2. LISTAS SEQUENCIAIS COM TRATAMENTO DE FALHAS 97

17 for (int p = 1; p < 4; p++) {


18 x = (MeuItem)[Link](1);
19 [Link](x);
20 }
21 [Link]("z1:"); [Link]();
22 [Link]("z2:"); [Link]();
23 x = (MeuItem) [Link]("A2");
24 [Link](
25 "Item com chave A2 = " + [Link]);
26 [Link]("Itens removidos de z2:");
27 while (![Link]()) {
28 x = (MeuItem)[Link](1);
29 [Link](" chave = "+ [Link] +
30 " nome = " + [Link]);
31 }
32 }
33 ao e) {
catch ([Link]̧~
34 [Link]("Item n~ao encontrado.");}
35 catch ([Link] e) {
36 [Link]("Espaço esgotado.");}
37 }
38 }

As implementações de listas sequenciais apresentadas trabalham


com itens do tipo Item. O polimorfismo referencial universal vin-
culada à hierarquia de tipos enraizada em Item permite que uma
mesma lista armazene objetos de diferentes tipos, desde que sejam
da hierarquia de Item. Isso torna a lista flexı́vel, mas torna ne-
cessário que o usuário do tipo cuide de converter, via type casting,
itens retornados pelas operações retire e pesquise para os tipos
sendo manipulados pela aplicação, no caso, o tipo MeuItem.
A nova implementação de listas sequenciais apresentada a se-
guir faz uso do mecanismo de classes genéricas para eliminar essa
necessidade de conversão de tipo.
98 CAPÍTULO 4. LISTAS

4.3 Listas sequenciais genéricas

Classes genéricas são um mecanismo para criar tipos parametri-


zados, i.e., tipos que têm tipos como parâmetros, os quais podem
ser instanciados no momento do uso da respectiva classe genérica.
Geralmente essas classes genéricas implementam estruturas do tipo
contêiner de objetos de um mesmo tipo.
Com ListaS e ListaF, os elementos dessas listas podem ser quais-
quer objetos cujos tipos sejam Item ou seus descendentes, portanto,
dentre certos limites, representam estruturas heterogêneas. Entre-
tanto, muitas vezes, restringir os elementos da lista a ser de um
mesmo tipo ajuda na legibilidade do código e no processo de de-
puração, pois erros de tipos são mais facilmente identificados. Para
obter esse resultado, pode-se, em vez de representar a lista como
uma estrutura de objetos do tipo Item, define-se que esses objetos
sejam de um tipo genérico T, a ser instanciado em cada declaração
de uma nova lista.
Os parâmetros de tipo de uma classe genérica podem correspon-
der a qualquer tipo da hierarquia de Object, o que permitir cons-
truir lista de objetos de qualquer tipo, mas tem a restrição de que,
dentro da classe que implementa a lista, somente as operações de
Object são aplicáveis aos seus elementos. O uso outras operações,
como acesso a chave de um item, demandaria conversão de tipo
para não ser rejeitada pelo compilador e ainda poderia prejudicar
a genericidade da lista.
Nas implementações de ListaS e ListaF, aqui apresentadas, os
itens de qualquer lista pertencem a hierarquia da classe Item, assim
feito para que todo elemento de qualquer lista tenha uma chave.
Para manter essa semântica nas listas genéricas, deve-se forçar que
o tipo T, parâmetro da classe genérica, seja também um descen-
dente de Item, o qual é visto com limite superior desse tipo.
4.3. LISTAS SEQUENCIAIS GENÉRICAS 99

A nova classe ListaG, agora genérica, é a definida na Lista-


gem 4.12.

1 public class ListaG<T extends Item> {


2 ao extends Exception {}
public static class Indexaç~
3 public static class Estouro extends Exception {}
4 private T[] itens;
5 private int maxTam, último = -1;
6 public ListaG(int maxTam) {
7 [Link] = (maxTam <= 0)? 1: maxTam;
8 itens = (T[]) new Item[[Link]];
9 }
10 public boolean vazia() {...}
11 public void insira(T x) throws Estouro {...}
12 ao {...}
public T retire(int p) throws Indexaç~
13 public T pesquise(String chave) { ...}
14 public void imprimaChaves() {...}
15 }

Listagem 4.12 Tipo Genérico ListaG<T>

As operações de ListaG são as mesmas da Listagem 4.10, exceto


pelo uso do tipo genérico T ao invés de Item para parâmetro e
valor de retorno de algumas operações. Note que as operações da
Listagem 4.13 são as mesmas das listagens 4.3 e 4.8.

1 public boolean vazia() {


2 return (último == -1);
3 }
4 public void imprimaChaves() {
5 for (int i = 0 ; i <= último ; i++)
6 [Link](" " + itens[i].chave);
7 [Link]();
8 }

Listagem 4.13 Operações de ListaG<T>


100 CAPÍTULO 4. LISTAS

As demais operações de tipo ListaG são as definidas na Lista-


gem 4.14, onde destaca-se o uso do tipo genérico T.
1 public void insira(T x) throws Estouro {
2 if (último == maxTam-1) throw new Estouro();
3 else itens[++último] = x;
4 }
5 ao {
public T retire(int p) throws Indexaç~
6 T x = null; p--;
7 if (vazia() || p>último || p<0) throw new Indexaç~
ao();
8 else {
9 x = [Link][p]; último--;
10 for(int i = p; i <= último; i++) {
11 itens[i] = itens[i+1];
12 }
13 }
14 return x;
15 }
16 public T pesquise(String chave) {
17 int i = 0; boolean achou = false;
18 while ((i <= último) && (!achou)) {
19 if (itens[i].[Link](chave)) achou = true;
20 else i++;
21 }
22 if (achou) return itens[i]; else return null;
23 }

Listagem 4.14 Operações insira, retire e pesquise de ListaG<T>

O programa do usuário de ListaG<T> também usa a classe MeuItem


dos exemplos anteriores e é ligeiramente alterado para instanciar
esse tipo genérico adequadamente, como mostrado a seguir:

1 public class UsaListaG {


2 public static void main(String[] args) {
3 ListaG<MeuItem>z1 = new ListaG<MeuItem>(9);
4 ListaG<MeuItem>z2 = new ListaG<MeuItem>(9);
4.3. LISTAS SEQUENCIAIS GENÉRICAS 101

5 MeuItem x, itens[] = new MeuItem[6];


6 for (int i = 0; i < 6; i++)
7 itens[i] = new MeuItem("A" + i, "B" + i);
8 [Link]("Itens criados: ");
9 for (int i = 0; i < 6; i++) [Link](
10 "(" + itens[i].chave + "," + itens[i].nome + ")");
11 [Link]();
12 try {
13 [Link]("Itens criados inseridos em z1");
14 for (int i = 0; i < 6; i++) {[Link](itens[i]);}
15 [Link]("z1:"); [Link]();
16 [Link](
17 "Transfere 3 itens de z1 para z2");
18 for (int p = 1; p < 4; p++) {
19 x = [Link](1);
20 [Link](x);
21 }
22 [Link]("z1:"); [Link]();
23 [Link]("z2:"); [Link]();
24 x = [Link]("A2");
25 [Link]("Item com chave A2 = " + [Link]);
26 [Link]("Itens removidos de z2:");
27 while (![Link]()) {
28 x = [Link](1);
29 [Link](" chave = " + [Link] +
30 " nome = " + [Link]);
31 }
32 }
33 ao e) {
catch ([Link]̧~
34 [Link]("Item n~ao encontrado.");}
35 catch ([Link] e) {
36 [Link]("Espaço esgotado.");}
37 }
38 }

O programa acima imprime o resultado da Fig.4.9.


102 CAPÍTULO 4. LISTAS

4.4 Listas encadeadas

Na alocação encadeada, listas são constituı́das de células, cada uma


contendo um item da lista e um apontador para a célula seguinte.
Nesse tipo de implementação, são usadas posições não-contı́guas
de memória, sendo possı́vel inserir e retirar elementos sem haver
necessidade de deslocar os itens seguintes da lista, como ocorria
nas implementações com alocação sequencial.
Toda lista é identificada por uma referência à sua primeira célula,
a partir da qual pode-se caminhar por todas as demais células.
Nesse caso, uma lista vazia é denotada por um apontador nulo.
Alternativamente, uma lista pode ser munida de uma célula es-
pecial, denominada célula-cabeça, que passa a ser sempre a sua
primeira célula fı́sica. A célula-cabeça não contém informação e
serve para tornar as operações sobre a estrutura de lista unifor-
mes.
Por exemplo, uma lista com célula-cabeça e os itens x1, x2 e x3
tem a seguinte estrutura:

onde primeiro e último são apontadores para o inı́cio e fim da lista,


respectivamente.
Observe que para inserir uma nova célula na lista, precisa-se
apenas obter o endereço da célula após a qual ela será instalada. A
presença da célula-cabeça garante que a inserção do primeiro item
segue exatamente os passos de inserção de qualquer outro, dando
uniformidade às operações da lista.
A implementação da lista que deu origem à configuração acima
está esboçada na Listagem 4.15.
4.4. LISTAS ENCADEADAS 103

1 public class ListaE {


2 ao extends Exception {}
public static class Indexaç~
3 public static class Estouro extends Exception {}
4 private static class Celula {
5 Item valor; Celula prox;
6 Celula() {}
7 Celula(Item item) {[Link] = item;}
8 }
9 private Celula primeiro, ultimo;
10 public ListaE(){
11 primeiro = new Celula(); ultimo = primeiro;
12 }
13 public boolean vazia() { ... }
14 public void insira(Item x) { ... }
15 public Item retire(int p) { ... }
16 public Item pesquise(String chave){ ... }
17 public void imprimaChaves() {...}
18 }

Listagem 4.15 Tipo Abstrato Lista Encadeada

Observe que a construtora de ListaE cria a célula-cabeça, confi-


gurando uma lista inicialmente vazia com a seguinte forma:

O que justifica o código da Listagem 4.16 para a operação que


verifica situação de lista vazia.

1 public boolean vazia( ) {


2 return primeiro == ultimo;
3 }

Listagem 4.16 Operação [Link]


104 CAPÍTULO 4. LISTAS

A operação de inserção no fim da lista é mostrada na Lista-


gem 4.17.

1 public void insira(Item x) throws Estouro {


2 try {[Link] = new Celula(x);}
3 catch (Exception e) throw new Estouro();
4 ultimo = [Link];
5 }

Listagem 4.17 Operação [Link]

Por exemplo, a inserção do item x1 na lista vazia gera a seguinte


configuração:

A operação retire é mais complexa, pois deve inicialmente lo-


calizar a a célula a ser removida, i.e., o p-ésimo item da lista. Essa
operação tem custo O(n), conforme detalhado na Listagem 4.18.

1 ao {
public Item retire(int p) throws Indexaç~
2 Item retirado;
3 Celula r = [Link], q = [Link];
4 int i = 0;
5 while ((i < p) && (q != null)) {
6 r = q; q = [Link]; i++;
7 }
8 if ((i > 0) && (i == p) && (q != null)) {
9 [Link] = [Link]; retirado = [Link];
10 if ([Link] == null) [Link] = r;
11 } else throw new Indexaç~
ao();
12 return retirado;
13 }

Listagem 4.18 Operação [Link]


4.4. LISTAS ENCADEADAS 105

A operação pesquise tem custo O(n), pois a lista deve ser percor-
rida para localizar o elemento desejado, conforme a Listagem 4.19.
1 public Item pesquise(String chave) {
2 Celula p = ([Link]).prox;
3 boolean achou = false;
4 while ((p != null) && (!achou)){
5 if ([Link](chave))
6 achou = true;
7 else p = [Link];
8 }
9 if (achou) return [Link]
10 else return null;
11 }

Listagem 4.19 Operação [Link]

E a operação imprimaChaves está definida na Listagem 4.20.


1 public void imprimaChaves() {
2 Celula p = ([Link]).prox;
3 while (p != null) {
4 [Link](" "+ [Link]);
5 p = [Link];
6 }
7 [Link]();
8 }

Listagem 4.20 Operação [Link]

O programa usuário da lista encadeada ListaE é similar ao da


lista sequencial, sendo a única diferença as chamadas das funções
construtora:

1 public class UsaListaE {


2 public static void main(String[] args) {
3 ListaE z1 = new ListaE(), z2 = new ListaE();
106 CAPÍTULO 4. LISTAS

4 MeuItem x, itens[] = new MeuItem[6];


5 for (int i = 0; i < 6; i++)
6 itens[i] = new MeuItem("A" + i, "B" + i);
7 [Link]("Itens criados: ");
8 for (int i = 0; i < 6; i++) [Link](
9 "(" + itens[i].chave + "," + itens[i].nome + ")");
10 [Link]();
11 try {
12 [Link]("Itens criados inseridos em z1");
13 for (int i = 0; i < 6; i++) [Link](itens[i]);
14 [Link]("z1:"); [Link]();
15

16 [Link]("Transfere 3 de z1 para z2");


17 for (int p = 1; p < 4; p++) {
18 x = (MeuItem)[Link](1);
19 [Link](x);
20 }
21 [Link]("z1:"); [Link]();
22 [Link]("z2:"); [Link]();
23 x = (MeuItem) [Link]("A2");
24 [Link]("Item com chave A2 = " + [Link]);
25 [Link]("Itens removidos de z2:");
26 while (![Link]()) {
27 x = (MeuItem)[Link](1);
28 [Link](" chave = "+ [Link] +
29 " nome = " + [Link]);
30 }
31 }
32 ao e) {
catch ([Link]̧~
33 [Link]("Item n~ao encontrado.");
34 catch ([Link] e) {
35 [Link]("Espaço esgotado.");}
36 }
37 }
38

E o resultado impresso é o da Fig.4.9.


4.5. LISTAS MÁQUINA DE ESTADO 107

4.5 Listas máquina de estado

Uma lista pode ser vista como como máquina de estado na qual
suas operações podem causar mudanças no estado representado
por sua estrutura interna.
Considere, por exemplo, uma lista formada por um conjunto de
células encadeadas, como a descrita na Seção 4.4, em que uma delas
é definida como elemento corrente, sobre o qual suas operações são
realizadas. Nesse tipo de lista, inserções em lista são sempre feitas
à direita do corrente, o qual também denota a célula candidata à
remoção.
A noção de estado deve ser do conhecimento do usuário, mas de
uma forma abstrata, desprovida dos detalhes de implementação,
ligados ao estado concreto, conforme ilustra a Fig. 4.21.

Estado Abstrato Estado Concreto

Figura 4.21 Estados de Objetos de uma Lista


108 CAPÍTULO 4. LISTAS

As operações aplicáveis a um tipo com a estrutura de estado da


lista da Fig. 4.21 são as seguintes:
• public boolean vazia():
informa se a lista está vazia.
• public Item pesquise(String chave):
pesquisa a lista pela célula que contém a chave dada. A célula
encontrada torna-se a célula corrente.
• public Item valorDoCorrente() throws SemAtivo:
retorna o valor armazenado na célula corrente. Uma exceção
é levantada se a lista estiver vazia.
• public void mudeValorDoCorrente(Item item) throws SemAtivo:
muda o valor do item armazenado na célula corrente.
• public void insira(Item item):
cria uma nova célula com o item fornecido pelo parâmetro e
a insere na estrutura. Se a lista está vazia, insere essa célula
no inı́cio da estrutura, senão insere a nova célula à direita da
célula corrente. Em qualquer caso, a célula inserida passa a
ser a corrente.
• public Item retireCorrente():
se a lista estiver vazia nada faz, senão remove a célula corrente.
Se a célula removida era a última, a primeira célula, se houver,
torna-se a célula corrente.
• public void imprimaChaves():
imprime as chaves de todas as células.
Na implementação de uma lista de itens como uma máquina de
estado, definem-se os seguintes apontadores:
• primeiro: aponta para primeira célula, se houver.
• ativo: identifica célula corrente, se houver.
• anterior: aponta para célula anterior à corrente, se houver.
• seguinte: aponta para célula seguinte à corrente se houver.
4.5. LISTAS MÁQUINA DE ESTADO 109

1 public class ListaM {


2 private static class Célula {
3 Item valor; Célula prox;
4 Célula(Item item)[Link] = item;
5 }
6 private Célula primeiro;
7 private Célula ativo, anterior, seguinte;
8 public static class SemAtivo extends Exception {}
9 public ListaM() {
10 primeiro = null; anterior = null;
11 ativo = null; seguinte = null;
12 }
13 public boolean vazia() {return (primeiro == null);}
14 public Item pesquise(String chave) {...}
15 public Item valorDoCorrente() throws SemAtivo {...}
16 public void mudeValorDoCorrente(Item item)
17 throws SemAtivo{...}
18 public void insira(Item item) {...}
19 public Item retireCorrente() {...}
20 public void imprimaChaves() {...}
21 }

Listagem 4.22 Tipo Abstrato ListaM

O tipo ListaM está definido na Listagem 4.22. A operação


valorDoCorrente levanta uma exceção se a lista estiver vazia, senão
retorna o valor associado ao item armazenado na célula corrente.
1 public Item valorDoCorrente() throws SemAtivo {
2 if (vazia()) throw new SemAtivo();
3 return [Link];
4 }

Listagem 4.23 Operação [Link]

A operação mudaValorDoCorrente, definida na Listagem 4.24, atu-


aliza o valor na célula corrente se a lista não estiver vazia.
110 CAPÍTULO 4. LISTAS

1 public void mudaValorDoCorrente(Item item) throws SemAtivo {


2 if (vazia()) throw new SemAtivo();
3 [Link] = item;
4 }

Listagem 4.24 Operação [Link]

A operação pesquisa altera o corrente se a busca tiver sucesso.

1 public Item pesquise(String chave) {


2 Célula a = null;
3 if (primeiro == null) return null;
4 for (Célula p=primeiro; p!=null; a=p, p=[Link]) {
5 if ([Link](chave)) {
6 anterior = a; ativo = p; seguinte = [Link];
7 return [Link];
8 }
9 }
10 return null;
11 }

Listagem 4.25 Operação [Link]

A operação insira da Listagem 4.26 define um novo corrente.

1 public void insira(Item item) {


2 Célula célula = new Célula(item);
3 if (vazia()) {
4 primeiro = célula; anterior = null;
5 ativo = primeiro; seguinte = null;
6 return;
7 }
8 Cé[Link] = seguinte; [Link] = célula;
9 anterior = ativo; ativo = célula;
10 }

Listagem 4.26 Operação [Link]


4.5. LISTAS MÁQUINA DE ESTADO 111

A operação retireCorrente da Listagem 4.27 nada faz se a lista


estiver vazia, senão remove a célula corrente, a qual passa a ser a
que seguia a célula removida ou então a primeira célula da estru-
tura, no caso de a célula removida ter sido a última da lista. No
caso de a lista ficar vazia, corrente fica indefinido.

1 public Item retireCorrente() {


2 if (vazia()) return null;
3 ativo = seguinte;
4 if (ativo != null) seguinte = [Link];
5 [Link] = ativo;
6 if (ativo == null) {
7 ativo = primeiro; anterior = null;
8 if (ativo != null) seguinte = [Link];
9 else seguinte = null;
10 }
11 return [Link];
12 }

Listagem 4.27 Operação [Link]

A operação imprimaChaves percorre toda a lista, desde seu inı́cio,


imprimindo as chaves dos itens armazenados.

1 public void imprimaChaves() {


2 for (Célula p = primeiro ; p != null ; p = [Link]) {
3 [Link]([Link] + " ");
4 }
5 [Link]();
6 }

Listagem 4.28 Operação [Link]

O programa de teste de ListaM exercita as estruturas em pontos


cruciais para ajudar a compreender seu funcionamento.
112 CAPÍTULO 4. LISTAS

1 public class UsaListaM {


2 public static void main(String[] args) {
3 ListaM q = new ListaM();
4 MeuItem x, itens[] = new MeuItem[7];
5 for (int i = 0; i < 7; i++)
6 itens[i] = new MeuItem("A" + i,"B" + i);
7 [Link]("Itens:");
8 for (int i = 0; i < 7; i++) [Link](
9 "(" + itens[i].chave + "," + itens[i].nome + ")");
10 [Link]();
11 try {
12 [Link]("Cria lista q");
13 for (MeuItem item:itens) [Link](item);
14 [Link]("q: "); [Link]();
15 [Link]("Corrente: " +
16 [Link]().chave);
17 [Link]("Pesquisa " + itens[1].chave);
18 x = (MeuItem)[Link](itens[1].chave);
19 if (!(x==null))
20 [Link](itens[1].nome + " encontrado");
21 [Link](
22 "Corrente: " + [Link]().chave);
23 [Link]("Insere X1 após corrente");
24 [Link](new MeuItem("X1","Y1"));
25 [Link]("q: "); [Link]();
26 [Link](
27 "Corrente: " + [Link]().chave);
28 [Link](
29 "Muda valor do corrente para " + "X2");
30 [Link](new MeuItem("X2","Y2"));
31 [Link]("q: "); [Link]();
32 [Link](
33 "Corrente: " + [Link]().chave);
34 [Link](
35 "Remova corrente que n~
ao é último");
36 x = (MeuItem)[Link]();
4.5. LISTAS MÁQUINA DE ESTADO 113

37 [Link]("q: "); [Link]();


38 [Link](
39 "Corrente: " + [Link]().chave);
40 [Link]("Pesquisa A7");
41 if (!([Link]("A7") == null))
42 [Link]("Item n~ ao encontrado");
43 [Link](
44 "Corrente: " + [Link]().chave);
45 [Link]("Pesquisa A7");
46 if (!([Link]("A6")==null))
47 [Link]("Item n~ ao encontrado");
48 [Link](
49 "Corrente: " + [Link]().chave);
50 [Link](
51 "Remova corrente que é último");
52 x = (MeuItem)[Link]();
53 [Link]("q: "); [Link]();
54 [Link](
55 "Corrente: " + [Link]().chave);
56

57 } catch([Link] e) {
58 [Link]("Lista vazia");}
59 }
60 }

O programa acima produz o seguinte resultado:


Itens:(A0,B0)(A1,B1)(A2,B2)(A3,B3)(A4,B4)(A5,B5)(A6,B6)
Cria lista q
q: A0 A1 A2 A3 A4 A5 A6
Corrente: A6
Pesquisa A1
B1 encontrado
Corrente: A1
Insere X1 após corrente
q: A0 A1 X1 A2 A3 A4 A5 A6
Corrente: X1
Muda valor do corrente para X2
114 CAPÍTULO 4. LISTAS

q: A0 A1 X2 A2 A3 A4 A5 A6
Corrente: X2
Remova corrente que n~ao é último
q: A0 A1 A2 A3 A4 A5 A6
Corrente: A2
Pesquisa A7
Corrente: A2
Pesquisa A7
Item n~
ao encontrado
Corrente: A6
Remova corrente que é último
q: A0 A1 A2 A3 A4 A5
Corrente: A0

4.6 Listas duplamente encadeadas

O tipo abstrato ListaDE, de listas duplamente encadeadas, é mo-


delado por células que possuem três campos: o item com a in-
formação armazenada e os apontadores para a célula da esquerda
e a da direita.
Uma implementação de ListaDE circular nos dois sentidos e com
nodo ou célula-cabeça é ilustrada pela Fig. 4.29.

Figura 4.29 Exemplo de UsaListaM

Toda lista circular duplamente encadeada com nodo-cabeça tem


as seguintes propriedades:
4.6. LISTAS DUPLAMENTE ENCADEADAS 115

• Uma lista duplamente encadeada vazia somente tem o nodo-


cabeça, o qual deve ser configurado da seguinte forma:

• Se p aponta para uma célula em uma lista circular duplamente


encadeada, então:
p == ([Link]).dir == ([Link]).esq
• O caminhamento na lista pode ser feito em ambas as direções,
para a frente e para trás com a mesma facilidade.
O tipo ListaDE de listas duplamente encadeadas é o definido na
Listagem 4.30.
1 class ListaDE {
2 private static class Nodo {
3 Nodo esq, dir;
4 Item valor;
5 Nodo(Item valor) {[Link] = valor;}
6 }
7 private Nodo L;
8 public ListaDE() {
9 L = new Nodo(null); [Link] = L; [Link] = L;
10 }
11 private Nodo pesquiseNodo(String chave) {...}
12 public Item pesquise(String chave) {...}
13 public Item remova(String chave) {...}
14 public void insira(Item a) {...}
15 public void imprimaChaves() {...}
16 }

Listagem 4.30 Tipo Abstrato ListaDE

A construtora de ListaDE cria uma lista vazia com nodo-cabeça.


116 CAPÍTULO 4. LISTAS

A operação pesquise percorre a lista em busca da chave que lhe


for dada e retorna a referência ao item da primeira célula que con-
tiver a chave, ou retorna null, se a chave dada não for encontrada.
1 private Nodo pesquiseNodo(String chave) {
2 Nodo p = [Link];
3 while ((p != L) && (!([Link]).equals(chave))) {
4 p = [Link];
5 }
6 return p;
7 }
8 public Item pesquise(String chave) {
9 Nodo p = pesquiseNodo(chave);
10 if (p == L) return null;
11 return [Link];
12 }

Listagem 4.31 Operação [Link]

A operação removaChave, definida na Listagem 4.32, localiza a


célula que contém a chave fornecida, e então a remove. O item
da célula removida é retornado. Caso item com a chave dada não
estiver na lista, null é retornado.
1 public Item remova(String chave) {
2 Nodo p = pesquiseNodo(chave);
3 if (p != L) {
4 ([Link]).dir = [Link];
5 ([Link]).esq = [Link];
6 return [Link];
7 }
8 return null;
9 }

Listagem 4.32 Operação [Link]


4.6. LISTAS DUPLAMENTE ENCADEADAS 117

Para ilustrar o funcionamento de remova, suponha que o nodo a


ser removido seja o apontado por p na Fig. 4.33.

Figura 4.33 Exemplo de Remoção em ListaDE

A execução dos comandos 4 e 5 da Listagem 4.32 produz:


• comando 4: ([Link]).dir = [Link];

• comando 5: ([Link]).esq = [Link];

• E o nodo p foi removido e pode ser retornado por remova.


118 CAPÍTULO 4. LISTAS

A operação insira cria uma nova célula com o item fornecido e a


insere à direita do nodo-cabeça, conforme mostra a Listagem 4.34.

1 public void insira(Item a) {


2 Nodo p = new Nodo(a);
3 [Link] = L ;
4 [Link] = [Link];
5 ([Link]).esq = p;
6 [Link] = p;
7 }

Listagem 4.34 Operação [Link]

Para ilustrar o funcionamento de insira, suponha a seguinte


configuração da lista, onde o nodo p é o alocado na linha 2.

A execução dos comandos 3, 4, 5 e 6 da operação insira está


ilustrado no seguinte passo-a-passo.

• [Link] = L ; [Link] = [Link]; ([Link]).esq = p; [Link] = p;

• [Link] = L ;[Link] = [Link]; ([Link]).esq = p; [Link] = p;


4.6. LISTAS DUPLAMENTE ENCADEADAS 119

• [Link] = L ; [Link] = [Link]; ([Link]).esq = p; [Link] = p;

• [Link] = L ; [Link] = [Link]; ([Link]).esq = p; [Link] = p;

E a operação imprimachaves é a da Listagem 4.35.


1 public void imprimaChaves() {
2 Nodo p = [Link];
3 while ((p != L)) {
4 [Link](" " + [Link]);
5 p = [Link];
6 }
7 [Link]();
8 }

Listagem 4.35 Operação [Link]


120 CAPÍTULO 4. LISTAS

O programa de teste de ListaDE é o seguinte:

1 public class UsaListaDE {


2 public static void main(String[] args) {
3 ListaDE q = new ListaDE();
4 Item b;
5 MeuItem x, itens[] = new MeuItem[7];
6 for (int i = 0; i < 7; i++)
7 itens[i] = new MeuItem("A" + i,"B" + i);
8 [Link]("Itens:");
9 for (int i = 0; i < 7; i++) [Link](
10 "(" + itens[i].chave + "," + itens[i].nome + ")");
11 [Link]();
12 [Link]("Cria lista q");
13 for (MeuItem a:itens) [Link](a);
14 [Link]("q: "); [Link]();
15 [Link]("Pesquisa A3");
16 b = [Link]("A3");
17 if (b == null)
18 [Link]("A3 n~
ao encontrado");
19 else [Link]("A3 encontrado");
20 [Link]("Pesquisa A30");
21 b = [Link]("A30");
22 if (b == null)
23 [Link]("A30 n~ao encontrado");
24 else [Link]("A30 encontrado");
25 [Link]("Insere X1");
26 [Link](new MeuItem("X1","Y1"));
27 [Link]("q: "); [Link]();
28 [Link]("Remova A1");
29 [Link]("A1");
30 [Link]("q: "); [Link]();
31 [Link]();
32 }
33 }
4.7. EXERCÍCIO COM LISTAS ENCADEADAS 121

E o resultado do programa UsaListaDE é o seguinte:


Itens:(A0,B0)(A1,B1)(A2,B2)(A3,B3)(A4,B4)(A5,B5)(A6,B6)
Cria lista q
q: A0 A1 A2 A3 A4 A5 A6
Pesquisa A3
A3 encontrado
Pesquisa A30
A30 n~
ao encontrado
Insere X1
q: A0 A1 A2 A3 A4 A5 A6 X1
Remova A1
q: A0 A2 A3 A4 A5 A6 X1

4.7 Exercı́cio com listas encadeadas

Nesta seção, apresenta-se a execução passo-a-passo do algoritmo


da Listagem 4.36, o qual, dado uma lista simplesmente encadeada
como

inverte todos os seus apontadores, produzindo uma lista como

Suponha que a lista encadeada seja de células da forma:


class Célula {Item valor; Célula prox;}
122 CAPÍTULO 4. LISTAS

Listagem 4.36 apresenta o algoritmo de inversão dos apontadores


de uma lista apontada por t.

1 public Célula inverta(Célula t){


2 Celula p, q, r;
3 if (t == null) return;
4 p = t; q = [Link];
5 if (q == null) return;
6 do {
7 r = [Link];
8 [Link] = p;
9 p = q;
10 q = r;
11 } while (r != null);
12 [Link] = nil;
13 t = p;
14 return t;
15 }

Listagem 4.36 Algoritmo de Inverte Listas

A execução de inverta(t) até a linha 5, inclusive, para a lista


acima apontada por t produz a seguinte configuração:

e, a partir desse ponto, os apontadores começam a ser invertidos,


como mostra o passo-a-passo do seguinte trecho de programa, onde
o comando em vermelho é o último executado:
• Inicialmente, tem-se
do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);
4.7. EXERCÍCIO COM LISTAS ENCADEADAS 123

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);


124 CAPÍTULO 4. LISTAS

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);


4.7. EXERCÍCIO COM LISTAS ENCADEADAS 125

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);


126 CAPÍTULO 4. LISTAS

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);

• do{r = [Link]; [Link] = p; p = q; q = r;} while (r!=null);


4.7. EXERCÍCIO COM LISTAS ENCADEADAS 127

O algoritmo invert(t) é finalizado pelos comandos das linhas 12


e 13 da Listagem 4.36, que produzem o seguinte efeito:

• [Link] = nil; t = p;

• [Link] = nil; t = p;

• [Link] = nil; t = p;

• E a lista apontada por t está invertida.


128 CAPÍTULO 4. LISTAS

4.8 Conclusão

Interfaces de tipos abstratos de dados não devem depender da sua


representação interna. A escolha entre uma alocação sequencial ou
encadeada não pode ter efeito na interface do tipo lista, havendo
contudo possı́veis implicações no desempenho da operações.
Certamente, toda escolha apresenta vantagens e desvantagens.
Uma implementação de listas por meio de arranjos tem como prin-
cipal vantagem a economia de memória devido ao fato de não haver
necessidade de apontadores para encadear os itens. Por outro, a
estrutura sequencial eleva o custo para inserir ou retirar itens da
lista, devido a necessidade de deslocamento de itens. Em aplicações
em que não existe previsão quanto ao crescimento da lista, a uti-
lização de arranjos requer a implementação de algum mecanismo
para expandir a estrutura interna quando necessário.
No caso de listas com apontadores, tem-se a vantagem de ser
possı́vel, a custo constante, inserir ou retirar itens do meio da lista.
Isso é um aspecto importante quando os elementos da lista têm
que ser mantidos em ordem. E em aplicações em que não existe
previsão quanto ao crescimento da lista é conveniente usar lis-
tas encadeadas por apontadores, porque neste caso o tamanho
máximo da lista não precisa ser definido a priori. A principal des-
vantagem do uso de apontadores em relação ao de arranjos é o
consumo de memória extra para armazenar esses apontadores.
Outros pontos a ser observados quanto à escolha da estrutura
interna estão relacionados com o desempenho das operações re-
querido em dada aplicação. Por exemplo, lista com apontadores
favorecem a inserção e remoção de itens, enquanto uma lista com
arranjos facilita a localização de um item dada sua posição na lista.
Estruturas de Dados Fundamentais Capı́tulo 5: Pilhas 129

Exercı́cios
1. Reimplemente o tipo ListaS para expandir o arranjo de itens
sempre que for necessário para evitar estouro de tamanho da
área alocada.
2. Modifique a implementação de ListaG para criar uma pilha
encadeada genérica.

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre a estru-


tura de dados, e particularmente, do conceito de pilha, é o livro
clássico de Donald Knuth [6], que dá um tratamento formal e or-
ganizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
130 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 5

Pilhas

La semplicità è l’ultima sofisticazione1


Leonardo da Vinci (1452-1519)

Pilhas são listas lineares em que o elemento a ser retirado é


sempre o último que lhe foi inserido, sendo caracterizado por uma
estrutura LIFO2.
Em termos de implementação, uma pilha é uma lista linear em
que todas as inserções, retiradas e geralmente todos os acessos são
feitos em apenas um dos extremos da lista. Graficamente, diz-
se que os itens em uma pilha estão colocados um sobre o outro,
com o item inserido mais recentemente localizado no topo, e o
item da lista inserido menos recentemente, no fundo da pilha. Um
modelo intuitivo de uma pilha é o de um monte de pratos em uma
prateleira, no qual a prudência recomenda inserir e retirar um prato
por vez e somente a partir do seu topo.
Pilhas são extensivamente usadas em Computação para mode-
lar, por exemplo, processamento de estruturas aninhadas de pro-
fundidade imprevisı́vel, controle de sequências de chamadas de sub-
programas, avaliação de expressões aritméticas, tratamento de re-
cursivades e caminhamento em estruturas de dados do tipo árvores.
1
Simplicidade é o máximo em sofisticação
2
“LIFO é abreviatura de last-in-first-out.

131
132 CAPÍTULO 5. PILHAS

Um tipo abstrato de dados que implemente uma pilha de objetos


de um certo tipo T deve conter pelo menos as seguintes operações:
• public boolean vazia():
retorna true se a pilha estiver vazia, senão, false.
• public void empilhe(T x):
insere o item x no topo da pilha.
• public T desempilhe():
devolve o item do topo da pilha, retirando-o da pilha.

As duas representações da estrutura de dados mais utilizadas


na implementação de pilhas são: alocação sequencial por meio de
arranjos e alocação encadeada por apontadores.

5.1 Pilhas sequenciais

O tipo PilhaDeString da Listagem 5.1 usa um arranjo de objetos


do tipo String para armazenar os itens empilhados.
1 public class PilhaDeString {
2 private int máximo;
3 private int topo = -1;
4 private String[] info;
5 public static class ExMax extends Exception {}
6 public static class ExMin extends Exception {}
7 public PilhaDeString(int tamanho) {
8 máximo = (tamanho <= 1)? 0 : tamanho - 1;
9 item = new String[máximo + 1];
10 }
11 public void empilhe(String v) throws ExMax {...}
12 public String desempilhe() throws ExMin {...}
13 public boolean vazia() {...}
14 }

Listagem 5.1 Tipo Abstrato PilhaDeString


5.1. PILHAS SEQUENCIAIS 133

As operações de PilhaDeString estão detalhadas na Listagem 5.2.


1 public void empilhe(String v) throws ExMax {
2 if (topo == máximo) throw new ExMax();
3 info[++topo] = v;
4 }
5 public String desempilhe() throws ExMin {
6 if(vazia()) throw new ExMin();
7 return info[topo--];
8 }
9 public boolean vazia() {
10 return (topo == -1);
11 }

Listagem 5.2 Operações empilha, desempilha e vazia

O seguinte programa de teste é executado passo-a-passo a seguir.

1 public class UsaPilhaDeString {


2 public static void main(String[] args) throws ... {
3 PilhaDeString z = new PilhadeString(1001);
4 [Link]("X1"); [Link]("X2"); ...; [Link]("Xk");
5 [Link]();
6 }
7 }

A operação [Link]("X1") tem o seguinte efeito:


Antes:

Depois:
134 CAPÍTULO 5. PILHAS

A operação [Link]("X2") tem o seguinte efeito:


Antes:

Depois:

As operações [Link]("X3"); ...; [Link]("Xk") de empi-


lhamento em sequência de "X3" até "Xk" tem o seguinte efeito:
Antes:

Depois:

E a operação int y = [Link]()


Antes:

Depois:
5.2. PILHAS SEQUENCIAIS HETEROGÊNEAS 135

5.2 Pilhas sequenciais heterogêneas

O tipo PilhaDeObj da Listagem 5.3 usa um arranjo de Objects.

1 public class PilhaDeObj {


2 private int máximo;
3 private Object[] info;
4 private int topo = -1;
5 public class ExMax extends Exception {}
6 public class ExMin extends Exception {}
7 public PilhaObj(int tamanho) {
8 máximo = (tamanho <= 1)? 0 : tamanho-1;
9 info = new Object[máximo+1];
10 }
11 public void empilhe(Object v) throws ExMax {
12 if (topo == máximo) throw new ExMax();
13 info[++topo]= v;
14 }
15 public Object desempilhe( ) throws ExMin {
16 if(vazia()) throw new ExMin();
17 Object x = info[topo--];
18 return x;
19 }
20 public boolean vazia() {
21 return (topo == -1);
22 }
23 }

Listagem 5.3 Tipo Abstrato PilhaDeObj

E como PilhaDeObj trabalha com itens de qualquer tipo, cabe ao


usuário fazer as conversões de tipo que se fizerem necessárias dos
valores desempilhados, como em:
PilhaDeObj z = new PilhaDeObj[1000];
MeuItem a = new MeuItem(...); ...
[Link](a); ...; a = (MeuItem)[Link](); ...
136 CAPÍTULO 5. PILHAS

5.3 Pilhas encadeadas

Em uma alocação encadeada, a pilha é implementada por meio de


células que são conectadas na medida em que forem sendo inseridas
na estrutura, de forma que a célula inserida há mais tempo é dita
ser o fundo da pilha, e a última inserida é o seu topo, como ilustrado
na seguinte figura:

na qual cada célula contém uma informação, no exemplo, x1, x2 e


x3, e um apontador para célula que estiver abaixo dela na pilha.
A Listagem 5.4 implementa uma pilha de Objects por meio de
uma estrutura encadeada.
1 public class PilhaE {
2 private static class Célula {
3 Object info;
4 Célula prox;
5 Célula(Object info) {[Link] = info;}
6 }
7 private Célula topo;
8 public static class ExMax extends Exception {}
9 public static class ExMin extends Exception {}
10 public PilhaE() { }
11 public boolean vazia() {
12 return (topo == null);
13 }
14 public void empilhe(Object v) throws ExMax {...}
15 public Object desempilhe() throws ExMin {...}
16 }

Listagem 5.4 Tipo Abstrato PilhaE

Note que testes e conversões de tipo são necessários para se ter


5.3. PILHAS ENCADEADAS 137

acesso aos componentes de itens desempilhados, como mostra o


seguinte código:
PilhaE z = new PilhaE();
Object x;
MeuItem a = new MeuItem(...); ...
[Link](a); ...;
x = [Link]();
if ((MeuItem) x) {a = (MeuItem) x; ...; } ...
As operações empilhe e desempilhe estão definidas na Lista-
gem 5.5.
1 public void empilhe(Object v) throws ExMax
2 Célula q;
3 try {q = new Célula(v);}
4 catch(Exception e) {throw new ExMax();}
5 [Link] = topo;
6 topo = q;
7 }
8 public Object desempilhe() throws ExMin {
9 Célula q;
10 if(vazia()) throw new ExMin();
11 q = topo;
12 topo = [Link];
13 return [Link];
14 }

Listagem 5.5 Operações de PilhaE

Execução passo-a-passo de algumas operações de PilhaE:


1. Operação PilhaE z = new PilhaE():

2. Operação [Link](x1):
• q = new Célula(v); [Link] = topo; topo = q;
138 CAPÍTULO 5. PILHAS

• q = new Célula(v); [Link] = topo; topo = q;

• q = new Célula(v); [Link] = topo; topo = q;

3. Operação [Link](x2):
• q = new Célula(v); [Link] = topo; topo = q;

• q = new Célula(v); [Link] = topo; topo = q;

• q = new Célula(v); [Link] = topo; topo = q;


5.3. PILHAS ENCADEADAS 139

4. Operação [Link](x3):
• q = new Célula(v); [Link] = topo; topo = q;

• q = new Célula(v); [Link] = topo; topo = q;

• q = new Célula(v); [Link] = topo; topo = q;

5. Operação x = [Link]() inicia-se com


140 CAPÍTULO 5. PILHAS

• q = topo; topo = [Link]; return [Link];

• q = topo; topo = [Link]; return [Link];

• q = topo; topo = [Link]; return [Link];


E x3 é retornado.

5.4 Pilhas genéricas

O tipo abstrato de dados PilhaDeObj definido na Seção 5.2 opera


com qualquer tipo de objeto, mas demanda de seu usuário ações
para as devidas conversões de tipos sempre que um item for desem-
pilhado, para se ter acesso aos seus componentes. Muitas vezes,
essa flexibilidade não é necessária, e as operações de testes de tipo
e conversões associadas tendem a poluir o código.
Uma implementação alternativa que força todos os itens de uma
pilha a ser do mesmo tipo é a pilha genérica, implementada via
classes genéricas ou parametrizadas, como mostra a Listagem 5.6.
5.4. PILHAS GENÉRICAS 141

1 public class PilhaG<T> {


2 private int máximo;
3 private int topo = -1;
4 private T[] info;
5

6 public static class ExMax extends Exception {}


7 public static class ExMin extends Exception {}
8

9 public PilhaG(int tamanho) {


10 máximo = (tamanho <= 1)? 0 : tamanho-1;
11 info = (T[]) new Object[máximo+1];
12 }
13 public void empilhe(T v) throws ExMax {
14 if (topo == máximo) throw new ExMax();
15 info[++topo]= v;
16 }
17 public T desempilhe() throws ExMin {
18 if(vazia()) throw new ExMin();
19 return info[topo--];
20 }
21 public boolean vazia() {
22 return (topo == -1);
23 }
24 }

Listagem 5.6 Tipos Abstrato PilhaG

O tipo abstrato da Listagem 5.6 representa a pilha como um


arranjo de objetos do tipo T a ser definido no momento do ins-
tanciamento da pilha. Por exemplo, PilhaG<Integer> declara uma
pilha de Integer, enquanto PilhaGS<Double> declara uma de objetos
do tipo Double.
O programa da Listagem 5.7 ilustra o uso de PilhaGS<T> para
T instanciado por Integer e Double em uma mesma aplicação.
Ressalta-se que, nesse exemplo, conversões de tipo foram omitidas
142 CAPÍTULO 5. PILHAS

porque o compilador gera código para automaticamente converter


Integer em int e Double em double.

1 public class UsaPilhaG {


2 public static void main(String[] args) {
3 PilhaG<Integer> p = new PilhaG<Integer>(10);
4 PilhaG<Double> q = new PilhaG<Double>(10);
5 int a, x[] = {1,2,3,4,5,6,7,8,9,10};
6 double b, y[] = {1.0,2.0,3.0,4.0,5.0,6.0};
7 try {
8 for(int i : x) [Link](i);
9 for(double d : y) [Link](d);
10

11 [Link]("p:");
12 while(![Link]()) {
13 a = [Link]( );
14 [Link](" " + a);
15 }
16

17 [Link]("\nq: ");
18 while(![Link]()) {
19 b = [Link]( );
20 [Link](" " + b);
21 }
22

23 [Link]();
24 } catch(Exception e) [Link]("Erro");
25 }
26 }

Listagem 5.7 Usuário de PilhaG<T>

O resultado impresso pelo programa acima é o seguinte:


p: 10 9 8 7 6 5 4 3 2 1
q: 6.0 5.0 4.0 3.0 2.0 1.0
5.5. CONCLUSÃO 143

5.5 Conclusão

Exercı́cios
1. Reimplemente o tipo PilhaS para expandir o arranjo de itens
sempre que for necessário para evitar estouro de tamanho da
área alocada.
2. Modifique a implementação de PilhaG para criar uma pilha
encadeada genérica.

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre a estru-


tura de dados, e particularmente, do conceito de pilha, é o livro
clássico de Donald Knuth [6], que dá um tratamento formal e or-
ganizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
144 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 6

Filas

Il semble que la perfection soit atteinte


non quand il n’y a plus rien à ajouter,
mais quand il n’y a plus rien à retrancher1
Antoine de Saint-Exupéry (1900-1944)

Uma fila é uma lista linear em que todas as inserções são rea-
lizadas em um extremo da lista, e todas as retiradas, e todos os
acessos, são realizados no outro extremo da lista. Filas são por isso
chamada de estruturas FIFO2.
Filas são utilizadas quando deseja-se processar itens de acordo
com a regra de que o primeiro que chega deve ser o primeiro a ser
atendido. Sistemas operacionais utilizam filas para regular a ordem
na qual tarefas devem receber processamento e recursos devem ser
alocados.
As operações que caracterizam o tipo abstrato de dados Fila de
itens de um tipo T são as seguintes:
• void insira(T x): insere o item x no fim da fila.
• T retire(): retira o item do inı́cio da fila e retorna seu valor.
• boolean houveErro(): retorna true se última operação reali-
zada na fila incorreu em erro; false, se teve sucesso
1
Parece que a perfeição é obtida não quando nada há a acrescentar, mas quando nada pode ser removido.
2
FIFO é abreviatura de First-In-First-Out.

145
146 CAPÍTULO 6. FILAS

• Status status(): retorna a situação da fila após a última operação


realizada, onde o tipo Status é definido por
enum Status {OK,OVERFLOW,UNDERFLOW}
• boolean vazia(): retorna true se a fila estiver vazia; senão
retorna false.

6.1 Filas sequenciais

Uma implementação muito simples de uma fila é por meio de um


arranjo no qual seus elementos são armazenados, sendo o elemento
no inı́cio da fila indicado pelo indexador F e o do fim, por T, como
na seguinte figura:

A operação de inserir ou enfileirar um item faz a parte de trás da


fila deslocar-se para a direita, e a operação remover ou desenfileira
faz a parte da frente da fila mover-se na mesma direção. Essas
operações fazem a fila caminhar pelo arranjo que a representa,
ocupando novos espaços na sua parte de trás e descartando espaços
na sua parte da frente. E consequentemente, com as inserções e
retiradas de itens, a fila desloca-se de encontro ao limite do espaço
da memória alocada para o arranjo.
Uma solução para esse problema é tratar o arranjo como um
cı́rculo, onde a primeira posição segue a última. Assim para enfi-
leirar um item basta mover o apontador T uma posição no sentido
horário, e, para desenfileirar um item, basta mover o apontador F
uma posição no mesmo sentido, sempre considerando que após a
última posição do arranjo vem sua primeira posição.
6.1. FILAS SEQUENCIAIS 147

Isso resolve o problema citado acima, mas é preciso tomar um


pequeno cuidado na implementação dessa solução para evitar a
ambiguidade das condições de indicação de fila cheia e de fila vazia,
nas quais os apontadores F e T apontam para a mesma posição no
vetor quando a fila está vazia e também quando a fila estiver cheia,
como ilustrado pela seguinte figura:

Para resolver esse pequeno problema, basta definir que a condição


de fila vazia seja T==F, e a de fila cheia, F==T+1.

O preço a pagar por essa solução é que a posição apontada por


T seja sempre vazia.
148 CAPÍTULO 6. FILAS

Listagem 6.1 mostra a implementação do tipo FilasDeStringS,


usando arranjo e circularidade, e na qual os apontadores T e F são
implementados pelos campos trás e frente, respectivamente.

1 public class FilaDeStringS{


2 private String[] info;
3 private int frente, trás;
4 private int max;
5 private Status últimaOp;
6 public enum Status {OK, ERRO}
7 public static class Info {}
8 public FilaDeStringS(int tamanho) {
9 max = (tamanho <= 1) ? 0 : tamanho - 1;
10 info = new info[max + 1];
11 frente = 0; trás = 0;
12 }
13 public boolean vazia( ) {
14 return Frente == Tras;
15 }
16 public Status getStatus( ) {
17 return UltimaOp;
18 }
19 public void insira(String x) {...}
20 public Item retire() {...}
21 }

Listagem 6.1 Tipo Abstrato FilaDeString

Objetos do tipo FilaDeStringS tem a seguinte estrutura interna:

E a Listagem 6.2 mostra a implementação das operações insira,


que instala um novo item no fim fila, e a retire, que remova um
6.1. FILAS SEQUENCIAIS 149

item do seu inı́cio.


1 public void insira(String x) {
2 int seguinte = (trás = max) ? 0 : (trás + 1);
3 if (frente == seguinte)
4 últimaOp = [Link];
5 else {
6 info[trás] = x;
7 trás = seguinte;
8 últimaOp = [Link];
9 }
10 }
11 public String retire(){
12 if (vazia()) {
13 últimaOp = [Link];
14 return null;
15 } else {
16 String x = item[frente];
17 frente = (frente == max) ? 0 : frente + 1;
18 últimaOp = OK;
19 return x;
20 }
21 }

Listagem 6.2 Operações [Link] e [Link]

Execução passo-a-passo
O comando FilaDeStringS z = new FilaDeStringS(12) produz a se-
guinte configuração:

Supondo que a fila tenha espaço para a inserção de 11 novos


itens, os passos de inserção e remoção desses itens são os seguintes:
150 CAPÍTULO 6. FILAS

• Operação [Link]("x1"):

• Operação [Link]("x2"):

• Operação [Link]("x3"):

• Operação [Link]("x4");...;[Link]("x11"):

• Operação x = [Link]():

• Operação x = [Link]():

• Operação x = [Link]():
6.2. FILAS ENCADEADAS 151

• Operação x = [Link]():

• Operação [Link]("x12"):

• Operação [Link]("x13"):

• Operação [Link]("x14"):

6.2 Filas encadeadas

Como em listas e pilhas, o uso de uma estrutura de células enca-


deadas para implementar filas libera o programador de prever o
número máximo de itens que se pode armazenar. O uso de célula-
cabeça também facilita a implementação das operações, tornando-
as independentes do fato de as filas estarem ou não vazias. Quando
a fila está vazia, frente e trás apontam para a célula-cabeça:
152 CAPÍTULO 6. FILAS

A Listagem 6.3 contém a implementação de uma fila de cadeias


de caracteres com células encadeadas e com célula-cabeça.

1 public class FilaDeStringE {


2 private Celula frente, trás;
3 private Status últimaOp;
4 public enum Status {OK, ERRO}
5 private static class Celula {
6 String info;
7 Celula prox;
8 }
9 public FilaDeStringE() {
10 frente = new Célula();
11 trás = frente;
12 últimaOp = [Link];
13 }
14 public boolean vazia() { return tras == frente;}
15 public void insira(String x) {...}
16 public String retire() {...}
17 public Status status() {return últimaOp;}
18 }

Listagem 6.3 Tipo Abstrato FilaDeStringE

A seguinte figura exemplifica uma fila encadeada com os itens


"x1", "x2" e "x3", segundo a implementação da Listagem 6.3.

Para enfileirar um novo item, deve-se criar uma célula nova; nela
colocar o novo item e inseri-la após a célula apontada por trás, ou
seja, no fim da fila. E para desenfileirar um item, deve-se recuperar
a informação armazenada na célula apontada pela célula-cabeça e
6.2. FILAS ENCADEADAS 153

retirar a célula-cabeça da lista, tornando a célula que continha a


informação retirada a nova célula-cabeça, conforme detalhado nas
operações insira e remova da Listagem 6.4.

1 public void insira(String x){


2 [Link] = new Celula( );
3 tras = [Link];
4 [Link] = x;
5 ultimaOp = [Link];
6 }
7 public String retire() {
8 String x = null;
9 if (vazia()) ultimaOp = [Link];
10 else {
11 frente = [Link];
12 x = [Link];
13 ultimaOp = [Link];
14 }
15 return x;
16 }

Listagem 6.4 Operações insira e remova de FilaDeStringE

Execução passo-a-passo

Para ilustrar o funcionamento da fila encadeada, considere a criação


de uma fila de tipo FilaDeStringE z = new FilaDeStringE(), a qual
gera a seguinte estrutura que representa uma fila vazia:
154 CAPÍTULO 6. FILAS

E a inserção e a remoção de alguns itens nessa fila estão ilus-


tradas pelas seguintes transformações na representação interna da
fila:
• Operação [Link]("x1"):

• Operação [Link]("x2"):

• Operação [Link](x3):

• Operação x = [Link]():

onde q é a célula retirada.


6.3. CONCLUSÃO 155

6.3 Conclusão

Filas são extensivamente usadas em Computação para modelar,


por exemplo, a sequência de processos a ser escalonados pelo sis-
tema operacional, o gerenciamento de filas de impressão e o proto-
colo de troca de mensagens entre computadores em uma rede.

Exercı́cios
1. Reimplemente o tipo FilaDeStringS para expandir o arranjo de
itens sempre que for necessário para evitar estouro de tamanho
da área alocada.
2. Modifique a implementação de FilaDeStringE para tratar erros
de inserção e remoção por meio de exceções.

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre a estru-


tura de dados, e particularmente, do conceito de filas, é o livro
clássico de Donald Knuth [6].
Outros textos sobre o assunto são os de autoria de Alfred Aho
[2], Michel Goodrich [13], Roberto Sedgewick [20], Thomas Cormen
[22] e Nivio Ziviani [14].
***
156 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 7

Tabelas de Dispersão

Not everything that counts can be counted,


and not everything that can be counted counts.1
Albert Einstein (1879-1955)

A forma de representação da estrutura interna de uma tabela


tem influência direta no desempenho de suas operações de inserir,
remover e pesquisar itens. Tabelas sequenciais ordenadas permitem
realizar pesquisa binária, de custo logarı́tmico, para localizar seus
elementos, mas, em contrapartida, encarecem inserções e remoções,
pois podem exigir deslocamento de elementos para manter a sua
ordenação. Tabelas organizadas com células encadeadas favorecem
o desempenho das operações de inserção e remoção, ao custo da
operação de pesquisa que tem sempre custo linear.
Uma solução que garante igual eficiência nas operações de inse-
rir, remover ou pesquisar são as chamadas Tabelas de Dispersão,
nas quais os itens são armazenados em posições ou endereços cal-
culados a partir de uma transformação aritmética sobre a chave de
identificação de cada um.
Nesse tipo de tabela, o método de pesquisa por um registro ou
item é constituı́do de duas etapas principais. A primeira consiste
em computar o valor da função de transformação, também
1
Nem tudo que importa pode ser contabilizado, nem tudo que pode ser contabilizado interessa.

157
158 CAPÍTULO 7. TABELAS DE DISPERSÃO

conhecida por função de hashing, a qual transforma a chave de


pesquisa em um endereço na tabela. E na segunda etapa, conside-
rando que duas ou mais chaves podem ser transformadas em um
mesmo endereço de tabela, é necessário implementar um método
para lidar com essas colisões.
Graficamente, uma tabela de dispersão é um arranjo de itens,
cujos ı́ndices podem ser calculados a partir de suas chaves, como
mostra a seguinte figura, onde as letras E, F, C, D e U representam
chaves, e h, a função de transformação:

7.1 Transformação de chaves em endereços

Em princı́pio, não há restrições quanto ao tipo das chaves de iden-


tificação de itens de uma tabela, entretanto, na prática, é muito
comum que chaves sejam cadeias de caracteres, como nomes de
pessoas, ou até mesmo o CPF, que pode ser tratado como uma
cadeia de dı́gitos decimais. Dessa forma, nas implementações aqui
apresentadas, sem perda de generalidade, chaves serão sempre ob-
jetos do tipo String, a não ser quando explicitamente declaradas
7.1. TRANSFORMAÇÃO DE CHAVES EM ENDEREÇOS 159

diferentemente.
A Listagem 7.1 apresenta a estrutura básica dos itens a ser ins-
talados nas tabelas de dispersão descritas neste capı́tulo, as quais
são objetos do tipo Dispers~ao.
1 public class Item {
2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 }
5 ao {
public interface Dispers~
6 public void insira(Item x);
7 public Item retire(String chave);
8 public Item pesquise(String chave);
9 public void imprimaChaves();
10 }

Listagem 7.1 Interfaces da Tabela de Dispersão

Dada uma tabela de M posições, as chaves devem ser mapeadas


em inteiros dentro do intervalo [0..M-1] por meio de uma função
de transformação, a qual deve ser simples de ser computada, e que
para toda chave de entrada, qualquer uma das saı́das possı́veis seja
igualmente provável de ocorrer.
1 private int hash(String chave) {
2 int k = 0;
3 for (int i = 0; i < [Link](); i++)
4 k += [Link](i);
5 return k % M;
6 }

Listagem 7.2 Função de Transformação de Chaves

A função hash da Listagem 7.2 é a implementação de um dos


métodos mais antigos, e também muito popular, de transformar
uma chave do tipo String em um número inteiro, o qual consiste
160 CAPÍTULO 7. TABELAS DE DISPERSÃO

em somar o valor associado a cada um de seus caracteres, e usar o


resto da divisão dessa soma pelo tamanho M da tabela.
Recomenda-se cuidado na escolha do valor de M, o qual deve
ser um número primo, mas não qualquer primo. Conforme sugere
Donald Knuth [7], devem ser evitados os números primos obtidos
a partir de bi ± j, onde b é a base do conjunto de caracteres (ge-
ralmente b = 64 para BCD, 128 para ASCII, 256 para EBCDIC,
ou 100 para alguns códigos decimais), e i e j são pequenos intei-
ros. Uma boa escolha de M minimiza o número de colisões, isto é,
situações em que chaves diferentes são mapeadas para um mesmo
endereço.
É fato que colisões desse tipo são frequentemente inevitáveis.
Mesmo quando se dispõe de uma função de transformação que
distribua os itens de forma uniforme nas posições da tabela, existe
sempre uma alta probabilidade de haver colisões de endereços para
diferentes chaves, qualquer que seja a função de transformação
usada, e tais colisões têm que ser resolvidas de alguma forma, pois
não se pode ter mais de um item em uma mesma entrada da tabela.
Para demonstrar que, exceto em certas condições muito especi-
ais, colisões são inevitáveis, considere a probabilidade C(n) de, em
um grupo de n pessoas, haver pelo menos uma colisão de dia de
aniversário.
Para calcular de a probabilidade C(n), considerando um ano
tı́pico com 365 dias, tem-se que:

1. O número total de possibilidades de combinação de datas de


aniversários é 365n.

2. A probabilidade p(n) de, em um grupo de n pessoas, não haver


duas ou mais fazendo aniversário no mesmo dia, é dada por:
365.364.363.··· .(365−n+1)
p(n) = 365n
7.2. RESOLUÇÃO DE COLISÕES COM LISTAS ENCADEADAS 161

3. E que C(n) = 1 − p(n).

Valores de C(n) para alguns n são:


n 23 50 60 100
C(n) 50,7 97,0 99,0 99,99

Os valores de C(n) presentes nessa tabela sugere que existe alta


probabilidade de colisões quando se faz o preenchimento de uma
tabela de 365 posições por melhor que seja a função hash que define
o endereço de instalação de cada item.
Esse resultado surpreende, pois, nascimentos, em geral, ocor-
rem de forma independente entre si, sendo razoável esperar uma
distribuição uniforme ao longo do ano. Entretanto, veja que 23
nascimentos aleatórios armazenados em uma tabela de tamanho
365, i.e., ocupando apenas 6, 3% do espaço, tem uma probabili-
dade de colisão de endereços acima de 50%. E se o número de
nascimentos for 100, a taxa de ocupação passa a apenas 27, 4%,
mas a probabilidade de colisão sobe para 99, 9%!
Portanto, colisões em tabelas de dispersão podem ser considera-
das inevitáveis e devem ser devidamente tratadas, haja vista que
dois ou mais itens não podem ocupar o mesmo lugar.
Há pelo menos duas formas clássicas de resolver colisões em
tabelas de dispersão: Listas Encadeadas e Endereçamento Aberto,
que são apresentadas a seguir.

7.2 Resolução de colisões com listas encadeadas

Uma forma simples de resolver ascolisões é construir uma lista


linear encadeada de registros para cada endereço da tabela. As-
sim, todas as chaves com mesmo endereço são encadeadas em uma
mesma lista linear.
162 CAPÍTULO 7. TABELAS DE DISPERSÃO

Se a função de hash for boa, i.e., uma que garanta um bom


espalhamento de endereço, a maioria das listas de colisão terá no
máximo um item. E a pior função hash será aquela em que uma
das listas tem todos os itens, e as demais estão vazias.
Se, por exemplo, as chaves representadas pelas letras A, B, C, D,
E, F, G e H sejam tais que hash(A) = 0, hash(E) = 1, hash(B) = 5,
hash(F) = 5, hash(C) = 2, hash(G) = 2, hash(D) = 3 e hash(H) = 7,
a inserção dessas chaves numa tabela de dispersão produz:

Nesse método, o tamanho da tabela de dispersão pode ser me-


nor que o número de itens a ser instalados, os quais efetivamente
ocupam células encadeadas vinculadas ao que é chamado de vetor
de hash, que no exemplo acima é identificado por T.
O tipo abstrato de dados DicionárioE da Listagem 7.3 imple-
menta uma tabela de dispersão com resolução de colisões via esse
tipo de encadeamento. Nessa implementação, a função hash é a
definida na Listagem 7.2, e a tabela de dispersão passa a ser um ar-
7.2. RESOLUÇÃO DE COLISÕES COM LISTAS ENCADEADAS 163

ranjo de apontadores de listas de células encadeadas do tipo Célula.

1 class DicionárioE implements Dispers~ao {


2 private int M;
3 private Célula[] tabela;
4 private static class Célula {
5 public Item info;
6 public Célula prox;
7 public Célula(Item info, Célula prox){
8 [Link] = info;
9 [Link] = prox;
10 }
11 }
12 private int hash(String chave) {
13 int k = 0;
14 for (int i = 0; i < [Link](); i++)
15 k += [Link](i);
16 return k % M;
17 }
18

19 public DicionárioE(int tamanho) {


20 M = (tamanho < 1)? 1 : tamanho;
21 tabela = new Célula[M];
22 }
23 public void insira(Item x) { ... }
24 public Item retire(String chave) { ... }
25 public Item pesquise(String chave) { ... }
26 public void imprimaChaves() {...}
27 }

Listagem 7.3 Tipo Abstrato DicionárioE

A tabela de dispersão é representado pelo vetor tabela de apon-


tadores para objetos do tipo Célula, inicializado com o valor null,
que indica que todas suas entradas estão vazias.
A operação pesquise da Listagem 7.4 calcula o endereço no vetor
164 CAPÍTULO 7. TABELAS DE DISPERSÃO

hash que fornece o endereço da lista de itens onde a chave dada


poderá estar armazenada. Essa operação retorna o endereço do
primeiro objeto do tipo Item que tiver a chave dada, ou, então, se
item com essa chave não for encontrado, retorna null.

1 public Item pesquise(String chave){


2 int k = hash(chave);
3 Celula p = tabela[k];
4 while (p != null) {
5 if ([Link](chave)) return [Link];
6 else p = [Link];
7 }
8 return null;
9 }

Listagem 7.4 Operação Dicioná[Link]

A operação insira da Listagem 7.5 somente insere um novo item


na tabela se não houver um outro com mesma chave já instalado.
A inserção é sempre feita no inı́cio da lista de colisão associada ao
hash da chave.
1 public void insira(Item x) {
2 int k = hash([Link]);
3 Célula p = tabela[k];
4 while (p != null) {
5 if (([Link]).equals([Link])) return;
6 else p = [Link];
7 }
8 p = new Célula(x,tabela[k]);
9 tabela[k] = p;
10 }

Listagem 7.5 Operação Dicioná[Link]

A operação retire da Listagem 7.6 pesquisa a tabela por um


7.2. RESOLUÇÃO DE COLISÕES COM LISTAS ENCADEADAS 165

item com a chave dada, e se esse item for localizado, remove-o da


tabela e retorna seu endereço, senão, retorna null.
1 public Item retire(String chave){
2 int k = hash(chave);
3 Célula p = tabela[k];
4 Célula q = p;
5 while (p != null) {
6 if ([Link](chave)) {
7 if (p == tabela[k]) tabela[k] = [Link];
8 else [Link] = [Link];
9 return [Link];
10 } else { q = p; p = [Link]; }
11 }
12 return null;
13 }

Listagem 7.6 Operação Dicioná[Link]

A operação imprimaChaves da Listagem 7.7 lista todas as chaves


presentes na tabela. Para isso, faz-se uma varredura de toda a
tabela e para cada posição ocupada, percorre as listas de colisão
associada.

Análise de custo
Supondo que todos os itens do conjunto de interesse tenham igual
probabilidade de ser endereçados para qualquer entrada da tabela
de dispersão, o comprimento médio esperado de cada lista encade-
ada para resolver colisões é N/M , onde N representa o número de
itens instalados na tabela, e M o tamanho da tabela.
Nesse contexto, as operações pesquise, insira e retire têm
custo médio O(1+N/M ), onde a constante 1 representa o custo da
função hash para computar o endereço do item na tabela, e N/M ,
o custo para percorrer a lista de colisões associada.
166 CAPÍTULO 7. TABELAS DE DISPERSÃO

1 public void imprimaChaves() {


2 Célula p;
3 for (int k = 0; k < M; k++) {
4 if (tabela[k] != null) {
5 p = tabela[k];
6 while (p != null) {
7 [Link]([Link] + " ");
8 p = [Link];
9 }
10 }
11 }
12 [Link]();
13 }

Listagem 7.7 Operação Dicioná[Link]

Para valores de M próximos de N , i.e., quando a tabela de dis-


persão tem tamanho próximo do número total de itens, ou quando
M > N , o custo dessas operações se torna constante, praticamente
independente de N .

7.3 Resolução de colisões com endereçamento aberto

Quando o número de itens a ser armazenados na tabela puder ser


previamente estimado, então não haverá necessidade de se usar
células fora do arranjo principal da tabela de dispersão para arma-
zenar os itens, os quais podem ser armazenados diretamente nesse
arranjo em vez de se ter apontadores para lista de células contendo
itens. E para resolver colisões usa-se a técnica denominada en-
dereçamento aberto, a qual consiste em utilizar os lugares vazios
na própria tabela para armazenar os itens conflitantes.
Há várias possibilidades para a escolha desses lugares vagos,
sendo a mais simples, que é chamada de hashing linear, a que
7.3. RESOLUÇÃO DE COLISÕES COM ENDEREÇAMENTO ABERTO 167

seleciona a posição vaga hj na tabela pela fórmula:

hj = (h(x) + j) % M , para 1 ≤ j ≤ M − 1

a qual indica que, após o cálculo do hash da chave x, h(x), pesquisa-


se por uma posição vaga nas proximidades de h(x), incrementando
j, respeitando-se os limites da tabela.
Para exemplificar o funcionamento do hashing linear, considere
a inserção em uma tabela de dispersão das chaves representadas pe-
las letras A B C D E F , cujos hashes sejam hash(A)=0, hash(B)=7,
hash(C)=8, hash(D)=0, h(E)=7 e h(F)=4.

• Inicialmente, tem-se a tabela vazia:

• A chave A, cujo hash(A) = 0, foi inserida na posição livre 0.

• a chave B, cujo hash(B) = 7, foi inserida na posição livre 7:

• A chave E, cujo hash(E) = 7 foi inserida na primeira posição


livre subsequente à 7:

• A chave D, cujo hash(D) = 0, foi inserida na primeira posição


168 CAPÍTULO 7. TABELAS DE DISPERSÃO

livre após 0:

• A chave C, cujo hash(C) = 8, foi inserida na posição 2, que


era a primeira livre a partir da posição 8, considerando que a
posição 0 segue a posição 8, pois a tabela é vista como circular:

• A chave F , cujo hash(F) = 4, foi inserida na posição livre 4.

Suponha que o item de chave D deva ser retirado. Nesse caso,


sua posição deve marcada como vazia, e para isso deve receber
uma marca diferente das indicadoras de posições originalmente va-
zias, para assegurar que itens armazenados após itens removidos
continuem acessı́veis.
Como hash(D)=0 e essa posição não contém o item com chave D,
deve-se pesquisar sequencialmente pela chave a partir de 0, que no
caso, será encontrada na posição 1, conforme a seguinte figura:

Antes :

que após sua remoção torna-se:

Depois:
7.3. RESOLUÇÃO DE COLISÕES COM ENDEREÇAMENTO ABERTO 169

onde o amarelo indica item removido, o que garante que uma pes-
quisa pela chave C, para a qual hash(C) = 7, terá sucesso, pois
faz-se-á uma busca nas posições 7, 8, 0, 1 e 2. A posição marcada
em amarelo serve também para indicar que a posição está livre
para inserção de novos itens.
A Listagem 7.8 esquematiza a implementação de uma tabela de
dispersão que resolve colisões por meio de endereçamento aberto.
1 ao {
public class DicionárioL implements Dispers~
2 private int M;
3 private Item[] tabela;
4 private final Item reusável = new Item(" ");
5 private boolean posicaoLivre(int k) {
6 return (tabela[k]==null) || (tabela[k]==reusável);
7 }
8 private int hash(String chave) {
9 int k = 0;
10 for (int i = 0; i < [Link](); i++)
11 k += [Link](i);
12 return k % M;
13 }
14 public DicionárioL(int tamanho) {
15 M = (tamanho < 1)? 1 : tamanho;
16 tabela = new Item[M];
17 }
18 public Item pesquise(String chave) {...}
19 public void insira(Item x) {...}
20 public Item retire(String chave) {...}
21 public void imprimaChaves() {...}
22 }

Listagem 7.8 Tipo Abstrato de Dados DicionárioL

O tipo dos itens que podem ser instalados em objetos DicionárioL


e a respectiva interface dessa classe estão definidos na Listagem 7.1.
O objeto reusável, declarado na linha 4 da classe acima, é usado
170 CAPÍTULO 7. TABELAS DE DISPERSÃO

para marcar posições da tabela que podem ser reusadas.


A operação pesquise, que está detalhada na Listagem 7.9, inicia-
se calculando o hash da chave dada para determinar o ponto inicial
da pesquisa sequencial para localizá-la. Caso a chave não seja
encontrada na tabela, a operação retorna null.

1 public Item pesquise(String chave) {


2 int k, i = 1;
3 k = hash(chave);
4 while ((i <= M-1) && (tabela[k] != null)) {
5 if ((tabela[k] != reusavel) &&
6 (tabela[k].[Link](chave)))
7 return tabela[k];
8 k = (k + 1) % (M-1);
9 i++;
10 }
11 return null;
12 }

Listagem 7.9 Operação Dicioná[Link]

A operação insira da Listagem 7.10 somente instala o item dado


se não houver item com a mesma chave da tabela.

1 public void insira(Item x) {


2 int i = 1, k;
3 k = hash([Link]);
4 while ((i <= M) && !posicaoLivre(k)) {
5 if (tabela[k].[Link]([Link])) return;
6 k = (k + 1) % (M-1);
7 i++;
8 }
9 if (posicaoLivre(k)) tabela[k] = x;
10 }

Listagem 7.10 Operação Dicioná[Link]


7.3. RESOLUÇÃO DE COLISÕES COM ENDEREÇAMENTO ABERTO 171

A operação retire da Listagem 7.11 pesquisa a chave dada na


tabela e retorna o endereço do item se o encontrar, do contrário,
retorna null.

1 public Item retire(String chave) {


2 int k, i = 1;
3 k = hash(chave);
4 while ((i <= M) && (tabela[k] != null)) {
5 if ((tabela[k] != reusável) &&
6 (tabela[k].[Link](chave))) {
7 Item x = tabela[k];
8 tabela[k] = reusável;
9 return x;
10 }
11 k = (k + 1) % (M-1);
12 i++;
13 }
14 return null;
15 }

Listagem 7.11 Operação Dicioná[Link]

A operação imprimaChaves da Listagem 7.12 faz uma varredura


de toda a tabela e imprime as chaves encontradas.

1 public void imprimaChaves() {


2 for (int k = 0; k < M; k++) {
3 if ((tabela[k] != null) && (tabela[k] != reusável)){
4 [Link](tabela[k].chave + " ");
5 }
6 }
7 [Link]();
8 }

Listagem 7.12 Operação Dicioná[Link]


172 CAPÍTULO 7. TABELAS DE DISPERSÃO

Análise de custo

Seja α = N/M o fator de carga da tabela. Conforme demonstrado


por Donald E. Knuth [7] (Vol 3, pp.520–521, 1973), o custo de uma
pesquisa com sucesso é:
1 1

C(N ) ≈ 2 1+ 1−α

E o custo de uma pesquisa sem sucesso é:

C 0(N ) ≈ 1 1 2

2 1 + ( 1−α )

Quando α aproxima-se de 1, o termo 1/(1 − α) tende para ∞,


explodindo o custo da pesquisa no hashing linear. Esse fenômeno
é chamado agrupamento (clustering) e pode ocorrer à medida
que a tabela começa a ficar cheia, pois cada nova chave inserida
tende a ocupar uma posição na tabela que esteja contı́gua a muitas
outras posições já ocupadas, o que deteriora o tempo necessário
para pesquisas.
Entretanto, apesar de o hashing linear ser um método relati-
vamente pobre para resolver colisões, os resultados apresentados
são bons. Segundo Knuth, quando a taxa de ocupação da tabela
é menos que 75%, o hashing linear é tão bom quanto o de lista
encadeada. E quando N = M − 1, o número médio de testes de
uma pesquisa sem sucesso é (M + 1)/2, e o número
p de testes em
uma pesquisa com sucesso é aproximadamente πM/8. O melhor
caso é sempre O(1).

7.4 Conclusão

A técnica de transformação de chaves em endereços para imple-


mentar tabelas de dispersão apresenta as seguintes vantagens:
Estruturas de Dados Fundamentais Capı́tulo 8: Árvores 173

• Alta eficiência no custo de pesquisa, que é O(1) para o caso


médio em uma tabela bem implementada.
• Simplicidade de implementação.

E as principais desvantagens são as seguintes:


• Custo para recuperar os registros na ordem lexicográfica das
chaves é alto, sendo necessário ordenar o arquivo.
• Pior caso é O(N ), que pode ocorrer se a função de trans-
formação for de baixa qualidade.

Exercı́cios
1. Hashing Perfeito é aquele em que nunca há colisões. Pesquise
a literatura e faça um resumo de como implementar esse tipo
de transformação de chaves em endereços.
2. Se um alto percentual das listas encadeadas de itens com
mesmo endereço comecarem a ficar muito longas, o que pode
ser feito para resolver esse problema?

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre a estru-


tura de dados, e particularmente, do conceito de tabela de dis-
persão, é o livro clássico de Donald Knuth [7], que dá um trata-
mento formal e organizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
174 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 8

Árvores

All our wisdom is stored in the trees 1


Santosh Kalwar (1982)

Definição 8.1 Árvore é um conjunto finito de N ≥ 0 nodos,


tal que, se N > 0, existe um nodo especial, chamado raiz da
árvore, e os restantes N−1 nodos estão particionados em m
conjuntos disjuntos, A1, · · · , Am, cada um dos quais é por sua
vez uma árvore. Esses subconjuntos A1, · · · , Am são chamados
subárvores do respectivo nodo raiz.

Árvores são estruturas não-lineares, que servem como repre-


sentação natural para dados aninhados e apresentam excelente
relação custo-benefı́cio quanto à demanda de memória e à eficiência
das operações usuais de busca, inserção e remoção de itens.
Árvores podem ser representadas por grafos acı́clicos, como o
da Fig 8.1, nos quais podem não haver indicação de qual é o nodo
ou vértice raiz, e qualquer um pode assim ser escolhido. Árvores
desse tipo são chamadas de árvores livres.
Para cada fixação do nodo raiz de uma árvore livre, tem-se uma
árvore distinta.
1
Toda nossa sabedoria está armazenada nas árvores

175
176 CAPÍTULO 8. ÁRVORES

Figura 8.1 Uma Árvore Livre

Note que toda árvore é um grafo, mas nem todo grafo é árvore,
pois grafos com ciclos, como o da Fig 8.2, não satisfazem a definição
matemática de árvores, porque dos nodos de um ciclo não se pode
separar um para ser sua raiz.

Figura 8.2 Um Grafo Cı́clico


177

Árvores ordenadas são árvores nas quais a ordenação dos ra-


mos de cada nodo é relevante, como exemplificado na Fig 8.3, que
mostra duas árvores distintas.

Figura 8.3 Duas Árvores Ordenadas

Árvores orientadas são árvores livres nas quais um vértice é


definido como sendo sua raiz e as arestas têm direção, como ilustra
a Fig 8.4, onde o vértice raiz é o único que não é apontado por
nenhum outro.

Figura 8.4 Uma Árvore Orientada

Uma estrutura de dados do tipo árvore orientada pode ser repre-


sentada, conforme ilustrado na Fig 8.5, como grafo acı́clico, com a
178 CAPÍTULO 8. ÁRVORES

raiz na parte de cima e subárvores sempre embaixo, podendo assim


omitir as setas de ordenação, pois são todas de cima para baixo.

Figura 8.5 Representação de uma Árvore Orientada

Toda árvore tem os seguintes atributos:

• Nı́vel: o nı́vel do nodo raiz é 0. Se um nodo está no nı́vel i


então a raiz de suas subárvores estão no nı́vel i + 1.
• Altura: a altura de um nodo é o comprimento do caminho
mais longo desse nodo até um nodo folha. A altura de uma
árvore é a altura do seu nodo raiz.
• Grau: o grau de um nodo é o número de subárvores que ele
possui; um nodo de grau 0 é chamado nodo terminal ou
folha. Na árvore da Fig 8.6, nodo R possui grau 2, X grau 1,
Z e Y possuem grau 0.

Uma floresta é um conjunto de zero ou mais árvores disjuntas.


Para transformar uma floresta em uma árvore basta acrescentar-
lhe um nodo raiz e considerar todas as árvores da floresta suas
subárvores. Toda árvore, inclusive a vazia, é por definição uma
floresta.
8.1. ÁRVORES BINÁRIAS 179

Figura 8.6 Atributos de uma Árvore

8.1 Árvores binárias

Definição 8.2 Árvore binária é um conjunto finito de no-


dos, o qual ou é vazio ou consiste em um nodo raiz e duas
subárvores binárias disjuntas, chamadas subárvore esquerda e
subárvore direita.

Árvores binárias são, portanto, árvores ordenadas e orientadas,


tendo tipicamente a aparência da árvore da Fig. 8.7, onde o nodo
do topo é a raiz.

Figura 8.7 Uma Árvore Binária

Árvores binárias são usadas para representar organização de da-


180 CAPÍTULO 8. ÁRVORES

dos ou informações em diversos campos do conhecimento e de ati-


vidades humanas, por exemplo, são úteis para descrever:
1. Árvores genealógicas, nas quais os pais de uma pessoa são
apresentados como descendentes do nodo associado à pessoa.
2. O histórico de um torneio futebol, em que cada jogo é repre-
sentado por um nodo que indica o seu vencedor, e tem dois
jogos previamente vencidos pelos dois adversários representa-
dos como seus descendentes.
3. Uma expressão aritmética com operadores binários, na qual
cada operador faz o papel de um nodo da árvore, sendo seus
dois operandos representados como suas subárvores.
Uma floresta pode ser representada como uma árvore binária.
A floresta da Fig. 8.8, que é formada pelas árvores (a) e (b), pode
ser representada pela árvore binária (c), cuja raiz é o nodo A:

Figura 8.8 Floresta e Árvore Binárias I

Na árvore (c), os ponteiros-direitos dos nodos estão desenhados


8.1. ÁRVORES BINÁRIAS 181

na horizontal, e os esquerdos, na vertical. Nessa figura, ponteiros-


esquerdos denotam a relação de nodo-pai para nodo-filho-mais-à-
esquerda conforme as árvores da floresta, e os ponteiros-direitos
conectam os respectivos nodos-irmãos. A transformação de uma
floresta em árvore binária é obtida via os seguintes passos:
• Conecte com ponteiros horizontais todos os nodos raı́zes das
árvores da floresta dada, i.e., trate essas raı́zes como nodos
irmãos e defina o primeiro nodo dessa lista de irmão como a
raiz da árvore binária.
• Para cada nodo x já instalado na árvore binária em construção:
– identificar os filhos de x na sua árvore de origem;
– construa uma lista com cópias desses nodos, conectando-os
pelos seus apontadores-direitos;
– conecte o primeiro nodo dessa lista de irmãos como filho
do nodo x, usando o apontador-esquerdo de x.
A árvore (c) da Fig. 8.8 pode ser redesenhada para produzir o
diagrama de árvore binária no esquema tradicional da Fig. 8.9.

Figura 8.9 Floresta e Árvores Binárias II

Invertendo o processo, suponha que ponteiros-direitos da árvore


binária da Fig. 8.10 conectem irmãos e os esquerdos implementem
182 CAPÍTULO 8. ÁRVORES

a relação pai-filho. Assim, nessa árvore, nodos B e E são irmãos e


são filhos de A.

Figura 8.10 Floresta e Árvores Binárias III

Então, pode-se dizer que essa árvore binária corresponde à floresta


formada pelas árvores (a), (b) e (c) da Fig. 8.11.

Figura 8.11 Floresta e Árvores Binárias IV

Uma operação importante sobre estruturas de dados que arma-


zenam coleções de itens é a de visitar todos os seus elementos para
sobre eles realizar alguma ação. Se com listas e filas, fazer uma
varredura sequencial de visitação a todos os itens presentes nes-
sas estruturas é uma operação relativamente simples, com árvores
não há uma ordem natural como em listas e filas, sendo preciso
implementar estratégias mais elaboradas para atender objetivos
especı́ficos. Por exemplo, pode-se desejar visitar os nodos em or-
dem crescente dos valores das chaves ou então em uma ordem que
8.1. ÁRVORES BINÁRIAS 183

facilite a remoção nodos da estrutura com um mı́nimo de impacto.


Nesse sentido, as ordens canônicas de caminhamento em árvores
binárias são:

1. Pré-ordem:
visita raiz;
caminha subárvore esquerda em pré-ordem;
caminha subárvore direita em pré-ordem.
Na Fig. 8.12, visitam-se: A, B, D, C, E, G, F, H e J.
2. In-ordem ou Central:
caminha subárvore esquerda na ordem central;
visita raiz;
caminha subárvore direita na ordem central.
Na Fig. 8.12, visitam-se: D, B, A, E, G, C, H, F e J.
3. Pós-ordem:
caminha subárvore esquerda em pós-ordem;
caminha subárvore direita em pós-ordem;
visita raiz.
Na Fig. 8.12, visitam-se: D, B, G, E, H, J, F e C.

Figura 8.12 Uma Árvore Binária


184 CAPÍTULO 8. ÁRVORES

8.2 Caminhamentos em árvores binárias

O tipo abstrato de dados ÁrvoreBinária1 da Listagem 8.13 imple-


menta as operações tı́picas de árvores binárias para caminhar na
sua estrutura, pesquisar, inserir e remover itens.

1 public class ÁrvoreBinária1 {


2 private Nodo raiz;
3 private static class Nodo {
4 Item item; Nodo esq, dir;
5 Nodo(Item item) {[Link] = item;}
6 void visite() {[Link]();}
7 }
8 private void preordem(Nodo raiz) {...}
9 private void inOrdem(Nodo raiz) {...}
10 private void posOrdem(Nodo raiz) {...}
11 public ÁrvoreBinária1() {}
12 public void preOrdem() {preOrdem(raiz);}
13 public void inOrdem() {central(raiz);}
14 public void pósOrdem() {pósOrdem(raiz);}
15 public void insira(Item item) {...}
16 public Item remova(String chave) {...}
17 public Item pesquise(String chave) {...}
18 }

Listagem 8.13 Tipo Abstrato ArvoreBinária

Destacam-se na Listagem 8.13 a declaração da estrutura de cada


nodo da árvore via a classe privada Nodo, a qual contém o campo
item e os ponteiros para as respectivas subárvores, e o uso do tipo
Item, formado por uma chave e pela operação visite destinada
a realizar alguma ação com as informações do respectivo nodo,
sendo a ação default a de impressão da chave, conforme definido
na Listagem 8.14.
As operações insira, remova e pesquise somente são detalhadas
8.2. CAMINHAMENTOS EM ÁRVORES BINÁRIAS 185

1 public class Item {


2 public String chave;
3 public void visite() {[Link](chave);}
4 }

Listagem 8.14 Tipo Abstrato Item

nos próximos capı́tulos, quando novas propriedades forem agrega-


das à estrutura de árvores binárias. O foco do presente capı́tulo
são as operações de caminhamento na árvore, que são efetivamente
implementadas pelas operações homônimas privadas detalhadas na
Listagem 8.15.
1 private void preOrdem(Nodo raiz) {
2 if (raiz != null) {
3 [Link](); preOrdem([Link]); preOrdem([Link]);
4 };
5 }
6 private void inOrdem(Nodo raiz) {
7 if (raiz != null) {
8 inOrdem([Link]); [Link](); inOrdem([Link]);
9 };
10 }
11 private void pósOrdem(Nodo raiz) {
12 if (raiz != null) {
13 posOrdem([Link]); posOrdem([Link]); [Link]();
14 };
15 }

Listagem 8.15 Algoritmos de Caminhamento

Os custos de execução T (n), em uma árvore com n nodos, dos


métodos preOrdem, inOrdem e PósOrdem são definidos pelas seguintes
equações de recorrência:

T (n) = 1 + 2T (n), para n > 1
T (1) = 1
186 CAPÍTULO 8. ÁRVORES

cuja solução é T (n) = 2n − 1, sendo, portanto, o custo de um


caminhamento é linearmente proporcional ao tamanho da árvore.
Alternativamente, os algoritmos de caminhamento podem ser
implementados para operar de forma iterativa em vez de recursiva,
como apresentado nas listagens 8.16, 8.18, 8.19 e 8.20.

1 public class ÁrvoreBinária2 {


2 private Nodo raiz;
3 private static class Nodo {
4 Item item; Nodo esq, dir;
5 Nodo(Item item) {[Link] = item;}
6 void visite() {[Link]();}
7 }
8 private static class Par {
9 public Nodo a; public int d;
10 public Par(Nodo a, int d) {this.a = a; this.d = d;}
11 }
12 private static class Pilha<T> {...}
13 private void preordem(Nodo raiz) {...}
14 private void central(Nodo raiz) {...}
15 private void posOrdem(Nodo raiz) {...}
16 public static class Item {
17 public String chave;
18 public void visite() {[Link](chave);}
19 }
20 public ÁrvoreBinária2() {}
21 public void preOrdem() {preOrdem(raiz);}
22 public void central() {central(raiz);}
23 public void pósOrdem() {pósOrdem(raiz);}
24 public void insira(Item item) {...}
25 public Item remova(Chave chave) {...}
26 public Item pesquise(Chave chave) {...}
27 }

Listagem 8.16 Tipo Abstrato ArvoreBinária


8.2. CAMINHAMENTOS EM ÁRVORES BINÁRIAS 187

Para isso a declaração da classe ÁrvoreBinária deve ser alte-


rada para incluir a declaração de uma pilha e das novas versões do
métodos privados preOrdem, central e posOrdem, como detalhados
a seguir.
A pilha mencionada deve ser genérica, como a esquematizada
na Listagem 8.17, para acomodar os três tipos de caminhamento.

1 public class Pilha<T> {


2 private int máximo; private int topo = -1;
3 private T[] info;
4 public Pilha(int tamanho) {
5 máximo = (tamanho <1) ? 1 : tamanho - 1;
6 info = (T[]) new Object[máximo+1];
7 }
8 public void empilhe(T v) {
9 if (topo == máximo) return;
10 info[++topo]= v;
11 }
12 public T desempilhe() {
13 if(vazia()) return null;
14 return info[topo--];
15 }
16 public boolean vazia() {
17 return (topo == -1);
18 }
19 public void limpe() {topo = -1;}
20 }

Listagem 8.17 Tipo PilhaDeNodo

A operação preOrdem iterativa da Listagem 8.18 tem o mesmo


efeito que a definição recursiva apresentada na Listagem 8.15.
Nessa implementação, a partir de um nodo p, que no caso é a raiz
da árvore, caminha-se para a esquerda, visitando cada nodo e empi-
lhando o caminho de retorno para depois caminhar nas subárvores
188 CAPÍTULO 8. ÁRVORES

1 void preOrdem(Nodo p) {
2 Pilha<Nodo> s = new Pilha<Nodo>(10);
3 while (true) {
4 while (p != null) {
5 [Link]();
6 [Link](p); p = [Link];
7 }
8 if ([Link]()) return;
9 p = [Link]();
10 p = [Link];
11 }
12 }

Listagem 8.18 Caminhamento Pré-Ordem Iterativo

da direita. Solução similar para o caminhamento em ordem central


encontra-se na Listagem 8.19.
1 private void central(Nodo p) {
2 Pilha<Nodo> s = new Pilha<Nodo>(10);
3 while (true) {
4 while (p != null) {
5 [Link](p); p = [Link];
6 }
7 if [Link]() return;
8 p = [Link]();
9 [Link]();
10 p = [Link];
11 }
12 }

Listagem 8.19 Caminhamento In-Ordem Iterativo

A versão iterativa do caminhamento pós-ordem demanda o em-


pilhamento do nodo e a informação se a descida está sendo feita
pela esquerda ou pela direita, como esquematizado na Listagem 8.20.
Observe que pares contendo endereço de um nodo e a informação
8.3. CAMINHAMENTOS COM ITERADORES 189

1 private void posOrdem(Nodo p){


2 Pilha<Par> s = new Pilha<Par>(10);
3 Par x;
4 while (true){
5 while (p != null) {
6 [Link](p,0); p = [Link];
7 }
8 do {if [Link]( ) return;
9 x = [Link](); p = x.a;
10 if (x.d == 1) [Link]();
11 else {[Link](p,1); p = [Link];}
12 } while (x.d == 1);
13 }
14 }

Listagem 8.20 Caminhamento In-Ordem Iterativo

de que trata-se de raiz de subárvore esquerda ou direita são, após


cada desempilhamento, usados saber se a subárvore direita de nodo
desempilhado ainda precisa ser percorrida.
Entretanto, os tipos ÁrvoreBinária1 e ÁrvoreBinária2 não permi-
tem caminhamento em paralelo nas três ordens. Uma vez iniciado
um caminhamento, ele é executado até o fim. Uma forma de im-
plementar operações que permitam caminhamento concomitante
nas três ordens é tratar a árvore binária como um tipo contêiner
de itens dotado de iteradores.

8.3 Caminhamentos com iteradores

Um contêiner é uma estrutura de dados que armazena um conjunto


de itens de um mesmo tipo. Muitas vezes deseja-se recuperar os
itens do contêiner, um de cada vez, em uma dada ordem. Isso pode
ser obtido via um mecanismo denominado iteradores.
190 CAPÍTULO 8. ÁRVORES

Listagem 8.21 traz uma implementação de árvore binária como


um contêiner dotado de três iteradores.
1 public class ArvoreIteradora {
2 private Nodo raiz;
3 private static class Nodo {
4 Item item; Nodo esq, dir;
5 Nodo(Item item) {[Link] = item;}
6 void visite() {[Link]();}
7 }
8 private static class Par {
9 public Nodo a; public int d;
10 public Par(Nodo a, int d) {this.a = a; this.d = d;}
11 }
12 private class IteradorPre implements Iterador {...}
13 private class IteradorIn implements Iterador {...}
14 private class IteradorPos implements Iterador {...}
15 public ArvoreIteradora() {}
16 public void insira(Item item) {...}
17 public Item remova(String chave) {...}
18 public boolean pesquise(String chave) {...}
19 public Iterador iteradorPre() {
20 return new IteradorPre( );
21 }
22 public Iterador iteradorIn() {
23 return new IteradorIn( );
24 }
25 public Iterador iteradorPos() {
26 return new IteradorPos( );
27 }
28 }

Listagem 8.21 Tipo ÁrvoreComIterador

Os iteradores usados são objetos do tipo Iterador, como o de-


finido na Listagem 8.22, o qual consiste basicamente em duas
operações: reinicie e próximo, e têm a função de armazenar o
8.3. CAMINHAMENTOS COM ITERADORES 191

estado interno de cada iteração.

1 public interface Iterador {


2 public void reinicie();
3 public Item próximo();
4 }

Listagem 8.22 Interface do Iteradores

Um contêiner pode ter vários iteradores definidos, e cada um


deles, quando criados, devem ser posicionados de forma a retornar o
primeiro item do contêiner associado, segundo uma ordem definida
em sua implementação. A operação reinicie recoloca o iterator na
posição inicial, pronto para retornar o primeiro item do contêiner,
e próximo retorna sempre o próximo item na ordem definida pelo
caminhamento, ou retorna null, se não houver mais itens a ser
retornados.
1 private class IteradorPre implements Iterador {
2 private Nodo a;
3 private Item r;
4 private Pilha<Nodo> p = new Pilha<Nodo>(10);
5 public IteradorPre() a = raiz;
6 public void reinicie() {[Link]( ); a = raiz;}
7 public Item próximo() {
8 while ((a == null) && !([Link]())) {
9 a = [Link](); a = [Link];
10 }
11 if (a != null) {
12 r = [Link]; [Link](a); a = [Link];
13 return r;
14 } else return null;
15 }
16 }

Listagem 8.23 IteradorPre


192 CAPÍTULO 8. ÁRVORES

As operações iteradorPre, iteradorIn e iteradorPos criam ob-


jetos iteradores, que funcionam como máquinas de estado, e que
controlam o passo-a-passo de cada um dos caminhamentos: a cada
chamada da operação próximo, o item corrente do caminhamento
é retornado e o iterador fica posicionado para retornar o próximo
item na próxima chamada. As listagens 8.23, 8.24 e 8.25 detalham
a implementação das classes privadas IteradorPre, IteradorIn e
IteradorPos, respectivamente.

1 private class IteradorIn implements Iterador {


2 private Nodo a;
3 private Item r;
4 private Pilha<Nodo> p = new Pilha<Nodo>(10);
5 public IteradorIn() a = raiz;
6 public void reinicie() {[Link](); a = raiz;}
7 public Item próximo() {
8 while (a != null) {[Link](a); a = [Link];}
9 if ([Link]()) return null;
10 a = [Link]();
11 r = [Link]; a = [Link];
12 return r;
13 }
14 }

Listagem 8.24 IteradorIn

Os três iteradores fazem seus caminhamentos nas árvores, passo-


a-passo, de forma independente um do outro, usando cada um uma
pilha própria, como na implementação das árvores iterativas da
Listagem 8.16. Esses iteradores retornam um item de cada vez,
e retêm em seus estados correntes as informações necessárias, in-
cluindo o conteúdo das pilhas de caminhamento, para, na chamada
subsequente, retornar o próximo item da sequência.
8.4. ÁRVORES BINÁRIAS ESTENDIDAS 193

1 private class IteradorPos implements Iterador {vor


2 private Nodo a; private Item r; private int d = 1;
3 private Pilha<Par> p = new Pilha<Par>(10);
4 public IteradorPos() a = raiz;
5 public void reinicie() {
6 [Link](); a = raiz; d = 1;
7 }
8 public Item próximo() {
9 Par x;
10 while (true) {
11 if(a == null) {
12 if ([Link]()) return null;
13 x = [Link](); a = x.a; d = x.d;
14 }
15 switch (d) {
16 case 1: [Link](new Par(a,2)); a = [Link]; break;
17 case 2: [Link](new Par(a,3)); a = [Link]; d = 1;
18 break;
19 case 3: r = [Link]; a = null; return r;
20 }
21 }
22 }
23 }

Listagem 8.25 InteradorPos

8.4 Árvores binárias estendidas

Definição 8.3 Árvore binária estendida é um conjunto de fi-


nito de nodos, o qual consiste em um nodo externo ou em um
nodo interno e de duas subárvores binárias estendidas, cha-
madas subárvore esquerda e subárvore direita. (Knuth [7])
Os nodos internos de uma árvore binária estendida são repre-
sentados por cı́rculos, e os externos por pequenos quadrados, como
ilustrado na Fig. 8.26
194 CAPÍTULO 8. ÁRVORES

Figura 8.26 Nodos Internos e Externos

Os nodos externos podem ser usados para armazenar alguma


informação pertinente a cada aplicação. Os nodos internos podem
ter também informações além dos ponteiros de subárvores, como
chaves de pesquisa para localização de nodos externos.
O interessante é que o número de nodos externos é igual ao de
internos mais um, o que garante espaço em nodos externos para
associar informação a toda chave armazenada nos nodos internos.

Teorema 8.1 Em uma árvore binária estendida qualquer, o


número de nodos externos EI é igual ao número de nodos in-
ternos I acrescido de uma unidade. Isto é, EI = I + 1

Prova I (por indução matemática):


1. Para I = 1, temos obviamente que EI = 2, portanto
EI = I + 1, para I = 1.
2. Suponha EI = I + 1, para I = n, i.e., En = n + 1 . Para
acrescentar um nodo interno, i.e., fazer I = n + 1, deve-se
transformar um externo em interno e adicionar dois novos ex-
ternos como seus filhos.
Portanto, En+1 = (En − 1) + 2.
8.5. CONCLUSÃO 195

3. Resolvendo a equação:
En+1 = (En − 1) + 2
En+1 = (n + 1 − 1) + 2
En+1 = (n + 1) + 1
EI = I +1
Prova II (por análise das propriedades da árvore):

1. Sejam EI = número de nodos externos


I = número de nodos internos.
2. A todo nodo, exceto o nodo raiz, chega uma aresta, portanto
existem EI + I − 1 arestas na árvore.
3. Por outro lado, de todo nodo interno saem duas aresta, por-
tanto existem 2I arestas na árvore.
4. Portanto, EI + I − 1 = 2I.
5. Então EI = I + 1.
Teorema 8.2 O número máximo de nodos externos em uma
árvore binária estendida de altura h é 2h.
Prova:
altura número máximo de nodos
1 2 21
2 4 22
Observe que:
3 8 23
··· ··· ···
h 2h 2h
Portanto, o número máximo de nodos externos = 2h.

8.5 Conclusão

Árvores são estruturas não-lineares, que servem como representa-


ção natural para dados aninhados e apresentam excelente relação
196 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

custo-benefı́cio quanto à demanda de memória e à eficiência das


operações usuais de busca, inserção e remoção de itens.
Em árvores bem construı́das, os custos dessas operações são de
ordem logarı́tmica, e a demanda adicional de memória resume-se
ao encadeamento de nodos para formar a estrutura hierárquica
desejada.

Exercı́cios
1. Hashing Perfeito é aquele em que nunca há colisões. Pesquise
a literatura e faça um resumo de como implementar esse tipo
de transformação de chaves em endereços.
2. Se um alto percentual das listas encadeadas de itens com
mesmo endereço comecarem a ficar muito longas, o que pode
ser feito para resolver esse problema?

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre a estru-


tura de dados, e particularmente, do conceito de árvores, são os
livros clássicos de Donald Knuth [6, 7], que dão um tratamento
formal e organizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
Capı́tulo 9

Filas de Prioridades

Action expresses priorities1


Mahatma Gandhi (1869-1948)

As filas apresentadas no Capı́tulo 6 são estruturas FIFO, que


obedecem à regra de que o item inserido há mais tempo deve ser o
próximo a ser retirado. Entretanto, há situações em que se deseja
dar graus de prioridade aos itens de uma fila e usar esses graus
para definir a ordem de retirada de elementos. Esse tipo de fila é
denominado Fila de Prioridades.
Definição 9.1 Fila de Prioridade é uma estrutura em que o
item a ser retirado é sempre o de maior prioridade, indepen-
dentemente da ordem de inserção.
Filas de prioridades são usadas na implementação de importan-
tes algoritmos na área de Computação, por exemplo: (i) sistemas
operacionais usam filas de prioridades, onde as chaves represen-
tam o tempo em que eventos devem ocorrer; (ii) alguns métodos
numéricos iterativos são baseados na seleção repetida de um item
com maior ou menor valor; (iii) há sistemas de gerência de memória
que usam a técnica de substituir a página menos utilizada na
memória principal do computador por uma nova página.
1
Ação expressa prioridades

197
198 CAPÍTULO 9. FILAS DE PRIORIDADES

Uma lista de prioridade pode ser implementada como uma lista


itens não ordenados com custo de inserção O(1) e de remoção O(n).
Com uma lista ordenada, os custos se invertem. Uma opção mais
eficiente é usar uma estrutura baseada em heaps e árvores binárias
completas, as quais têm custo de construção O(n lg n), e os de
inserção e retiradas, O(lg n), conforme apresentados a seguir.

9.1 Árvore binária completa

Definição 9.2 Uma árvore binária completa é uma árvore biná-


ria que tem seus nodos numerados de 1 a n, de tal forma que
nodo bk/2c é pai do nodo k, para 1 < k ≤ n e é dotada das
seguintes propriedades2:
• nodos-folha aparecem em no máximo dois nı́veis adja-
centes;
• nodos do nı́vel mais baixo estão sempre posicionados
mais à esquerda.

Fig. 9.1 mostra dois exemplos de árvore binária completa.

(a) (b)

Figura 9.1 Duas Árvores Binárias Completas


2
bxc é o maior inteiro menor ou igual a x
9.1. ÁRVORE BINÁRIA COMPLETA 199

Essa propriedade de que o nodo de ı́ndice bk/2c é pai do nodo


k permite armazenar árvores binárias completas em um arranjo a
partir do ı́ndice 1, não sendo necessário armazenar os ponteiros de
subárvores, conforme mostra a Fig. 9.2, onde os arranjos (a) e (b)
são representações lineares das respectivas árvores da Fig. 9.1.

(a)

(b)

Figura 9.2 Duas Árvores Binárias Completas

Essa possibilidade de trabalhar com um arranjo de itens como


se fosse uma árvore binária é vantajosa sob vários aspectos, como
enconomia de memória e eficiência em operações de busca.
Uma árvore binária completa estendida tem as propriedades:
• Número de nodos externos = número de nodos internos + 1.
• Número de nodos externos ≤ 2h, onde h = altura.

• Altura hi de nodo de número i em uma árvore completa com


n nodos internos é definida por hi = lg(n/i), pois se nodo i
for pai de 2i, que é pai de 4i, · · · , que é pai de 2hi i, tal que
(i2hi ≥ n) ∧ (i2hi−1 ≤ n) ⇒ lg(n/i) ≤ hi ≤ lg(n/i) + 1,
portanto, hi = lg(n/i).
200 CAPÍTULO 9. FILAS DE PRIORIDADES

9.2 Conceito de heaps

Heap é uma propriedade imposta a uma sequência de valores com-


paráveis por ==, < e >, proposta por R. W. Floyd em 1964, para
tornar eficientes operações de inserção e remoção de itens em uma
estrutura, como árvores binárias completas.

Definição 9.3 Heap é uma sequência de itens com chaves


c[1], c[2], . . . , c[n] tais que
c[bi/2c] ≥ c[i], ∀i tal que 1 ≤ bi/2c < i ≤ n.

Heaps não são árvores, são uma propriedade que pode ser im-
posta a toda sequência de valores, inclusive a um arranjo de chaves
representando uma árvore binária completa, de forma a torná-lo
uma fila de prioridade, como ilustrado na Fig. 9.3.

Figura 9.3 Duas Árvores Binárias Completas

Na árvore binária completa da Fig. 9.3, cada nodo é identificado


por um caractere, que representa chave nele armazenada, cujo valor
é seu código ascii. E note que na representação por um arranjo a
maior chave está sempre na posição 1 do vetor, e que todas as
9.3. CONSTRUÇÃO DE FILAS DE PRIORIDADE 201

chaves satifazem a definição de um heap, criando um mecanismo


para implementação eficiente de uma fila de prioridades.

9.3 Construção de filas de prioridade

Uma sequência de itens armazenados em um arranjo, com o pri-


meiro item na posição de ı́ndice 1, pode ser interpretada como
uma árvore binária completa, como a da Fig. 9.4, que mostra um
arranjo de chaves ou itens e a árvore binária completa associada.

Figura 9.4 Duas Árvores Binárias Completas

Para transformar uma árvore binária completa representada por


uma sequência s de n chaves em uma representação que permita
implementação eficiente de uma fila de prioridade, é necessário
reorganizar essa sequência de forma a transformá-la em um heap.
Isso pode ser feito segundo os seguintes passos:
1. Faça p = n/2 + 1, i.e., defina p como o primeiro ı́ndice mais à
esquerda em s, tal que 2p > n. Note que os nodos da árvore
binária completa s[p], s[p+1], ..., s[n] satisfazem a propri-
edade de heap, pois não têm filhos nessa árvore.
202 CAPÍTULO 9. FILAS DE PRIORIDADES

2. Faça uma varredura em s a partir de p para a esquerda até


a posição de ı́ndice 1, fazendo todas as trocas de posição das
chaves para que os nodos visitados atendam a propriedade do
heap, i.e., assegure-se que para todo nodo i visitado nessa
varredura, s[i] ≥ s[2*i] e s[i] ≥ s[2*i+1]

A aplicação do algoritmo acima na seguinte árvore binária com-


pleta, representada como um arranjo, onde p = 8,

produz as seguintes transformações em sua representação:


• Para i = 7:

deve-se trocar as chaves I e N de posição, pois, lexicografica-


mente N > G e N > I.

• Nesse ponto, todas as chaves a partir da posição 7 satisfazem


o heap. Deve-se então, ajustar o chave da posição i = 6,

o que requer a troca das chaves F e L de posição, pois, lexi-


cograficamente L > E e F < L, e após a troca F, em sua nova
posição, satisfaz o heap, porque nodos na sua posição não têm
filhos, produzindo a seguinte configuração:
9.3. CONSTRUÇÃO DE FILAS DE PRIORIDADE 203

• Para i = 5:

deve-se trocar A e J de posição.

• Para i = 4:

faz-se a troca de K e M.

• Para i = 3:

deve-se trocar as chaves C e N de posição, pois, lexicografica-


mente N > C e N > L:

Deve-se também trocar C de posição com I, porque I > C e


I > G, produzindo a seguinte configuração:
204 CAPÍTULO 9. FILAS DE PRIORIDADES

• Para i = 2:

deve-se inicialmente trocar B e M de posições, produzindo:

e B deve ser trocado de posição com K:

• Para i = 1:

não são necessárias mais trocas, e o processo se encerra: o


arranjo final é um heap, representando a seguinte árvore:
9.4. IMPLEMENTAÇÃO DE FILA DE PRIORIDADES 205

9.4 Implementação de fila de prioridades

Como todo item de uma fila de prioridades deve ter uma prioridade
definida, os itens armazenáveis em filas do tipo FilaP, definido a
seguir, devem ser de tipos derivados da classe Item, declarada na
Listagem 9.5, em que o campo prioridade é um inteiro.

1 public class Item {


2 public int prioridade;
3 public Item(int prioridade) {
4 [Link] = prioridade;
5 }
6 public void listaDados() {[Link](prioridade);}
7 }

Listagem 9.5 Raiz dos Itens da Fila

A fila de prioridade é o tipo abstrato de dados FilaP, declarado


segundo o esquema da Listagem 9.6.

1 public class FilaP<T extends Item> {


2 private T[] fila;
3 private int nElementos;
4 private void posicione(int raiz) {...}
5 public FilaP(int tamanho) {...}
6 public FilaP(int tamanho, T[] vetor) {...}
7 public static class FilaCheia extends Exception {}
8 public void insira(T x) throws FilaCheia {...}
9 public static class FilaVazia extends Exception {}
10 public T retire() throws FilaVazia {...}
11 public void redimensione(int tamanho){...}
12 public boolean vazia() {return(nElementos == 0);}
13 public void listaDados() {...}
14 }

Listagem 9.6 Tipo Abstrato FilaP


206 CAPÍTULO 9. FILAS DE PRIORIDADES

As construtoras de FilaP, declaradas na Listagem 9.7, permitem


criar uma lista vazia ou uma com seu heap já inicializado.
1 public FilaP(int tamanho) {
2 if (tamanho < 1) tamanho = 1;
3 fila = (T[]) new Item[tamanho];
4 nElementos = 0;
5 }
6 public FilaP(int tamanho, T[] vetor) {
7 if (tamanho < [Link]) tamanho = [Link];
8 fila = (T[]) new Item[tamanho];
9 for(int i=1 ; i<=[Link]; i++) fila[i]=vetor[i-1];
10 nElementos = [Link];
11 for(int i = (nElementos/2); i > 0; i--) posicione(i);
12 }

Listagem 9.7 Construtoras de ]twFilaP

O vetor de itens passado à construtora é transformado em heap


com auxı́lio operação posicione, definida na Listagem 9.8.
1 private void posicione(int raiz) {
2 int i, j; T x;
3 i = raiz; j = 2 * i; x = fila[i];
4 while (j <= nElementos) {
5 if (j < nElementos) {
6 if (fila[j].prioridade<fila[j+1].prioridade) j=j+1;
7 }
8 if ([Link] >= fila[j].prioridade) break;
9 fila[i] = fila[j];
10 i = j; j = 2 * i;
11 }
12 fila[i] = x;
13 }

Listagem 9.8 Construção Parcial de Heap

Uma chamada posicione(i) move, se necessário, o item fila[i]


9.4. IMPLEMENTAÇÃO DE FILA DE PRIORIDADES 207

para uma posição k que satisfaça a propriedade do heap, i.e., em


que fila[k] >= fila[2*k] e fila[k] >= fila[2*k+1]. Para isso, a
operação posicione(i) faz um caminhamento de descida na árvore
binária associada à fila a partir do nodo de ı́ndice i, possivelmente
até uma folha. O custo médio dessa operação é a metade da altura
da árvore, i.e., C(n) = (1/2) lg n, onde n é o número de itens
na fila. Como essa operação é aplicada pela construtura a todo
item da fila, o custo médio da construtora é C(n) = (n/2) lg n ou
C(n) = O(n lg n).
A operação insira instala inicialmente o novo item no fim da fila
e em seguida realiza uma caminhada em direção à raiz da árvore
associada, trocando itens de posição, até encontrar a localização
onde o novo item deve ser posicionado para atender a propriedade
do heap, como mostrado na Listagem 9.9. O custo médio dessa
operação é a metade da altura da árvore: C(n) = (1/2) lg n ou
C(n) = O(lg n).

1 public void insira(T x) throws FilaCheia {


2 if (nElementos >= [Link]-1) throw new FilaCheia();
3 nElementos++;
4 int i, j = nElementos;
5 while (j > 1) {
6 i = j/2;
7 if ([Link] <= fila[i].prioridade) break;
8 fila[j] = fila[i];
9 j = i;
10 }
11 fila[j] = x;
12 }

Listagem 9.9 Operação [Link]

A operação retire, da Listagem 9.10, remove e retorna o item


da primeira posição da fila, a qual passa a ser ocupada pelo último
208 CAPÍTULO 9. FILAS DE PRIORIDADES

item da fila e, em seguida, executa-se posicione(1) para refazer o


heap. O custo de retire é então igual à altura da árvore completa
associada, i.e., C(n) = lg n ou C(n) = O(lg n), onde n é número
de itens na fila.
1 public T retire() throws FilaVazia {
2 if (vazia()) throw new FilaVazia();
3 T x = fila[1];
4 nElementos--;
5 if (nElementos > 0) {
6 fila[1] = fila[nElementos+1];
7 fila[nElementos+1] = null;
8 posicione(1);
9 }
10 return x;
11 }

Listagem 9.10 Operação [Link]

A operação listaDados, definida na Listagem 9.11, percorre toda


fila imprimindo informações de cada item armazendo.
1 public void listaDados() {
2 for(int i=1; i<=nElementos; i++) {
3 fila[i].listaDados();
4 [Link](",");
5 }
6 [Link]();
7 }

Listagem 9.11 Operação [Link]

9.5 Uso de fila de prioridade

A classe MeuItem, definida na Listagem 9.12, acrescenta ao campo


prioridade herdado de sua superclasse Item o campo valor e define
9.5. USO DE FILA DE PRIORIDADE 209

a operação listaDados para imprimir a prioridade e o valor de cada


elemento da fila.
1 public class MeuItem extends Item {
2 public int valor;
3 public MeuItem(int prioridade, int valor) {
4 super(prioridade);
5 [Link] = valor;
6 }
7 public void listaDados() {
8 [Link]("(" + valor + "," + prioridade + ")");
9 }
10 }

Listagem 9.12 Um Item do Usuário

O programa de teste de FilaP a seguir cria uma fila iniciada com


seis itens, depois acrescenta mais dois e, por último, remove os três
de maiores prioridades:

1 public class UsaFilaP {


2 public static void main(String[] args)
3 throws [Link], [Link] {
4 int tamanho = 10;
5 int prioridade;
6 int[] prioridades = {0,1,8,4,7,3,2,5,6};
7 MeuItem x;
8

9 [Link]("Inicia fila:");
10 MeuItem[] vetor = new MeuItem[6];
11 for(int i=1; i<= [Link]; i++)
12 vetor[i-1] = new MeuItem(prioridades[i],10*i);
13 FilaP<MeuItem> f = new FilaP<MeuItem>(tamanho,vetor);
14 [Link]("Lista conteudo da Fila:");
15 [Link]();
16

17 [Link]("Insira outros itens:");


210 CAPÍTULO 9. FILAS DE PRIORIDADES

18 for (int i=7; i<=8; i++) {


19 x = new MeuItem(prioridades[i],10*i);
20 [Link](x);
21 }
22 [Link]("Conteúdo da Fila:");
23 [Link]();
24

25 [Link]("Retire tr^
es itens:");
26 for (int i=0; i<3; i++) {
27 x = [Link]( );
28 [Link]("Retirado = " + [Link] );
29 }
30 [Link]("Conteudo da Fila:");
31 [Link]();
32 }
33 }

O programa UsaFilaP acima produz o seguinte resultado:


Inicia fila:
Lista conteudo da Fila:
(20,8),(40,7),(30,4),(10,1),(50,3),(60,2),
Insira outros itens:
Conteúdo da Fila:
(20,8),(40,7),(70,5),(80,6),(50,3),(60,2),(30,4),(10,1),
Retire tr^es itens:
Retirado = 8
Retirado = 7
Retirado = 6
Conteudo da Fila:
(70,5),(30,4),(60,2),(10,1),(50,3),

9.6 Conclusão

Filas FIFO têm custos O(1) para inserção e remoção de itens.


Entretanto, em muitas aplicações impõe-se algum critério de prio-
ridade na ordem em que elementos são removidos de uma fila, por
Estruturas de Dados Fundamentais Capı́tulo 10: Árvores Patricia 211

exemplo, na implementação de sistemas operacionais de computa-


dores ou de gerência de memória virtual. Nesses casos, o uso de
filas lineares pode gerar altos custos para realizar remoções base-
adas em prioridades, pois a fila pode ter que ser pesquisada para
determinar o item a ser removido.
Fila de prioridades é uma solução eficiente para evitar essa
elevação de custos, porque permitem que as operações de inserção
e remoção sejam O(lg n), como foi demonstrado neste capı́tulo.

Exercı́cios
1. Como o algoritmo da FilaP funciona se houver itens com pri-
oridades repetidas?

Notas Bibliográficas

Heap foi inventado por por J.W.J. Williams, em 1964, para supor-
tar eficiente operações de inserção e remoção de itens em filas.
O texto fundamental para um estudo aprofundado sobre a es-
trutura de dados, e particularmente, do conceito de filas de priori-
dades, são os livros clássicos de Donald Knuth [6, 7], que dão um
tratamento formal e organizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
212 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 10

Árvores Patricia

Intelligence is not the ability to store in-


formation, but to know where to find it 1
Albert Einstein (1879-1955)

Pesquisa digital é um método de pesquisa em estruturas que


armazenam um conjunto de chaves e que é baseado no uso da
representação dessas chaves, as quais podem ser uma sequência
de caracteres ou de dı́gitos, para localizar os registros ou itens
desejados. Os métodos de pesquisa digital são particularmente
vantajosos quando as chaves são longas e de tamanho variável.
Os principais métodos de pesquisa digital são Tabelas de Dis-
persão, objeto de estudo do Capı́tulo 7, Árvores de Pesquisa
Digital, Árvores Trie e Árvores Patricia, apresentadas a
seguir.
Na pesquisa digital, comparações de chaves podem ser minimi-
zadas ou até eliminadas totalmente. No caso de Tabelas de Dis-
persão, a chave é transformada em um endereço, e comparações de
chaves somente ocorrem para resolver colisões. Nas outras estrutu-
ras, chaves são vistas como sequências de bits ou de caracteres, os
quais são usados para direcionar o processo de busca, sendo testes
diretos de chaves minimizados.
1
Inteligência não é a capacidade de armazenar informações, mas de saber como encontrá-las

213
214 CAPÍTULO 10. ÁRVORES PATRICIA

10.1 Árvores de pesquisa digital

Uma árvore binária de pesquisa digital, tipicamente, é uma árvore


na qual as chaves são armazenadas diretamente em seus nodos in-
ternos e os bits da chave de pesquisa são usados para governar a
descida pela árvore2. Se a chave que se está buscando diferir da
chave do nodo em análise, a busca deve continuar na subárvore
esquerda desse nodo se o bit correspondente ao caminhamento
já realizado for 0, do contrário, a pesquisa deve prosseguir pela
subárvore direita.
Para ilustrar processo de construção de uma árvore binária de
pesquisa digital, considere a inserção das seguintes chaves de cinco
bits cada uma, iniciando-se com uma árvore vazia:
A:00001 S:10011 E:00101 R:10010 C:00011 H:01000 I:01001
N:01110 G:00111 X:11000 M:01101 P:10000 L:01100

• A inserção da primeira chave A:00001 produz a árvore:

• A inserção de S:10011 deve ser à direita de A, porque seu pri-


meiro bit é 1:

2
Se no lugar de bits forem usados os caracteres que formam as chaves, a árvore pode ser m-ária
10.1. ÁRVORES DE PESQUISA DIGITAL 215

• A inserção de E:00101 é à esquerda de A, pois seu primeiro bit


é 0:

• A inserção de R:10010 é à esquerda de S, porque seus dois


primeiros bits são 10:

• A inserção de C:00011 é à esquerda de E, porque seus dois


primeiros bits são 00:
216 CAPÍTULO 10. ÁRVORES PATRICIA

• A inserção de H:01000 é à direita de E, porque seus dois pri-


meiros bits são 01 e esse prefixo leva a esse ponto de inserção:

• A inserção de I:01001 é à esquerda de H, porque seus três


primeiros bits são 010:

Observe que o caminho da raiz até cada chave inserida é ape-


nas um prefixo da cadeia de bits da respectiva chave e que
diferentes chaves compartilham prefixos. Assim, toda chave
encontrada ao longo do processo de inserção deve ser testada
para verificar se a chave que a ser inserida já está ou não na
árvore. O presente algoritmo não prevê inserção repetida de
uma mesma chave.
10.1. ÁRVORES DE PESQUISA DIGITAL 217

• A inserção de N:01110 é à direita de H, porque seus três primei-


ros bits são 011:

• A inserção de G:00111 é à direita de C, porque seus três primei-


ros bits são 001:

• A inserção de X:11000 é à direita de S, porque seus dois pri-


meiros bits são 11:
218 CAPÍTULO 10. ÁRVORES PATRICIA

• A inserção de M:01101 é à esquerda de N, porque seus quatro


primeiros bits são 0110:

• A inserção de P:10000 é à esquerda de R, porque seus três


primeiros bits são 100:
10.1. ÁRVORES DE PESQUISA DIGITAL 219

• A inserção de L:01100 é à esquerda de M, porque seus cinco


primeiros bits são 01100:

E processo de inserção das chaves


A 00001 S 10011 E 00101 R 10010 C 00011 H 01000 I 01001
N 01110 G 00111 X 11000 M 01101 P 10000 L 01100
em uma árvore de pesquisa digital vazia é concluı́do.
220 CAPÍTULO 10. ÁRVORES PATRICIA

A árvore de pesquisa digital resultante da inserção dessas chaves


é a seguinte:

O algoritmo de busca deve fazer o caminhamento a partir da


raiz da árvore, comparando a chave de busca com a chave no nodo
sendo visitado. Se essas chaves forem distintas, a busca prossegue
à esquerda ou à direira conforme o valor do bit correspondente ao
caminho já percorrido. O custo médio dessa operação é a metade
do comprimeno das chaves.

10.2 Árvores trie

Uma trie é uma árvore m-ária cujos nodos internos contêm ape-
nas vetores de tamanho m de apontadores de subárvores, os quais
correspondem, aos dı́gitos ou caracteres que formam chaves. Um
nodo interno no nı́vel i da árvore representa o conjunto de todas
as chaves que começam com a mesma sequência de i dı́gitos ou
caracteres, e dele saem m ramificações, cada uma correspondente
ao (i + 1)-ésimo dı́gito ou caractere da chave. Somente as folhas
10.2. ÁRVORES TRIE 221

da árvore trie contêm chaves armazenadas.


Uma trie binária trata as chaves como uma sequência de bits,
nesse caso, m = 2. No processo de busca de chaves, em vez de
caminhar na árvore de acordo com o resultado de comparação da
chave de pesquisa com chave em nodos, na trie, caminha-se de
acordo com os bits de chave de pesquisa.
O algoritmo de inserção de chaves em uma trie inicia-se com
o processo de busca na árvore, a partir da raiz, da posição de
inserção, verificando, um a um, os bits da chave de busca até um
nodo externo ser atingido. Nesse ponto, há duas possibilidades:
1. Se o nodo externo atingido for vazio, um novo nodo externo
com a chave a ser inserida deve ser criado e inserido nesse
ponto.
2. Se o nodo externo atingido tiver a chave que tenta-se inserir,
o processo deve ser interrompido, porque chaves repetidas não
permitidas nessa trie.
3. Se o nodo externo atingido contiver uma chave distinta da
chave de busca, cria-se um novo nodo externo para conter a
chave a ser inserida, remove o nodo externo atingido pela busca
e no seu lugar instala uma subárvore de nodos internos cujas
folhas contêm a chave do nodo externo atingido e a nova chave,
e com tantos nodos internos nessa subárvore quanto forem os
bits do prefixo comum das duas chaves que colidem.
Para ilustrar o funcionamento do algoritmo de inserção de chaves
em trie binária, considere a inserção das seguintes chaves de 6-bits,
numerados de 1 a 6:
B 010010 C 010011 H 011000
J 100001 Q 101000 W 110110
K 100010
em uma trie inicialmente vazia.
As operações de inserção passo-a-passo são as seguintes:
222 CAPÍTULO 10. ÁRVORES PATRICIA

• Inserção da chave B = 010010:


Como a árvore está vazia, um nodo externo contendo B torna-
se seu nodo raiz:

• Inserção da chave C = 010011:


A busca por C na árvore acima atinge o nodo externo contendo
B = 010010, o qual diferencia-se de C apenas no sexto bit. Com
o prefixo comum 01001 das chaves B e C, deve-se construir uma
subárvore cujos nodos internos indicam os bits a ser testados e
cujas folhas são os nodos externos contendo B e C, e na qual o
nodo externo B é folha esquerda e C é direita, conforme deter-
minado pelo sexto bit de cada uma. Essa subárvore substitui
o nodo externo B original, produzindo a árvore:

• Inserção da chave H = 011000:


10.2. ÁRVORES TRIE 223

O caminho, desde a raiz, dirigido pelo prefixo 011 de H leva um


nodo sem subárvore direita, onde a chave H deve ser inserida
como um nodo externo, conforme a figura a seguir:

• Inserção da chave J = 100001:


A chave J é inserida à direita da raiz, como mostra a figura:
224 CAPÍTULO 10. ÁRVORES PATRICIA

Isso ocorre porque o primeiro bit de J é 1 e o ponteiro para


subárvore direita da raiz era nulo, devendo, nesse ponto, ser
criado um nodo externo contendo J.
• Inserção da chave Q = 101000:
O prefixo 1 de Q leva a colisão com a chave J = 100001 logo
após a inspeção de seu primeiro bit. Assim, o nodo externo
contendo J deve ser substituı́do por uma subárvore formada
pelos bits de número 2 e 3 de J e Q. O bit 3, que diferencia as
duas chaves, gera o nodo interno ao qual estão conectados dois
nodos externos contendo J e Q. A árvore resultante é a seguinte:

• Inserção da chave W = 110110:


O caminho, desde a raiz, dirigido pelo prefixo 11 de W leva a um
nodo sem subárvore direita. Nesse ponto deve-se inserir um
nodo externo contendo a chave W, conforme a figura a seguir:
10.2. ÁRVORES TRIE 225

• Inserção da chave K = 100010:


O prefixo 100 leva a colisão com J = 100001. Esse nodo externo
deve ser substituı́do pela subárvore formada pelos nodos inter-
nos correspondentes ao prefixo compartilhado pelas chaves:
226 CAPÍTULO 10. ÁRVORES PATRICIA

A árvore binária trie resultante da inserção das chaves B=010010,


C=010011, H=011000, J=100001, Q=101000, W=110110 e K=100010 é a se-
guinte:

O formato espacial das tries, diferentemente de árvores binárias


comuns, não depende da ordem em que as chaves são inseridas e sim
da sequência de bits que as formam. Há, contudo, a possibilidade
de formação de caminhos de uma só direção para chaves que têm
prefixos em comum. Por exemplo, se duas chaves diferirem somente
no último bit, elas formarão um caminho cujo comprimento é igual
ao tamanho delas, não importando quantas chaves existirem na
árvore. Esse problema é resolvido pelas árvores Patricia.

10.3 Conceito de árvore Patricia

O conceito de árvores Patricia foi concebido por D. R. Morrison, em


1968, que criou esse tipo de árvore para aplicações em recuperação
de informação em arquivo de grande porte.
10.3. CONCEITO DE ÁRVORE PATRICIA 227

O nome Patricia é um acrônimo baseado na seguinte frase em


Inglês: Practical Algorithm To Retrieve Information Coded In
Alphanumeric
Esse conceito recebeu de D. E. Knuth, em 1973, e R. Sedgewick,
em 1988, novos tratamentos, apresentando árvore Patricia como
um caso particular de árvores de pesquisa digital. Essencialmente,
a estrutura da árvore Patricia segue o espı́rito das tries, mas sem
apresentar a inconveniência de longos caminhos de uma só direção,
os quais são eliminados pela definição de que cada nodo interno da
árvore contém o ı́ndice do bit a ser testado para decidir qual ramo
tomar durante a pesquisa, e somente nodos internos corresponden-
tes a bits que diferenciam chaves são representados. A estrutura
de uma árvore Patricia é ilustrada pela seguinte figura:

onde nodos internos têm o ı́ndice do bit a ser testado e os nodos


externos contêm a chave ou o registro.
Os custos de pesquisa, inserção e remoção de chaves em árvores
Patricia dependem apenas do comprimento das chaves de busca,
sendo, portanto, O(1), em qualquer caso.
228 CAPÍTULO 10. ÁRVORES PATRICIA

Teorema 10.1 [Prefixo]Todas as chaves que estão abaixo de


um nodo cujo ı́ndice é k têm o mesmo prefixo de comprimento
k − 1.

Prova:
Por definição, o k-ésimo bit das chaves que estão abaixo do nodo
interno com ı́ndice k é o primeiro bit que as diferencia. Se assim
não fosse, haveria na árvore Patricia um nodo interno com ı́ndice
b menor que k − 1 separando chaves que somente se diferenciam a
partir do b-ésimo bit.

10.4 Transformação de trie em Patrı́cia

Inicialmente, uma árvore Patricia é construı́da a partir de uma


trie apenas para mostrar o relacionamento existente entre elas.
Posteriormente, o algoritmo para construção de árvores Patricia
diretamente a partir da chaves é apresentado em detalhes. Nesse
sentido, considere a seguinte árvore trie binária:
10.4. TRANSFORMAÇÃO DE TRIE EM PATRÍCIA 229

construı́da a partir da inserção das seguintes chaves de 6 bits:


B=010010 C=010011 H=011000 J=100001 Q=101000 K=100010.

O primeiro passo para transformar essa trie em Patricia é ar-


mazenar em cada nodo interno o ı́ndice do bit, a partir de 1, a ser
testado quando esse nodo for visitado pelo mecanismo de busca
por chaves. A árvore resultante é a seguinte:

Observe que a árvore acima possui nodos internos que têm ape-
nas uma subárvore e, por isso, pouco contribuem para diferenciar
chaves que possuem prefixos comuns, como os nodos 2, 4 e 5 da
subárvore esquerda e o nodo 4 da subárvore da direita da figura
acima. Esses nodos internos podem ser eliminados, reduzindo a
altura da árvore e, portanto, acelerando o algoritmo de pesquisa,
cujo custo é proporcional a essa altura. Após a eliminação desses
230 CAPÍTULO 10. ÁRVORES PATRICIA

nodos internos tem-se a seguinte árvore:

A pesquisa por uma chave em uma árvore Patricia é dirigida


por uma varredura nos bits da chave de busca e um caminhamento
associado na árvore. Como de praxe, inicia-se o processo pelo
nodo raiz, e, para cada nodo interno de rótulo i visitado, recupera-
se da chave de busca o seu i-ésimo bit, e com seu valor determina
se o caminhamento segue para a subárvore da esquerda ou para
a da direita. Esse caminhamento somente termina quando um
nodo externo for atingido. Nesse momento, deve-se testar a chave
armazenada nesse nodo externo com a chave de busca. Esse teste
é necessário, pois, no caminhamento realizado, nem todo bit da
chave de busca pode ter sido testado.
O custo máximo da operação de pesquisa é O(lg K), onde K é
o comprimento da chave, portanto o custo é O(1), pois independe
do número de chaves.
10.5. CONSTRUÇÃO DIRETA DE PATRICIA BINÁRIA 231

10.5 Construção direta de Patricia binária

Árvores Patricia podem ser construı́das diretamente sem a neces-


sidade de etapa de construção uma trie, pela inserção das chaves,
uma a uma, em uma árvore inicialmente vazia.
Para tornar as operações da árvore Patricia mais uniformes, su-
ponha que A seja uma chave especial que não está no conjunto de
chaves da aplicação e que o bit 0 de toda chave seja um bit extra
virtual acordado como 0. Os bits reais de cada chave são numera-
dos a partir de 1. Assim, define-se como vazia uma árvore Patricia
que tenha a configuração:

Para exemplificar o processo de inserção, suponha A:000000 e a


inserção das seguintes chaves de 6 bits:
B:001000 C:000100 D:101000 E:101001 F:100000 G:011000
123456 123456 123456 123456 123456 123456
as quais são inseridas na árvore passo-a-passo da seguinte forma:
• Inserção de B:001000:
O processo de inserção de B:001000 causa sua colisão com
A:000000, pois o bit 0 de ambas é 0. Como B 6= A, deve-se
inserir B na árvore em algum ponto do caminho da raiz até
o nodo externo atingido. Para isso, cria-se um nodo interno
contendo 3, que é o número de ordem do primeiro bit após o
prefixo comum de A e B. Como o bit 3 de B é 1, o nodo externo
criado para armazenar B é instalado como o ramo direito do
nodo interno criado. Do contrário, seria instalado no ramo es-
querdo. O ponteiro ainda vazio do nodo interno é usado para
232 CAPÍTULO 10. ÁRVORES PATRICIA

encadear o par de nodos criados na árvore. A figura a seguir


mostra o par de nodos criado e a árvore vazia.

O passo seguinte é a inserção do nodo interno identificado pelo


bit 3 no seu devido lugar no caminho da raiz até o nodo ex-
terno A, produzindo:

• Inserção de C:000100:
O percurso iniciado na raiz e dirigido pelos bits de C leva ao
nodo externo A:000000, pois os bits 0 e 3 de C têm valor 0, e
o bit 4 é que diferencia A de C, como destaca o par de nodos
criado, cujo indice de seu nodo interno é 4, conforme mostrado
na figura a seguir:
10.5. CONSTRUÇÃO DIRETA DE PATRICIA BINÁRIA 233

O ponteiro esquerdo do nodo interno identificado por 4 é usado


para instalar o par de nodos criado no seu devido lugar no
caminho que vai da raiz até A, conforme mostra a figura:

• Inserção de D:101000:

Como o único bit de D testado é 3, e B:001000 e D:101000 têm


esse bit igual 1, é preciso encontrar o primeiro bit dentro de
seus prefixos que diferencia seus caminhos. No caso, é o bit 1,
que vale 0 para B e 1 para D, a partir do qual tem-se o par de
nodos abaixo a ser inserido no caminho da raiz até B:
234 CAPÍTULO 10. ÁRVORES PATRICIA

A inserção do par criado acima produz a árvore:

• Inserção de E:101001:
O percurso iniciado na raiz leva ao nodo externo D:101000, pois
o bit 1 de D e de E tem valor 1, e primeiro bit que diferencia
essas chaves é o de ı́ndice 6, o que provoca a criação do par de
nodos mostrado na seguinte figura:
10.5. CONSTRUÇÃO DIRETA DE PATRICIA BINÁRIA 235

A instalação do par de nodos criado no seu devido lugar, no


caminho da raiz até D, produz:

• Inserção de F:100000:
O percurso iniciado na raiz leva ao nodo externo D:101000 e à
236 CAPÍTULO 10. ÁRVORES PATRICIA

criação do par de nodos mostrado na seguinte figura:

A instalação do par de nodos criado no seu devido lugar, no


caminho da raiz até D, produz:
10.5. CONSTRUÇÃO DIRETA DE PATRICIA BINÁRIA 237

• Inserção de G:011000:
O percurso iniciado na raiz leva ao nodo externo B = 001000 e
à criação do par de nodos mostrado na seguinte figura:

Após a inserção do par tem-se a árvore:


238 CAPÍTULO 10. ÁRVORES PATRICIA

Portanto, a árvore resultante da inserção das chaves B:001000,


C:000100, D:101000, E:101001, F:100000 e G:011000 é a seguinte:

10.6 Implementação de Patricia I

Na implementação a seguir de uma árvore Patricia todas as cha-


ves contidas nos itens nela instalados são cadeias de caracteres de
tamanho fixo, e os itens têm o tipo Item definido na Listagem 10.1.
1 public class Item {
2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 public String valor() {return "?";}
5 }

Listagem 10.1 Tipos de dados de Patricia

A árvore é projetada para armazenar objetos do tipo Item e seus


10.6. IMPLEMENTAÇÃO DE PATRICIA I 239

derivados. A fixação do tipo da chave e dos itens facilitam a apre-


sentação dos algoritmos e não representa perda de generalidade,
pois as adaptações necessárias a uma aplicação especı́fica são de
fácil realização.
1 public class Patricia {
2 private Nodo raiz;
3 private int tamanhoDaChave;
4 private interface Nodo {};
5 private Item item0;
6 private static class NodoInterno implements Nodo {
7 public int index;
8 public Nodo esq, dir;
9 public NodoInterno(int index) [Link] = index;
10 }
11 private static class NodoExterno implements Nodo {
12 public Item item;
13 public NodoExterno( Item item) {[Link] = item;}
14 }
15 private int bit(String chave, int k) {...}
16 private String checkChave(String chave)
17 throws ChInvalida {...}
18 private void imprima(Nodo t, int k) {...}
19 public Patricia(int tamanhoDaChave, Item item0) {...}
20 public class ChInvalida extends Exception {}
21 public Item pesquise(String chave)throws ChInvalida{...}
22 public void insira(Item item)
23 throws ChInvalida, [Link], [Link] {...}
24 public void retire(String chave) throws ChInvalida {...}
25 public void imprima() {...}
26 public void imprimaBits(String chave, int tamanho) {...}

Listagem 10.2 Tipo Abstrato Patricia

Na declaração da classe Patricia da Listagem 10.2, há dois tipos


de nodos: os internos e os externos, ambos são implementações da
interface Nodo. Nodos internos contêm o ı́ndice do bit da chave a
240 CAPÍTULO 10. ÁRVORES PATRICIA

ser testado durante caminhamentos e os ponteiros para suas res-


pectivas subárvores. Nodos externos armazenam apenas um item
de dados, o qual inclui uma chave.
As chaves são objetos do tipo String, como definido na Lista-
gem 10.1, cujo comprimento fixo, tamanhoDaChave, em caracteres, é
definido no momento de construção de um objeto Patricia.
A função construtora de Patricia, definida na Listagem 10.3,
recebe como parâmetro o número de caracteres de cada chave e um
item contendo uma chave especial, fora do conjunto de chaves da
aplicação, e constrói a árvore vazia com um nodo externo contendo
essa chave especial, de forma a tornar uniformes todas as operações
da árvore.
1 public Patricia(int tamanhoDaChave, Item item0) {
2 if (tamanhoDaChave < 1) tamanhoDaChave = 1;
3 this.item0 = item0;
4 [Link] = tamanhoDaChave;
5 NodoInterno u = new NodoInterno(0);
6 NodoExterno q = new NodoExterno(item0);
7 [Link] = q;
8 raiz = u;
9 }

Listagem 10.3 Função Construtora de Patricia

Assim, a árvore Patricia vazia criada pela construtora tem um


nodo interno contendo o ı́ndice 0, que vale 0 para toda chave, e o
nodo externo contendo a chave especial A, conforme exemplicado
pela seguinte figura:
10.6. IMPLEMENTAÇÃO DE PATRICIA I 241

A função bit da Listagem 10.4 devolve, dadas uma chave e uma


posição k, o valor do k-ésimo bit dessa chave, sendo o seu primeiro
bit o de ordem 1.
1 private int bit(String chave, int k) {
2 if (k <= 0) return 0;
3 int n = (k-1)/8;
4 int j = 7 - (k-1) % 8;
5 int a = [Link](n);
6 return ((a >>> j) & 1);
7 }

Listagem 10.4 Valor do k-ésimo Bits da Chave

Embora caracteres sejam armazenados em código unicode de


16-bits, a presente implementação somente considera os 8-bits de
baixa ordem de cada caractere, i.e., o código ascii correspondente, e
ignora seus oito bits de ordem mais alta. Assim, ignorando os bytes
de alta ordem de cada caractere, uma chave de vários caracteres
é vista como a sequência de bits como a da seguinte figura, onde
D = 8 * [Link]():

e os bits são numerados a partir de 1, bit 0 é por definição 0, todas


as chaves têm pelo menos um caractere, são de tamanho máximo
fixo e nenhuma é prefixo de outra.
A função checkChave, detalhada na Listagem 10.5, somente aceita
chaves não-nulas, de comprimento maior que zero, distintas da
chave especial A e de tamanho menor ou igual a tamanhoDaChave.
Chaves menores que esse tamanho são completadas com o caractere
branco.
242 CAPÍTULO 10. ÁRVORES PATRICIA

1 private String checkChave(String chave)throws ChInvalida {


2 if (chave == null) throw new ChInvalida();
3 if ([Link]() == 0) throw new ChInvalida();
4 if ([Link](chave)) throw new ChInvalida();
5 int t = [Link]();
6 if (t > tamanhoDaChave) throw new ChInvalida();
7 if (t < tamanhoDaChave) {
8 char[] c = new char[tamanhoDaChave-t];
9 for (int i=0; i<tamanhoDaChave - t; i++) c[i] = ’ ’;
10 chave = chave + new String(c);
11 }
12 return chave;
13 }

Listagem 10.5 Consistência da Chaves

Em uma árvore como a da figura abaixo, a operação pesquise,


definida na Listagem 10.6, nela caminha, a partir da raiz, conforme
os valores dos bits indicados pelo ı́ndice de cada nodo interno vi-
sitado, até atingir um nodo externo, quando o valor integral da
chave de busca deve ser comparado ao da chave nesse nodo.
10.6. IMPLEMENTAÇÃO DE PATRICIA I 243

1 public Item pesquise(String chave) throws ChInvalida {


2 Nodo t = raiz; NodoInterno p; NodoExterno q;
3 chave = checkChave(chave);
4 while (t instanceof NodoInterno) {
5 p = (NodoInterno) t;
6 if (bit(chave,[Link]) == 0)
7 t = [Link]; else t = [Link];
8 }
9 q = (NodoExterno) t;
10 if ([Link](chave)) return [Link];
11 else return null;
12 }

Listagem 10.6 Operação [Link]

O algoritmo de inserção de um item com chave k segue os se-


guintes passos:
1. Com base nos bits da chave k, percorra a árvore Patricia, a
partir da raiz, até encontrar um nodo externo de chave k 0.
2. Se k 0 for igual a k, o nodo já está na árvore, e a inserção
termina.
3. Senão, compare os bits de k com os de k 0 até encontrar o
primeiro bit, na posição b, que diferencia essas chaves.
4. Crie um nodo interno interno contendo b e um nodo externo
contendo o registro com k.
5. Forme um par de nodos, ligando o nodo interno ao externo
pelo apontador de subárvore esquerda ou direita, conforme o
valor do bit de ı́ndice b da chave k.
6. Determine o ponto de inserção do par de nodos na árvore per-
correndo, o caminho a partir de k 0, de baixo para cima, até
chegar a um nodo interno cujo ı́ndice seja menor que b.
244 CAPÍTULO 10. ÁRVORES PATRICIA

O custo do algoritmo de pesquisa em árvore Patricia é no máximo


o comprimento da chave. Na média, o custo tende a ser baixo, por-
que nem todos os bits das chaves são testados durante a busca.
O algoritmo de inserção de chaves na árvore, definido na Lis-
tagem 10.8, necessita retroceder no caminho percorrido durante a
descida na árvore para concretizar o processo de inserção, e, para
isso, usa o tipo Pilha<T> definido na Listagem 10.7.
1 public class Pilha<T> {
2 private int máximo;
3 private int topo = -1;
4 private T[] info;
5 public static class ExMax extends Exception {}
6 public static class ExMin extends Exception {}
7 public Pilha(int tamanho) {
8 máximo = (tamanho <= 1)? 0 : tamanho-1;
9 info = (T[]) new Object[máximo+1];
10 }
11 public void empilhe(T v) throws ExMax {
12 if (topo == máximo) throw new ExMax();
13 info[++topo]= v;
14 }
15 public T desempilhe() throws ExMin {
16 if(vazia()) throw new ExMin();
17 return info[topo--];
18 }
19 public boolean vazia() {
20 return (topo == -1);
21 }
22 }

Listagem 10.7 Tipos de dados de Patricia

O algoritmo insira consiste na busca de nodo externo que com-


partilhe prefixo com a chave de busca, criação de par de nodos,
contendo o nodo a ser inserido, e inserção desse par na árvore.
10.6. IMPLEMENTAÇÃO DE PATRICIA I 245

1 public void insira(Item item)


2 throws ChInvalida, [Link], [Link] {
3 String chave = checkChave([Link]);
4 int b, anterior;
5 Nodo v, t;
6 NodoInterno p, u; NodoExterno q;
7 Pilha<NodoInterno> s = new Pilha<NodoInterno>(50);
8 // Localize nodo externo t e primeiro bit da diferença
9 t = raiz; anterior = -1;
10 while (t instanceof NodoInterno) {
11 p = (NodoInterno) t;
12 [Link](p);
13 if ([Link] == anterior + 1) anterior++;
14 if (bit(chave,[Link])==0) t=[Link]; else t=[Link];
15 }
16 String chaveEncontrada = ((NodoExterno) t).[Link];
17 if ([Link](chaveEncontrada)) return;
18

19 // Crie par de nodos endereçado por u


20 b = anterior;
21 while (bit(chave,b) == bit(chaveEncontrada,b)) b++;
22 u = new NodoInterno(b);
23 q = new NodoExterno(item);
24 if (bit(chave,b)== 0) [Link] = q; else [Link] = q;
25

26 // Insira o par de nodos u na árvore


27 p = [Link]();
28 while ([Link] > b) p = [Link]();
29 if (bit(chaveEncontrada,[Link]) == 0) {
30 v = [Link]; [Link] = u;
31 } else v = [Link]; [Link] = u;
32 if (bit(chave,[Link]) == 0) [Link]=v; else [Link]=v;
33 }

Listagem 10.8 Operação [Link]


246 CAPÍTULO 10. ÁRVORES PATRICIA

Conforme a Listagem 10.8, a inserção de F na árvore do lado


esquerdo da figura abaixo gera o par de nodos identificado por u:

que é instalado na árvore, conforme mostra a seguinte figura:

Listagem 10.9 detalha a operação retire, a qual inicialmente


localiza o nodo externo que deve ser retirado juntamente com o
nodo interno que tem seu endereço, i.e., seu nodo pai. Se o item a
ser retirado não estiver na árvore, nada faz.
10.6. IMPLEMENTAÇÃO DE PATRICIA I 247

1 public void retire(String chave) throws ChInvalida {


2 Nodo a = raiz, q, t = raiz;
3 NodoInterno p, u;
4 NodoExterno r;
5 chave = checkChave(chave);
6 p = (NodoInterno) t;
7 while (t instanceof NodoInterno) {
8 a = p; p = (NodoInterno) t;
9 if (bit(chave,[Link])== 0) t=[Link]; else t=[Link];
10 }
11 r = (NodoExterno)t;
12 u = (NodoInterno) a;
13 if ([Link]([Link])) {
14 if (t == [Link]) q = [Link]; else q = [Link];
15 if (p == [Link]) [Link] = q; else [Link] = q;
16 }
17 }

Listagem 10.9 Operação [Link]

O nodo com o item a ser retirado é identificado pelo apontador


t, conforme ilustram as Figuras 10.10, 10.11, 10.12 e 10.13, que
apresentam os quatro contextos possı́veis para t, e onde a e p apon-
tam para o nodo avô e pai, respectivamente, do nodo a ser retirado.
Note que o uso de um nodo externo especial com o qual a árvore
Patricia vazia é configurada garante que uma das configurações
exibidas seja sempre possı́vel. As tranformações descritas a seguir
são realizadas pelas linhas 11 a 16 do algoritmo da Fig. 10.9.
A retirada do nodo t e do nodo interno que lhe está associado
são concretizadas pela modificação de apontador contido no nodo
avô a, conforme mostrado em cada uma das configurações exibidas.
Na Fig. 10.10, a subárvore irmã de t, que fica à sua direita,
torna-se a subárvore-esquerda do nodo avô a, e, nesse processo, o
248 CAPÍTULO 10. ÁRVORES PATRICIA

par formado pelo nodo interno de ı́ndice b e o nodo externo k é


removido da árvore.

(a) (b)

Figura 10.10 Caso I da Operação [Link]

Na Fig. 10.11, a subárvore irmã de t, que fica à sua esquerda,


torna-se a subárvore-esquerda de a, e o par de nodos contendo b e
k é removido.

(a) (b)

Figura 10.11 Caso II da Operação [Link]

Na Fig. 10.12, a subárvore irmã de t, que fica à sua esquerda,


torna-se a subárvore-direita de a, eliminando da árvore o par for-
mado por b e k.
10.6. IMPLEMENTAÇÃO DE PATRICIA I 249

(a) (b)

Figura 10.12 Caso III da Operação [Link]

E por último, na Fig. 10.13, a subárvore irmã de t, que fica à


sua direita, torna-se a subárvore-direita de a.

(a) (b)

Figura 10.13 Caso IV da Operação [Link]

Concluindo a implementação de Patricia, as operações imprima


e imprimaBits, definidas na Listagem 10.14, servem para dar su-
porte a atividades de manutenção da implementação. A árvore
é impressa como um texto, onde a hierarquia de nodos é defi-
nida por pares de parênteses definidos pelos arranjos colcheteL e
colcheteR, sendo que A{B}{C}, A[B][C], A(B)(C) e A<B><C> denotam
a mesma subárvore de raiz A e subárvores B e C. Diferentes pares de
250 CAPÍTULO 10. ÁRVORES PATRICIA

parênteses são usados para facilitar a leitura da árvore impressa.


1 private void imprima(Nodo t, int k) {
2 String[] colcheteL = {"","[","(","<"};
3 String[] colcheteR = {"","]",")",">"};
4 NodoInterno p;
5 NodoExterno q;
6 if (t == null) return;
7 if (t instanceof NodoInterno) {
8 p = (NodoInterno) t;
9 [Link]([Link]);
10 k = (k + 1) % [Link];
11 [Link](colcheteL[k]);
12 imprima([Link],k);
13 [Link](colcheteR[k] + colcheteL[k]);
14 imprima([Link],k);
15 [Link](colcheteR[k]);
16 return;
17 }
18 q = (NodoExterno) t;
19 [Link]([Link]());
20 }
21 public void imprima() {
22 imprima(raiz,-1);
23 [Link]();
24 }
25 public void imprimaBits(String chave, int tamanho) {
26 for (int i = 1; i <= tamanho; i++)
27 [Link](bit(chave,i));
28 }

Listagem 10.14 Operações de Impressão de chaves

A árvore Patricia armazena objetos do tipo Item, como MeuItem


definido na Listagem 10.15, que é usado no programa de teste
apresentado a seguir.
10.6. IMPLEMENTAÇÃO DE PATRICIA I 251

1 public class MeuItem extends Item {


2 public String valor;
3 public MeuItem(String chave, String valor) {
4 super(chave);
5 [Link] = valor;
6 }
7 public String valor() return valor;
8 }

Listagem 10.15 Tipos de dados de Patricia

O programa de teste UsaPatricia insere seis chaves em uma


árvore Patricia inicialmente vazia, na qual A é o valor da chave-
inicial, fora do conjunto de chaves da aplicação. A árvore cons-
truı́da após cada inserção é impressa para conferência, e o pro-
grama termina removendo pela ordem definida na linha 3 todas as
chaves.

1 public class UsaPatricia {


2 public static void main(String[] args) throws
3 int ordem[] = {0, 6, 5, 2, 4, 1, 3};
4 String[] valor = {"A","B","C","D","E","F","G"};
5 String[] chaves= {"\u0000","\u0020","\u0010","\u00A0",
6 "\u00A4","\u0080","\u0060"};
7 MeuItem meusItens[] = new MeuItem[[Link]];
8 for (int i=0; i< [Link]; i++)
9 meusItens[i] = new MeuItem(chaves[i],valor[i]);
10 Patricia f = new Patricia(1,meusItens[0]);
11 [Link]("Patricia vazia: ");
12 [Link]();
13 [Link]("Inserç~
ao de itens ----------:");
14 for (int i=1; i < [Link]; i++) {
15 [Link](meusItens[i]);
16 [Link](
17 "Inserido " + meusItens[i].valor + ":");
18 [Link](meusItens[i].chave,8);
252 CAPÍTULO 10. ÁRVORES PATRICIA

19 [Link](); [Link]();
20 }
21 [Link]("Remoç~
ao de itens -----------:");
22 for (int i=1; i < [Link]; i++) {
23 [Link](meusItens[ordem[i]].chave);
24 [Link](
25 "Retirado " + meusItens[ordem[i]].valor);
26 [Link]();
27 }
28 }
29 }

O resultado impresso do programa de teste, que mostra o passo-


a-passo das operações de inserção e remoção de itens, é o seguinte:
Patricia vazia: 0{A}{}
Inserç~ao de itens ----------:
Inserido B:00100000
0{3[A][B]}{}
Inserido C:00010000
0{3[4(A)(C)][B]}{}
Inserido D:10100000
0{1[3(4<A><C>)(B)][D]}{}
Inserido E:10100100
0{1[3(4<A><C>)(B)][6(D)(E)]}{}
Inserido F:10000000
0{1[3(4<A><C>)(B)][3(F)(6<D><E>)]}{}
Inserido G:01100000
0{1[2(3<4{A}{C}><B>)(G)][3(F)(6<D><E>)]}{}
Remoç~
ao de itens -----------:
Retirado G
0{1[3(4<A><C>)(B)][3(F)(6<D><E>)]}{}
Retirado F
0{1[3(4<A><C>)(B)][6(D)(E)]}{}
Retirado C
0{1[3(A)(B)][6(D)(E)]}{}
Retirado E
0{1[3(A)(B)][D]}{}
10.6. IMPLEMENTAÇÃO DE PATRICIA I 253

Retirado B
0{1[A][D]}{}
Retirado D
0{A}{}

E a árvore resultante da inserção das chaves B:001000, C:000100,


D:101000, E:101001, F:100000 e G:011000 em uma árvore vazia é
descrita pela expressão
0{1[2(3<4{A}{C}><B>)(G)][3(F)(6<D><E>)]}{}

a qual é a representação textual da árvore apresentada graficamente


na Fig. 10.16.

Figura 10.16 Uma Árvore Patricia


254 CAPÍTULO 10. ÁRVORES PATRICIA

10.7 Chaves de tamanhos variados

A implementação da árvore Patricia da Seção 10.6 requer que todas


as chaves tenham o mesmo tamanho, porque o algoritmo usado
precisa identificar o primeiro bit, da esquerda para a direita, que
diferencia os prefixos de duas chaves. Como o bit da diferença
pode ser o último da maior chave, em princı́pio, todas as chaves
deve ter o tamanho da maior delas. Para garantir essa condição, no
algoritmo apresentado, chaves menores que o máximo estabelecido
são completadas com o caractere branco.
Se chaves pudessem ter tamanhos diferentes, poderia ocorrer
que uma seja prefixo de outra, não havendo como diferenciá-la,
exceto pelos seus tamanhos, mas isso não faz parte do algoritmo
tradicional de árvores Patricia. Uma solução simples para permitir
tamanhos diferentes, é completar virtualmente os bits das chaves
menores quando necessário. Para ilustrar essa técnica, considere o
seguinte conjunto de chaves de tamanho variando de 2 a 4 bits:
A:00 B:01 C:10 D:0000 E:0001 F:0010 G:1000
Chave A nunca ocorre na aplicação e é usada como uma chave espe-
cial para construir uma árvore conceitualmente vazia. No exemplo
a seguir, para simplicar a apresentaçao, chaves são tratadas como
cadeias de pelo menos um caractere, os caracteres de cada chave
tenham apenas dois bits (00,01,10) e o caractere de código 11 não
faz parte de chave alguma. Em uma implementação real, caracteres
são códigos ascii de 8 bits.
Assim, uma árvore inicialmente vazia é a seguinte:
10.7. CHAVES DE TAMANHOS VARIADOS 255

A inserção das chaves acima seguem o seguinte passo-a-passo:


• O processo de inserção da chave B:01 leva ao nodo externo
A:00:

sendo 2 o ı́ndice do bit da diferença entre A e B, conforme


indicado pelo par de nodos a ser adicionado:

para produzir:

• O processo de inserção da chave C:10 também leva ao nodo


A:00:

sendo agora 1 o ı́ndice do bit que as diferencia, conforme indica


o par de nodos criado:
256 CAPÍTULO 10. ÁRVORES PATRICIA

e a inclusão do par de nodos criado na árvore produz:

• O processo de inserção da chave D:0000 leva ao nodo A:00, con-


forme mostra a seguinte figura:

Nesse caso, 3 é o ı́ndice do bit da diferença entre A:00 e D:0000,


porque bits além do comprimento de qualquer chave é virtu-
almente definido como 1. O valor de A não é alterado, apenas
10.7. CHAVES DE TAMANHOS VARIADOS 257

bits além do seu comprimento são imaginados com valor 1.


Assim, o par de nodos criado para resolver o conflito entre A e
D tem seu nodo interno com rótulo 3, conforme mostra a figura:

e a inclusão na árvore do par de nodos criado para diferenciar


A e D produz:

• O processo de inserção da chave E:0001 leva ao nodo externo


D:0000:
258 CAPÍTULO 10. ÁRVORES PATRICIA

e como 4 é o ı́ndice do primeiro bit que diferencia D:0000 e


E:0001, deve-se gerar o par de nodos mostrado na figura abaixo,
o qual deve ser devidamente inserido na árvore no caminho da
raiz até D:

Após a inserção do par de nodos entre o nodo interno 3 e o


externo D, tem-se o seguinte resultado:
10.7. CHAVES DE TAMANHOS VARIADOS 259

• O processo de inserção da chave F:0010 leva a uma colisão com


o nodo externo contendo a chave A:00, pois, por definição, o
bit de ı́ndice 3 de A é virtualmente 1, conforme o caminho
destacado na seguinte figura:

Nesse caso, para achar o bit da diferença entre F:0010 e A:00,


esta chave deve ser vista como virtualmente expandida para
260 CAPÍTULO 10. ÁRVORES PATRICIA

A:0011, gerando o par de nodos indicado na figura a seguir:

a qual, após o par de nodos contendo F ser devidamente inse-


rido entre o nodo interno 3 e o externo A, torna-se:
10.7. CHAVES DE TAMANHOS VARIADOS 261

• O processo de inserção da chave G:1000 leva ao nodo externo


C:10, conforme mostra a figura:

Nesse caso, o ı́ndice do bit da diferença é 3, pois o terceiro bit


de C é o virtual 1, devendo ser criado o par de nodos mostrado
na figura a seguir, o qual deve ser inserido entre o nodo interno
1 e o externo C:
262 CAPÍTULO 10. ÁRVORES PATRICIA

produzindo o seguinte resultado:

Em conclusão, a árvore Patricia resultante da inserção das cha-


ves B:01, C:10, D:0000, E:0001, F:0010 e G:1000 em uma árvore vazia
é a da seguinte figura:
10.8. IMPLEMENTAÇÃO DE PATRICIA II 263

10.8 Implementação de Patricia II

Listagem 10.17 define a estrutura do tipo abstrato de dados Patricia,


que opera com chaves de tamanho variados.

1 public class Patricia {


2 private Nodo raiz;
3 private interface Nodo {};
4 private Item item0;
5 private static class NodoInterno implements Nodo {
6 public int index;
7 public Nodo esq, dir;
8 public NodoInterno(int index) {[Link] = index;}
9 }
10 private static class NodoExterno implements Nodo {
11 public Item item;
12 public NodoExterno( Item item) {[Link] = item;}
13 }
14 private int bit(String chave, int k) {...}
15 private void checkChave(String chave)
16 throws ChInvalida {...}
17 private void imprima(Nodo t, int k) {...}
18 public Patricia(Item item0) {...}
19 public class ChInvalida extends Exception {}
20 public Item pesquise(String chave)
21 throws ChInvalida {...}
22 public void insira(Item item)
23 throws ChInvalida, [Link], [Link] {...}
24 public void retire(String chave)throws ChInvalida {...}
25 public void imprima() {...}
26 public void imprimaBits(String chave, int nBits) {...}
27 }

Listagem 10.17 Tipo Abstrato Patricia

Nesta implementação de Patricia, chaves são cadeias de carac-


teres restritos por programação ao conjunto ascii, podem ter um
264 CAPÍTULO 10. ÁRVORES PATRICIA

tamanho qualquer, mas devem conter pelo menos um caractere


e uma chave pode ser prefixo de outras. Teoricamente todas as
chaves são vistas como sendo do tamanho da maior delas, por-
que consulta a bits além de seu comprimento retorna sempre 1.
Para implementar esse efeito e evitar ambiguidade, o caractere as-
cii ‘\u00FF’ é tratado como inválido, e, portanto, não pode compor
qualquer chave.
Cada chave é tratada como uma cadeia de bits, na qual os do
byte esquerdo de cada caractere unicode é ignorado, e os bits da
chave são indexados sequencialmente por sua ordem dentro do
string de bits, de tal forma que o bit mais à esquerda do byte
direito de primeiro caractere unicode da chave tem ordem 1, e o
primeiro bit do segundo caractere da chave tem ordem 9, e assim
em diante. O número total de bits de uma chave x é 8*[Link]().
Os tipos Item e Pilha são exatamente os homônimos definidos
nas listagens 10.1 e10.7.
As operações impactadas pela liberação do limite de tamanho de
cada chave são bit, checkChave, e a função construtora de Patricia.
A operação bit foi alterada para retornar o valor 1 para toda con-
sulta ao bit além do limite de tamanho da chave, como mostra a
Listagem 10.18.

1 private int bit(String chave, int k) {


2 if (k <= 0) return 0;
3 int n = (k-1)/8;
4 if (n < [Link]()) {
5 int j = 7 - (k-1) % 8;
6 int a = [Link](n);
7 return ((a >>> j) & 1);
8 } else return 1;
9 }

Listagem 10.18 Operação [Link]


10.8. IMPLEMENTAÇÃO DE PATRICIA II 265

A operação checkChave consiste a chave, a qual não deve conter o


caracter ‘\u00FF’, que é usado para estender virtualmente chaves
menores, como mostra a Listagem 10.19.

1 private void checkChave(String chave) throws ChInvalida {


2 if (chave == null) throw new ChInvalida();
3 if ([Link]() == 0 ) throw new ChInvalida();
4 if ([Link](0x00FF) >= 0) throw new ChInvalida();
5 if ([Link](chave)) throw new ChInvalida( );
6 }

Listagem 10.19 Operação [Link]

A função construtora da Listagem 10.20 apenas inicia a árvore


com um nodo externo especial que lhe é passado como parâmetro,
assim criando a Patricia vazia. A chave desse item especial não
pode ser usada para identificar qualquer outro item na aplicação.

1 public Patricia(Item item0) {


2 this.item0 = item0;
3 NodoInterno u = new NodoInterno(0);
4 NodoExterno q = new NodoExterno(item0);
5 [Link] = q;
6 raiz = u;
7 }

Listagem 10.20 Operação Construtora de Patricia

As demais operações do tipo abstrato Patricia, i.e., pesquise,


insira, retire, imprima e imprimaBits, são exatamente as homôni-
mas definidas nas listagens 10.6, 10.8, 10.9 e 10.14 da implementação
de Patricia com chaves de tamanho fixo.
O programa de teste desta implementação de árvore Patricia usa
o tipo MeuItem, derivado de Item e definido na Listagem 10.15, e
exercita a inclusão e remoção de chaves de vários tamanhos:
266 CAPÍTULO 10. ÁRVORES PATRICIA

1 public class UsaPatricia {


2 public static void main(String[] args) throws
3 [Link], [Link], [Link] {
4 MeuItem item0 = new MeuItem("\u0000","A");
5 String[] valor = {"B", "C", "D", "E", "F", "G"};
6 String[] chaves = {
7 "\u0010" + "\u0010","\u0010" + "\u0010" + "\u0010",
8 "\u0010" + "\u0011",
9 "\u00A4" + "\u0020" + "\u0010" + "\u1010","\u0080",
10 "\u00A4"+"\u0020"+"\u0010"+"\u1010"+"\u0011"};
11 MeuItem meusItens[] = new MeuItem[[Link]];
12 for (int i=0; i< [Link]; i++)
13 meusItens[i] = new MeuItem(chaves[i],valor[i]);
14 Patricia f = new Patricia(item0);
15 [Link]("Patricia vazia: ");
16 [Link]();
17 [Link]("Inserç~
ao de itens-----------:");
18 for (int i=0; i < [Link]; i++) {
19 [Link](meusItens[i]);
20 [Link](
21 "Inserido " + meusItens[i].valor + ":");
22 [Link](meusItens[i].chave,
23 8*meusItens[i].[Link]());
24 [Link](); [Link]();
25 }
26 [Link]("Remoç~
ao de itens ----------:");
27 int ordem[] = {5, 4, 1, 3, 0, 2};
28 for (int i=0; i < [Link]; i++) {
29 [Link](meusItens[ordem[i]].chave);
30 [Link](
31 "Retirado " + meusItens[ordem[i]].valor);
32 [Link]();
33 }
34 }
35 }
10.9. PATRICIA M -ÁRIAS 267

A saı́da do programa de teste é:


Patricia vazia: 0{A}{}
Inserç~ao de itens--------------:
Inserido B:0001000000010000
0{4[A][B]}{}
Inserido C:000100000001000000010000
0{4[A][17(C)(B)]}{}
Inserido D:0001000000010001
0{4[A][16(17<C><B>)(D)]}{}
Inserido E:10100100001000000001000000010000
0{1[4(A)(16<17{C}{B}><D>)][E]}{}
Inserido F:10000000
0{1[4(A)(16<17{C}{B}><D>)][3(F)(E)]}{}
Inserido G:1010010000100000000100000001000000010001
0{1[4(A)(16<17{C}{B}><D>)][3(F)(33<G><E>)]}{}
Remoç~
ao de itens -------------:
Retirado G
01[4(A)(16<17CB><D>)][3(F)(E)]
Retirado F
0{1[4(A)(16<17{C}{B}><D>)][E]}{}
Retirado C
0{1[4(A)(16<B><D>)][E]}{}
Retirado E
0{4[A][16(B)(D)]}{}
Retirado B
0{4[A][D]}{}
Retirado D
0{A}{}

10.9 Patricia m-árias

Frequentemente, as chaves de uma árvore Patricia são processadas


com base em seus caracteres em vez de seus bits. Nesse caso, as
árvores deixam de ser binárias, passando a ser m-árias, onde m é
tamanho do conjunto de caracteres que podem compor as chaves.
268 CAPÍTULO 10. ÁRVORES PATRICIA

Nesse modelo, todo nodo interno tem m ramos, um para caractere


do conjunto.
O princı́pio de funcionamento de árvores Patricia m-ária é o
mesmo das árvores binárias correspondentes, mas alguns cuidados
precisam ser tomados, como são revelados no exercı́cio que se segue,
no qual, com o objetivo de simplicar a apresentação do método,
supõe-se um conjunto de apenas três caracteres para formar chaves,
chamados de A, B e C.
Para esse exercı́cio de inserção em árvore Patricia ternária, su-
ponha que a árvore vazia seja:

onde o caractere de ordem 0 de toda chave seja por definição A. Isto


significa que os ramos B e C do nodo raiz nunca serão exercitados.
Considere que as seguintes chaves de cinco caracteres:
X1:CBCAA X2:CBABA X3:ACACA X4: CBABC X5: ABCAB X6: AAAAB
X7:CCACC X8:BBBAA X9:CAAAA X10:CBBAA X11:CBABB X12:CBAAC
As quais são identificadas pelos rótulos Xi, para 1 ≤ i ≤ 12, e
devam ser inseridas, nessa ordem, numa árvore Patricia ternária
inicialmente vazia. Os passos de inserção são os seguintes:
• Inserção de X1:
Como o caractere 0 de toda chave é supostamente A, a busca
por X1 leva ao ramo rotulado A:

que é o local de instalação de um nodo externo contendo a


chave X1 como mostra a figura:
10.9. PATRICIA M -ÁRIAS 269

• Inserção de X2:
A busca por X2 leva ao nodo externo X1. Analogamente ao
algoritmo para árvore binária, deve-se identificar o ı́ndice do
primeiro caractere mais à esquerda que diferencia X1 e X2, que
no caso é o ı́ndice 3, que em X1 é C e em X2 é A. Deve-se então
criar um par de nodos composto de um nodo interno contendo
3 e um externo contendo X2 e ligado ao nodo interno pelo ramo
A, como mostrado na figura a seguir.

Note que o par de nodos deve ser ligado à arvore Patricia via
o ramo C, porque o terceiro caractere de X1 é C, para produzir:
270 CAPÍTULO 10. ÁRVORES PATRICIA

• Inserção de X3:
A busca por X3 leva ao nodo externo X2, sendo 1 o ı́ndice do
primeiro caractere que diferencia essas chaves, como mostrado
na figura:

E o par de nodos criado é instalado no caminho da raiz até X2


via o ramo C do nodo interno do par:

• Inserção de X4:
A busca por X4 leva ao nodo X2, sendo 5 o ı́ndice do caractere
que diferencia seus prefixos, levando a criação do par de nodos
mostrado na figura:
10.9. PATRICIA M -ÁRIAS 271

Após a inserção do par na árvore via seu ramo rotulado por A,


tem-se:

• Inserção de X5:
Para X5, a busca de seu local de inserção leva a X3, sendo 2 o
ı́ndice do caractere de separação de prefixos, e à criação do par
de nodos da figura:
272 CAPÍTULO 10. ÁRVORES PATRICIA

A inserção do nodo interno do par no caminho da raiz ao nodo


externo X3 produz:

• Inserção de X6:
A busca por X6 leva a ramo vazio rotulado por A do nodo
interno 2:
10.9. PATRICIA M -ÁRIAS 273

Como a posição de instalação está livre, basta conectar nesse


ponto um nodo externo contendo X6:

• Inserção de X7:
Para a inserção de X7, cria-se o par de nodos com o seu nodo
interno rotulado por 2, como mostrado na seguinte figura:
274 CAPÍTULO 10. ÁRVORES PATRICIA

O par criado deve ser incorporado à árvore via o ramo vazio B


do nodo interno entre os nodos internos de rótulos 1 e 3 perten-
centes ao caminho da raiz até o nodo externo X4, produzindo
a árvore:
10.9. PATRICIA M -ÁRIAS 275

• Inserção de X8:
O processo de inserção de X8 leva a uma posição vaga:

onde x8 pode ser instalado diretamente:


276 CAPÍTULO 10. ÁRVORES PATRICIA

• Inserção de X9:
O mesmo ocorre com X9:

produzindo a árvore:
10.9. PATRICIA M -ÁRIAS 277

• Inserção de X10:
Idem para X10:

que produz:
278 CAPÍTULO 10. ÁRVORES PATRICIA

• Inserção de X11:
E também com X11:

que produz o resultado final:


10.10. CONCLUSÃO 279

A extensão desse algoritmo exercitado para operar com um con-


junto de caracteres realista é fácil de ser feita: os apontadores de
subárvores dos nodos internos devem ser um arranjo do tamanho
do conjunto de caracteres permitidos.

10.10 Conclusão

Pesquisa digital é um método de pesquisa em estruturas que ar-


mazenam um conjunto de chaves e que faz uso da representação
dessas chaves, as quais podem ser uma sequência de caracteres ou
de bits, para localizar os registros ou itens desejados.
Os métodos de pesquisa digital são particularmente vantajosos
quando as chaves são longas e de tamanhos variados.
Árvore Patricia é um tipo de pesquisa digital baseado em uma
estrutura que permite eficiente recuperação de informação com cus-
tos proporcionais ao comprimento das chaves de pesquisa, as quais
podem ser de tamanho variável, facilitando acesso eficiente a ban-
cos de dados não-estruturados.

Exercı́cios
1. Como os algoritmos de inserção, pesquisa e remoção de uma
árvore Patricia devem ser modificados para funcionar com
chaves repetidas?

Notas Bibliográficas

Árvores de pesquisa digital foram inventadas por E.G. Coffman e


J. Eve em 1970. Árvores Patricia foram criadas por Morrison D.
R. em 1968, como um mecanismo para aplicações em recuperação
de informação em arquivos de grande porte. D. E. Knuth em 1973
deu novo tratamento ao algoritmo de Morrison, reapresentando-o
280 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

de forma mais clara, como um caso particular de pesquisa digital,


essencialmente, um caso especial de árvore trie binária. Mais tarde
R. Sedgewick, em 1988, apresentou novos algoritmos de pesquisa e
de inserção baseados nos algoritmos propostos por Knuth.
O texto fundamental para um estudo aprofundado sobre árvores
Patricia são os livros clássicos de Donald Knuth [6, 7], que dão um
tratamento formal e organizado para a matéria.
Hoje em dia há inúmeros textos sobre o assunto, como os de
autoria de Alfred Aho [2], Michel Goodrich [13], Roberto Sedgewick
[20], Thomas Cormen [22] e Nivio Ziviani [14].
***
Capı́tulo 11

Árvores Binárias de Pesquisa

Even the best tree sometimes has bad fruit 1


Hungarian Proverb

Seção 2.5 do Capı́tulo 2 apresenta o conceito de dicionário, o qual


foi definido como um tipo abstrato caracterizado pelas operações
(i) insira, para inserir um novo item, (ii) pesquise, para localizar
um item a partir de uma chave, (iii) retire, para remover um item
dada sua chave e (iv) próximo, que, a cada chamada, retorna o
próximo item na ordem do valor crescente de sua chave.
Na implementação de um tipo dicionário, pode-se usar uma das
seguintes estruturas de dados para representar seus objetos:
• Listas lineares
• Tabelas ordenadas
• Tabelas de dispersao
• Árvores de pesquisa digital
• Árvores trie
• Árvores Patricia
• Árvores binárias de pesquisa
• Árvores balanceadas: AVL, SBB, Rubro-Negras, B
Este capı́tulo define árvores binárias de pesquisa, descreve suas
1
Até a melhor árvore, às vezes, tem fruto ruim

281
282 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

implementações e avalia o desempenho e custo das operações de


dicionários por elas representados.

11.1 Conceito de árvores binárias de pesquisa

Definição 11.1 Uma árvore binária de pesquisa é uma árvore


binária que armazena os itens de um dicionário em seus nodos
de forma que:
• Todo nodo interno contém um item de informação.
• Os nodos da subárvore à esquerda de um nodo interno
têm chaves menores que a desse nodo interno.
• Os nodos da subárvore à direita de um nodo interno têm
chaves maiores que a desse nodo interno.
Essencialmente, dada uma estrutura R(E)(D), onde R representa
um nodo interno de uma árvore binária de pesquisa, E e D, suas
subárvores, tem-se a relação invariante de que toda chave em E é
menor que a chave em R, que, por sua vez, é maior que todas as
chaves em D.
Árvores binárias de pesquisa são uma estrutura de dados que
pode ser muito eficiente para armazenar os itens de dicionários. É
particularmente adequada quando existe necessidade de considerar
alguma combinação dos seguintes requisitos:
• acesso direto e sequencial eficientes
• facilidade de inserção e retirada de registros
• boa taxa de utilização de memória
• utilização de memória primária e secundária.
A figura abaixo é um exemplo de árvore binária de pesquisa, na
qual os nodos internos estão rotulados pelo valor de suas chaves, e
os nodos retangulares representam apontadores nulos.
11.2. TIPO ABSTRATO DE DADOS ABP 283

O caminhamento em ordem central nessa árvore percorre os itens


armazenados em ordem crescente dos valores das chaves.
E o custo da pesquisa por uma chave em uma árvore binária de
pesquisa com n nodos internos depende do seu formato:
• Em uma árvore bem equilibrada, como a do exemplo acima, o
custo é O(lg n).
• Em árvores degeneradas, como uma que todo nodo interno
tem no máximo uma subárvore, o custo sobe para O(n).

Assim, em aplicações em que um bom desempenho da pesquisa


por itens armazenados deve ser garantido, as operações de inserção
e remoção de nodos numa árvore de pesquisa devem cuidar para
manter a árvore sempre equilibrada.

11.2 Tipo abstrato de dados Abp

A árvore binária de pesquisa apresentada nesta seção destina-se a


armazenar objetos do tipo Item que está definido na Listagem 11.1,
o qual contém uma chave de identificação que pode ser qualquer
cadeia de caracteres não nula, exceto a que for definida pelo usuário
como identificador de um nodo especial, o qual é usado para criar
a árvore binária vazia e nunca pode ser removido.
284 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

1 public class Item {


2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 public String valor() {return "?";}
5 }

Listagem 11.1 Item do Dicionário Abp

O tipo abstrato de dados Abp, que implementa o dicionário


usando árvore binária de pesquisa, é caracterizado pelas seguin-
tes operações:
• Abp(Item item0):
construtora que cria uma árvore conceitualmente vazia com
um único nodo contendo item0, o qual tem uma chave espe-
cial escolhida pelo usuário do tipo. A inclusão desse nodo na
árvore assegura uniformidade em diversas operações, simplifi-
cando seus algoritmos.
• Item pesquise(String chave) throws ChInvalida:
retorna o item da árvore com a chave dada ou retorna null se
não for encontrado.
• void insira(Item item) throws ChInvalida:
insere novo item na árvore, não permitindo duplicação.
• Item retire(String chave) throws ChInvalida:
remove da árvore o item com a chave dada e o retorna. Se o
item não for encontrado, retorna null.
• void reiniciePróximo():
assegura que o iterador definido pela operação próxima esteja
pronto para devolver o primeiro item da sequência.
• Item próximo() throws [Link], [Link]:
a cada chamada retorna o próximo item da árvore em ordem
crescente do valor das chaves, de acordo com um caminha-
mento em ordem central.
11.2. TIPO ABSTRATO DE DADOS ABP 285

• void imprima():
imprime a árvore em forma textual para ajudar no processo
de depuração da implementação.

A implementação do tipo Abp tem a estrutura descrita na Lis-


tagem 11.2, onde raiz é o apontador para a árvore, item0, o nodo
especial definido, e a, um apontador usado pela operação próximo.

1 public class Abp {


2 private Nodo a, raiz;
3 private Item item0;
4 private static class Nodo {
5 public Item item;
6 public Nodo esq, dir;
7 public Nodo(Item item) {[Link] = item;}
8 }
9 private Pilha<Nodo> pilha = new Pilha<Nodo>();
10 public class ChInvalida extends Exception {}
11 private void checkChave(String chave)
12 throws ChInvalida {...}
13 private void imprima(Nodo t, int k) {...}
14 public Abp(Item item0) {...}
15 public Item pesquise(String chave)throws ChInvalida{...}
16 public void insira(Item item) throws ChInvalida {...}
17 public Item retire(String chave) throws ChInvalida {...}
18 public void reiniciePróximo(){[Link](); a = raiz;}
19 public Item próximo()throws [Link],[Link]{...}
20 public void imprima() {...}
21 }

Listagem 11.2 Item do Usuário do Dicionário Abp

A operação checkChave da Listagem 11.3 é usada para garantir


a validade das chaves, que não podem ser cadeias de caracteres
nulas ou iguais à chave especial definida para item0, definida no
momento de criação da árvore pela função construtora.
286 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

1 private void checkChave(String chave) throws ChInvalida {


2 if (chave == null) throw new ChInvalida();
3 if ([Link]() == 0) throw new ChInvalida();
4 if ([Link]([Link])) throw new ChInvalida();
5 }

Listagem 11.3 Consistência da Chaves

A função construtora definida na Listagem 11.4 cria a árvore


vazia consistindo de um nodo especial contendo um item que não
faz parte do dicionário da aplicação. Esse item é fornecido como
parâmetro. O campo a iniciado na linha 4 é usado pela operação
próxima para prosseguir na iteração sobre os itens armazenados.

1 public Abp(Item item0) {


2 this.item0 = item0;
3 raiz = new Nodo(item0);
4 a = raiz;
5 }
6

Listagem 11.4 Construtora de Abp

A operação pesquise está descrita na Listagem 11.5.

1 public Item pesquise(String chave) throws ChInvalida {


2 Nodo p = raiz; checkChave(chave);
3 while (p != null) {
4 if ([Link]([Link]) < 0) p = [Link];
5 else if ([Link]([Link]) > 0) p = [Link];
6 else return [Link];
7 }
8 return null;
9 }

Listagem 11.5 Operação [Link]


11.2. TIPO ABSTRATO DE DADOS ABP 287

Essa operação percorre a árvore, a partir da raiz, comparando


as chaves de cada nodo com a chave de pesquisa, e avançando para
esquerda ou direita, até que o nodo que contenha um item com
a chave dada seja encontrado ou o caminho se encerre. O valor
retornado é o item encontrado ou o valor null, se ele não estiver
na árvore.
O custo médio da operação pesquise é proporcional à metade da
altura da árvore. Se a árvore for balanceada, o custo é O(lg n), para
uma árvore com n itens, mas, para uma árvore não balanceada,
pode-se ter custo O(n) no pior caso.

1 public void insira(Item item) throws ChInvalida {


2 Nodo pai = null, p = raiz;
3 String chave = [Link];
4 checkChave([Link]);
5 while (p != null) {
6 pai = p;
7 if ([Link]([Link]) < 0) p = [Link];
8 else if ([Link]([Link])>0) p = [Link];
9 else return; // chave já está na abp
10 }
11 p = new Nodo(item);
12 if ([Link]([Link]) < 0) [Link] = p;
13 else [Link] = p;
14 }

Listagem 11.6 Operação [Link]

A operação insira da Listagem 11.6 faz uma pesquisa pelo nodo


que deve tornar-se o pai do nodo a ser inserido. Para isso, faz uma
caminhada na árvore no estilo do algoritmo da operação pesquise
até que seja encontrado um nodo cujo apontador a ser seguido seja
nulo. Esse é justamente o ponto de inserção do novo item, como
detalha o algoritmo de insira. Tentativas de inserção de itens
288 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

repetidos são ignoradas. Por exemplo, a inserção de um item com


chave I na seguinte árvore:

produz a nova árvore:

O custo de insira é o mesmo do pesquisa, haja vista que o ponto


de inserção precisa ser localizado.
A retirada de um item da árvore não é tão simples quanto a
inserção, pois há situações que requerem rearranjo da estrutura.
Se o nodo que contém o item a ser retirado possuir no máximo
um descendente, então a operação é simples, pois o pai do nodo
retirado adota o nodo que vai ficar órfão. Entretanto, no caso
de o nodo a ser retirado possuir dois descendentes, ele deve ser
preservado, devendo seu item ser substituı́do pelo item do nodo
11.2. TIPO ABSTRATO DE DADOS ABP 289

mais à direita na subárvore esquerda ou pelo item do nodo mais à


esquerda na subárvore direita. Ou seja, nodo que contém o item a
ser removido deve ser preenchido com o item de seu antecessor ou
sucessor. E então pode-se retirar fisicamente o nodo que continha
o antecessor ou o sucessor. Assim, há dois casos a considerar no
processo de remoção de um item, como ilustrado na remoção das
chaves D ou M nas figuras que se seguem:
• Caso I: remoção de nodo com até um filho:
Nesse caso, o avô adota o neto que está ficando órfão, usando
o apontador do nodo a ser eliminado, e o nodo que contém o
item a ser removido é eliminado. São quatro as configurações
possı́veis:
– Remoção de D - Situação I:

– Remoção de D - Situação II:


290 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

– Remoção de D - Situação III:

– Remoção de D - Situação IV:

Em resumo, nesses casos, o avô adota o neto que está ficando


órfão, e o nodo que contém o item a ser removido pode ser
eliminado.

• Caso II: remoção de nodo com dois filhos:


Nesse caso, o nodo que contém o item a ser retirado não pode
ser removido da árvore porque não há como o seu pai adotar os
dois filhos que estariam ficando orfãos. A solução é encontrar
outro nodo que possa ser retirado no lugar desse nodo e que
as informações nele contidas possam ser trocadas como as do
nodo que deveria ser retirado. Para isso, há duas soluções,
conforme ilustrado pelos seguintes exemplos:
11.2. TIPO ABSTRATO DE DADOS ABP 291

– Remoção de M - Solução I:
Localiza L o antecessor de M como mostrado na seguinte
figura:

Troca as informações de M com as de seu antecessor L:

Remove o nodo M como é feito no Caso I:

Desta forma, a propriedade da árvore foi preservada.


292 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

– Remoção de M - Solução II:


Localiza o sucessor F de M, como mostrado na seguinte fi-
gura:

Troca informações de M com as de seu sucessor F:

Remove nodo M como no Caso IV:

E as propriedades são da árvore de pesquisa foram preservadas.


11.2. TIPO ABSTRATO DE DADOS ABP 293

A Listagem 11.7 mostra a implementação da operação retire,


descrita acima, que usa o nodo antecessor para remover nodos com
dois filhos.
1 public Item retire(String chave) throws ChInvalida {
2 Nodo q = raiz, r, p = raiz;
3 checkChave(chave);
4 while (p != null) {
5 if ([Link]([Link]) < 0) {
6 q = p; p = [Link];}
7 else if ([Link]([Link]) > 0) {
8 q = p; p = [Link];}
9 else { //Retire Nodo p e filho de q
10 Item x = [Link];
11 if ([Link] == null) { // novo pai de [Link]
12 if (p == [Link]) [Link] = [Link];
13 else [Link] = [Link];
14 } else if ([Link] == null) { //novo pai de [Link]
15 if (p == [Link]) [Link] = [Link];
16 else [Link] = [Link];
17 } else { // troca p com seu antecessor
18 q = p; r = [Link];
19 while ([Link] != null) {
20 p = r;
21 r = [Link];
22 }
23 [Link] = [Link];
24 if (p == q) [Link] = [Link];
25 else [Link] = [Link];
26 }
27 return x;
28 }
29 }
30 return null;
31 }

Listagem 11.7 Operação [Link]


294 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

O tipo genérico Pilha<T> definido na Listagem 11.8 é usado pela


operação próximo.

1 public class Pilha<T> {


2 private int máximo;
3 private int topo = -1;
4 private T[] info, aux;
5 public static class ExMax extends Exception {}
6 public static class ExMin extends Exception {}
7 public Pilha() {
8 máximo = 50;
9 info = (T[]) new Object[máximo+1];
10 }
11 public void empilhe(T v) throws ExMax {
12 if (topo == máximo) {
13 try {aux = (T[]) new Object[máximo+51];}
14 catch (Exception e) {throw new ExMax();}
15 for (int i = 0; i <= máximo; i++) aux[i] = info[i];
16 info = aux;
17 máximo = máximo + 50;
18 }
19 info[++topo]= v;
20 }
21 public T desempilhe() throws ExMin {
22 if(vazia()) throw new ExMin();
23 return info[topo--];
24 }
25 public boolean vazia() {
26 return (topo == -1);
27 }
28 public void limpe() {topo = -1;}
29 }

Listagem 11.8 Tipo Genérico Pilha<T>

A operação próximo da Listagem 11.9 recupera os itens da árvore


segundo um caminhamento em ordem central: a cada chamada, o
11.2. TIPO ABSTRATO DE DADOS ABP 295

próximo item nessa ordem é retornado. Quando a sequência ter-


minar, retorna-se null. Para memorizar o ponto corrente do ca-
minhamento entre chamadas de próximo, o algoritmo usa o campo
globlal a, iniciado com o endereço da raiz da árvore. Em cada
chamada, se seu valor não for nulo, deve-se percorrer o ramo mais
à esquerda da subárvore por ele apontado até ele tornar-se nulo,
empilhando o caminho percorrido. Neste ponto, se a pilha estiver
vazia, a iteração terminou, e próximo deve indicar fato retornando
null. Do contrário, desempilha-se r, que aponta para o nodo com
o item a ser retornado, e faz a = [Link], para permitir a repetição
do processo acima na próxima chamada de próximo.

1 public Item próximo() throws [Link], [Link] {


2 Nodo r;
3 while (a != null) {
4 [Link](a);
5 a = [Link];
6 }
7 if ([Link]()) return null;
8 a = [Link]();
9 r = a; a = [Link];
10 return [Link];
11 }

Listagem 11.9 Operação [Link]́ximo

Para recolocar o iterador próximo em sua posicão inicial, pode-se


usar a operação reiniciePróximo, definida na Listagem 11.10.

1 public void reiniciePróximo() {


2 [Link]();
3 a = raiz;
4 }

Listagem 11.10 Operação [Link]́ximo


296 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

Listagem 11.11 apresenta a operação imprima, que exibe uma re-


presentação textual da árvore binária de pesquisa, usando parênteses,
colchetes e chaves para mostrar sua estrutura.
1 private void imprima(Nodo t, int k) {
2 final String[] colcheteL = {"","[","(","<"};
3 final String[] colcheteR = {"","]",")",">"};
4 if (t == null) return;
5 [Link]([Link]);
6 k = (k + 1) % [Link];
7 [Link](colcheteL[k]);
8 imprima([Link],k);
9 [Link](colcheteR[k] + colcheteL[k]);
10 imprima([Link],k);
11 [Link](colcheteR[k]);
12 }
13 public void imprima() {
14 imprima(raiz,-1);
15 [Link]();
16 }

Listagem 11.11 Operaçãp [Link]

E o tipo MeuItem da Listagem 11.12 é o usado no programa de


teste no fim desta seção.
1 public class MeuItem extends Item {
2 public String valor;
3 public MeuItem(String chave, String valor) {
4 super(chave);
5 [Link] = valor;
6 }
7 public String valor() {return valor;}
8 }

Listagem 11.12 Item do Usuário do Dicionário Abp


11.2. TIPO ABSTRATO DE DADOS ABP 297

O programa de teste UsaAbp é o seguinte:

1 public class UsaAbp {


2 public static void main(String[] args)
3 throws [Link], [Link], [Link] {
4 int ordem[] = {0, 3, 5, 2, 4, 1, 6};
5 String[] valor = {"m","g","s","j","l","h","i"} ;
6 String[] chaves = {"M","G","S","J","L","H","I"} ;
7 MeuItem meusItens[] = new MeuItem[[Link]];
8 MeuItem meuItem0 = new MeuItem("Z","Z"); // especial
9 MeuItem x;
10 for (int i=0; i< [Link]; i++)
11 meusItens[i] = new MeuItem(chaves[i],valor[i]);
12 Abp t = new Abp(meuItem0);
13 [Link]("Abp vazia: ");
14 [Link]();
15 [Link]("Inserç~
ao de itens------------:");
16 for (int i=0; i < [Link]; i++) {
17 [Link](meusItens[i]);
18 [Link](
19 " Inserido " + meusItens[i].chave + ": ");
20 [Link]();
21 }
22 [Link]("Chaves em ordem crescente: ");
23 [Link]́ximo();
24 x = (MeuItem)[Link]́ximo();
25 while (x != null) {
26 [Link]([Link] + " ");
27 x = (MeuItem) [Link]́ximo();
28 }
29 [Link]();
30 [Link]("Pesquisa por " + chaves[3] + ":");
31 x = (MeuItem) [Link](chaves[3]);
32 if (x != null) [Link](
33 " Encontrado: " + [Link]);
34 else [Link](" N~ ao encontrado");
35 [Link]("Remoç~
ao de itens -------------:");
298 CAPÍTULO 11. ÁRVORES BINÁRIAS DE PESQUISA

36 for (int i=0; i < [Link]; i++) {


37 x = (MeuItem) [Link](meusItens[ordem[i]].chave);
38 [Link](" Retirado " + [Link] + ": ");
39 [Link]();
40 }
41 }
42 }

E a saı́da do programa de teste é a seguinte:


Abp vazia: Z
Inserç~ao de itens------------:
Inserido M: ZM[][]
Inserido G: ZM[G()()][]
Inserido S: ZM[G()()][S()()]
Inserido J: ZM[G()(J<><>)][S()()]
Inserido L: ZM[G()(J<><L>)][S()()]
Inserido H: ZM[G()(J<H><L>)][S()()]
Inserido I: ZM[G()(J<HI[][]><L>)][S()()]
Chaves em ordem crescente: G H I J L M S Z
Pesquisa por J:
Encontrado: j
Remoç~
ao de itens -------------:
Retirado M: ZL[G()(J<HI[][]><>)][S()()]
Retirado J: ZL[G()(H<><I>)][S()()]
Retirado H: ZL[G()(I<><>)][S()()]
Retirado S: ZL[G()(I<><>)][]
Retirado L: ZG[][I()()]
Retirado G: ZI[][]
Retirado I: Z

11.3 Análise de custo

Dada uma árvore binária de pesquisa com n nodos, o número de


comparações de chaves realizadas em uma pesquisa com sucesso
é, no melhor caso, C(n) = O(1), no pior caso, C(n) = O(n) e
no caso médio, C(n) = O(lg n). O melhor caso ocorre quando o
11.3. ANÁLISE DE CUSTO 299

item procurado é o que está muito próximo à raiz da árvore. O


pior caso é quando a árvore é completamente degenerada, em que
todos os nodos somente têm um descendente e o último nodo nesse
encadeamento é o procurado. E o médio pressupõe-se uma árvore
balanceada em que sua altura é lg n.
Claramente, o tempo de execução dos algoritmos para árvores
binárias de pesquisa depende muito do formato das árvores: para
obter o pior caso, basta que as chaves sejam inseridas em ordem
crescente ou decrescente. Nesse caso, a árvore resultante é uma
lista linear, cujo número médio de comparações é (n + 1)/2.
Portanto, para garantir o desempenho desejado, a árvore binária
de pesquisa deve obedecer a critérios que garanta um bom balan-
ceamento, o qual pode ser classificado da seguinte forma:
• Árvore de pesquisa degenerada:
árvore em que a maior parte dos nodos tem no máximo uma
subárvore.
• Árvore de pesquisa randômica:
árvore construı́da por meio de inserções sucessivas de chaves
randômicas em uma árvore inicialmente vazia. A rigor, para
uma árvore com n chaves, possuindo n + 1 nodos externos, e
se essas n chaves dividem todos os valores possı́veis em n + 1
intervalos, uma inserção é considerada randômica se ela tem
probabilidade igual de ocorrer em qualquer um dos n + 1 in-
tervalos. Em uma árvore de pesquisa randômica, o número
esperado de comparações para recuperar um registro qualquer,
segundo Knuth [7], é cerca de 1, 39 lg n, apenas 39% pior que
numa árvore balanceada.
• Árvore de pesquisa perfeitamente balanceada:
árvore em que, para cada nodo, os números de nodos das suas
subárvores esquerda e direita diferem, no máximo, de uma
unidade.
300 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

• Árvore de pesquisa balanceada:


árvore binária em que a altura da subárvore esquerda de todo
nodo nunca difere em mais de uma unidade da altura da sua
respectiva subárvore direita.

11.4 Conclusão

Árvore binária de pesquisa provê um tipo de pesquisa que pode


permitir recuperação de informação muito eficiente com custos pro-
porcionais ao logaritmo do número de itens armazenados. Entre-
tanto, cuidados devem ser tomados para evitar a degeneração do
desempenho de suas operação devido à falta de balanceamento.

Exercı́cios
1. Como os algoritmos de inserção, pesquisa e remoção de uma
árvore binária de pesquisa devem ser modificados para funci-
onar com chaves repetidas?

Notas Bibliográficas

O texto fundamental para um estudo aprofundado de árvores binárias


de pesquisa são os livros clássicos de Donald Knuth [6, 7], que dão
um tratamento formal e organizado para a matéria.
***
Capı́tulo 12

Árvores de Busca Balanceadas

The whole science is nothing more than


1
a refinement of everyday thinking.
Albert Einstein (1879-1955)

Árvores perfeitamente balanceadas garantem um custo ótimo


nas operações de pesquisa, inserção ou remoção de itens, mas, em
geral, é custoso manter esse nı́vel de desempenho quando ocorrem
muitas operações de inserção e remoção.
Árvores balanceadas, por outro lado, não apresentam o custo
ótimo das perfeitamente balanceadas para essas operações, mas,
em compensação, o rebalanceamento requerido após inserções e
remoções é comparativamente menos custoso.
Árvores balanceadas são também chamada de AVL em home-
nagem a seus inventores, dois matemáticos russos: G.M Adel’son-
Vel’skii e E.M. Landis. O método por eles proposto, em 1962, usa
apenas dois bits a mais por nodo e nunca requer mais que O(lg n)
operações para pesquisar, inserir ou remover itens.
Definição 12.1 Uma árvore AVL ou balanceada é uma árvore
binária de pesquisa em que a altura da subárvore esquerda de
todo nodo nunca difere em mais de uma unidade da altura da
sua respectiva subárvore direita.
1
A ciência é apenas um refinamento do pensamento de todo dia.

301
302 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

A árvore da figura abaixo atende à Definição 12.1 de árvore AVL.

Teorema 12.1 (Adel’son-Vel’skii e Landis):


A altura h de uma árvore balanceada com n nodos internos
sempre satisfaz a lg(n + 1) ≤ h ≤ 1, 4404 lg(n + 2) − 0, 328.

A prova desse teorema encontra-se na página 453 do livro de D.


Knuth [7].
Teorema 12.1 permite afirmar que o custo de operações de pes-
quisa em árvore balanceada, o qual é proporcional à metade de sua
altura, é da ordem de no máximo 1, 4404 ∗ lg n, onde n é o número
de nodos, definindo assim limites claros para o desempenho de suas
operações.
Em árvore completamente balanceada, observa-se ainda que:
• nodos externos aparecem em no máximo dois nı́veis adjacentes;
• o tempo médio de pesquisa para uma distribuição uniforme das
chaves, onde cada chave é igualmente provável de ser usada em
uma pesquisa, é minimizado;
• o custo para manter a árvore completamente balanceada após
cada inserção ou remoção ainda pode ser considerado alto,
como é demonstrado a seguir.
12.1. DICIONÁRIO COM AVL 303

12.1 Dicionário com AVL

Dicionário é o nome comumente utilizado para descrever uma es-


trutura de dados destinada a armazenar um conjunto de registros
ou itens sujeitos a operações de inserção, remoção e pesquisa. Di-
cionário é um tipo abstrato de dados com pelo menos essas
operações. Listagem 12.1 relaciona as operações da classe Avl, que
implementa um dicionário via uma estrutura de árvore balanceada.

1 public class Avl {


2 private Nodo raiz;
3 private boolean h;
4 private Pilha pilha = new Pilha();
5 public class ChInvalida extends Exception {}
6 public Avl() {}
7 public Item pesquise(String chave) {...}
8 "Operaç~
oes auxiliares de rebalanceieApósInserç~
ao";
9 private void rebalanceieApósInserç~ao(Nodo p) {...}
10 public void insira(Item item) throws ChInvalida {...}
11 "Operaç~
oes auxiliares de rebalanceieApósRemoç~
ao";
12 ao(Nodo p) {...}
private void rebalanceieApósRemoç~
13 public Item retire(String chave) {...}
14 }

Listagem 12.1 Tipo Abstrato de Dados AVL

Os registros do dicionário usados pela classe Avl são objetos do


tipo Item definido na Listagem 12.2.

1 public class Item {


2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 public String valor() {return "?";}
5 }

Listagem 12.2 Tipo Raiz do Itens da AVL


304 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

Tipicamente um usuário do tipo Avl deve estender a classe Item


para incorporar os campos pertinentes a cada aplicação, como o
tipo MeuItem da Listagem 12.3.
1 class MeuItem extends Item {
2 public String valor;
3 public MeuItem(String chave, String valor) {
4 super(chave);
5 [Link] = valor;
6 }
7 public String valor() {return valor;}
8 }

Listagem 12.3 Um item do Usuário do TAD Avl

Os nodos de uma AVL são objetos da classe Nodo, que está de-
finida na Listagem 12.4, onde o campo bal registra o valor da
diferença entre a altura da subárvore direita do nodo e a de sua
respectiva subárvore esquerda. Os valores de bal devem ser manti-
dos com um dos valores −1, 0 e 1, conforme exigido pela definição
de árvores AVL.
1 public class Nodo {
2 Item item;
3 Nodo esq, dir;
4 byte bal; // -1,0 ou +1 = altura direita - esquerda
5 Nodo(Item item) {[Link] = item;}
6 }

Listagem 12.4 Nodo da Árvore AVL

As operações insira e retire usam uma pilha do tipo definido


na Listagem 12.5 para quardar o caminho descendente percorrido
na AVL da raiz até o nodo diretamente envolvido na respectiva
operação. Assim, após cada inserção ou remoção, pode-se fazer o
caminho inverso para rebalancear a árvore AVL adequadamente.
12.1. DICIONÁRIO COM AVL 305

1 public class Pilha {


2 private int máximo = 10, topo = -1;
3 private Nodo[] info, aux;
4 public Pilha() {info = new Nodo[máximo+1];}
5 public void empilhe(Nodo nodo) {
6 if (topo == máximo) {
7 aux = new Nodo[máximo+11];
8 for (int i=0; i<=máximo; i++) aux[i] = info[i];
9 info = aux;
10 máximo = máximo + 10;
11 }
12 info[++topo] = nodo;
13 }
14 public Nodo topo() {
15 if(vazia()) return null;
16 return info[topo];
17 }
18 public Nodo desempilhe() {
19 if(vazia()) return null;
20 return info[topo--];
21 }
22 public boolean vazia() {return (topo == -1);}
23 public void limpe() {topo = -1;}
24 }

Listagem 12.5 Pilha de Nodos

Os conjuntos de métodos privados, cujos cabeçalhos são exi-


bidos nas listagens 12.6 e 12.7, formam os blocos de declarações
"Operações auxiliares de rebalanceieApósInserção" e "Operações
auxiliares de rebalanceieApósRemoção", mencionados na classe
Avl, e que são usados privativamente nos processos de rebalan-
ceamento da árvore após inserções ou remoções de itens, conforme
está detalhado nas seções 12.3 e 12.6.
306 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

1 private Nodo aoSimplesLRins(Nodo p, Nodo p1) {...}


rotaç~
2 private Nodo aoDuplaLRins(Nodo p, Nodo p1) {...}
rotaç~
3 private Nodo aoSimplesRLins(Nodo p, Nodo p1) {...}
rotaç~
4 private Nodo aoDuplaRLins(Nodo p, Nodo p1) {...}
rotaç~
5 private Nodo reduzaEsquerdoIns(Nodo p, Nodo pai){...}
6 private Nodo reduzaDireitoIns(Nodo p, Nodo pai){...}

Listagem 12.6 Operações Auxiliares de rebalanceieApósInserç~


ao

1 private Nodo aoSimplesLRrem(Nodo p, Nodo p1) {...}


rotaç~
2 private Nodo aoDuplaLRrem(Nodo p, Nodo p1) {...}
rotaç~
3 private Nodo aoSimplesRLrem(Nodo p, Nodo p1) {...}
rotaç~
4 private Nodo aoDuplaRLrem(Nodo p, Nodo p1) {...}
rotaç~
5 private Nodo reduzaEsquerdoRem(Nodo p, Nodo pai){...}
6 private Nodo reduzaDireitoRem(Nodo p, Nodo pai){...}

Listagem 12.7 Operações Auxiliares de rebalanceieApósRemoç~


ao

As operações com nomes similares arroladas nas listagens 12.6


e 12.7 e que se diferenciam apenas nos sufixos Ins, ins, Rem ou rem
têm muitos elementos em comum, mas são distintas em diversos
importantes pontos, justificando, para facilitar seu entendimento,
serem apresentadas em separado.

12.2 Pesquisa por um item

A operação pesquise arrolada na Listagem 12.8 percorre árvore


binária de busca desde sua raiz em direção a uma folha, dirigindo
o caminhamento pela comparação da chave do item em cada nodo
com a chave de pesquisa, até que um item com a chave dada seja
encontrado ou um nodo-folha seja atingido. Caso o item com a
chave dada seja encontrado na árvore, retorna-se seu endereço, do
contrário, retorna-se null.
O custo médio dessa operação é proporcional ao comprimento
12.3. INSERÇÃO DE ITENS 307

do caminho percorrido desde a raiz da árvore, i.e., O(lg n), onde n


é o número total de nodos da árvore.
1 public Item pesquise(String chave) {
2 Nodo p;
3 if (chave == null) return null;
4 p = raiz;
5 while (p != null) {
6 if ([Link]([Link]) < 0)
7 p = [Link];
8 else if ([Link]([Link]) > 0)
9 p = [Link];
10 else return [Link];
11 }
12 return null;
13 }

Listagem 12.8 Operação [Link]

O custo máximo da operação pesquise é o comprimento do cami-


nho percorrido desde a raiz até uma folha, que, pelo Teorema 12.1,
é inferior a 1, 4404 ∗ lg(n + 2) − 0, 328.

12.3 Inserção de itens

A operação insira de uma árvore balanceada AVL é executada em


três etapas:
• localização, conforme os critérios de uma árvore binária de
pesquisa, do nodo ao qual um novo nodo com o novo item
deve ser conectado;
• instalação do novo nodo como filho do nodo localizado acima;
• promoção do rebalanceamento da árvore.

No processo de localização do ponto de inserção, o caminho per-


308 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

corrido deve ser devidamente empilhado para que se possa per-


corrê-lo de volta na fase de rebalanceamento.
O ponto de inserção é sempre um nodo que tem no máximo um
filho, e o novo nodo deve ser inserido como seu filho, na forma
descrita pelo método insira da Listagem 12.9.

1 public void insira(Item item) throws ChInvalida {


2 Nodo a, pai, p;
3 if (item==null||[Link]==null) throw new ChInvalida();
4 "Localize nodo-pai do novo nodo";
5 "Crie e instale novo nodo p como filho de pai";
6 rebalanceieApósInserç~
ao(p);
7 }

Listagem 12.9 Operação [Link]

O custo máximo dessa operação também é proporcional ao com-


primento do caminho percorrido desde a raiz da árvore até ponto
de inserção, i.e., O(1.44 lg n).
O primeiro ato de insira, o de localizar o ponto de inserção, é
definido pelo seguinte código de "Localize nodo-pai do novo nodo":

1 h = false;
2 pai = null; p = raiz;
3 [Link]();
4 while( p != null) {
5 [Link](p);
6 if ([Link]([Link]) < 0) {
7 pai = p;
8 p = [Link];
9 } else if ([Link]([Link]) > 0) {
10 pai = p;
11 p = [Link];
12 } else return; // ignora inserç~
ao de chave repetida
13 }
12.3. INSERÇÃO DE ITENS 309

O nodo localizado pelo trecho de programa acima tem seu en-


dereço é dado por pai, e ao qual um novo nodo deve ser ligado
como seu filho esquerdo ou direito, dependendo do valor da chave
do item sendo inserido. Por exemplo, o processo de inserção de
um nodo de chave 6 deixa na pilha o caminho 4, 5 e 7, que foi
percorrido para localizar o pai, e ao qual o nodo nodo 6 deve ser
ligado, produzindo a configuração ilustrada na seguinte figura:

Dessa forma, o código de "Crie e instale novo nodo p como filho


de pai" é simplesmente o seguinte:

1 p = new Nodo(item);
2 if (pai == null) raiz = p; return;
3 if ([Link]([Link]) < 0)
4 [Link] = p; else [Link] = p;

A partir desse ponto, o caminho de descida na árvore até o ponto


de inserção deve ser percorrido de volta, e, a cada passo, tem-se
uma subárvore como a representada pela figura
310 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

onde L e R são subárvores que têm alturas hL e hR , respectiva-


mente, tais que | hL − hR | pode ser 2 unidades em consequência
da inserção realizada, podendo requerer rebalanceamento.

12.4 Rebalanceamento após inserção

O rebalanceamento, dado o nodo p que acabou de ser inserido, é


realizado pela execução da operação rebalanceieApósInserç~ao(p)
definida na Listagem 12.10.
1 ao(Nodo p) {
private void rebalanceieApósInserç~
2 Nodo a, pai;
3 h = false;
4 while (![Link]() && !h) {
5 a = p; p = [Link]();
6 pai = ([Link]())? null : [Link]();
7 if ([Link]([Link]) < 0)
8 p = reduzaEsquerdoIns(p,pai);
9 else p = reduzaDireitoIns(p,pai);
10 }
11 }

Listagem 12.10 Rebalanceamento após Inserção

Observe que rebalanceamento requer que o caminho de volta à


raiz, que foi armazenado na pilha na primeira etapa do insira, seja
percorrido. Note que, para cada nodo visitado nesse caminho de
volta, pode-se ter que fazer uma reestruturação no posicionamento
dos nodos a ele relacionados para encurtar sua subárvore mais
longa, a fim de restabelecer o equilı́brio localmente. Esse processo
12.4. REBALANCEAMENTO APÓS INSERÇÃO 311

deve ser feito para todo nodo do caminho até a raiz da árvore ser
atingida ou ter-se garantia, indicada por h, de que os demais nodos
do caminho de volta já estão balanceados.
O encurtamento da altura de subárvores é realizado via as opera-
ções reduzaEsquerdoIns ou reduzaDireitoIns, as quais estão deta-
lhadas nas listagens 12.11 e 12.12, respectivamente. A subárvore a
ser encurtada é sempre a que contém o nodo que foi inserido.
Pela definição de AVL, antes da inserção de um novo nodo,
sempre tem-se, para todo nodo, que |hL − hR | ≤ 1, onde hL e hR
são as alturas das respectivas subárvores L e R de cada nodo p em
análise.
O nodo inserido na subárvore L ou R de p certamente afeta o
bal de p, que, via de consequência, pode causar a alteração do
bal de nodos no percurso de volta até a raiz. Assim, a cada nodo
visitado no caminho do ponto de inserção até a raiz, deve-se verifi-
car a necessidade de rebalanceamento e determinar a restruturação
necessária pelos seguintes critérios:
• Antes da inserção de nodo em L:
1. hL < hR : inserção OK, não causou desbalanceamento
2. hL = hR : inserção OK, não causou desbalanceamento
3. hL > hR : necessário re-equilı́brio
• Antes da inserção de nodo em R:
1. hL < hR : necessário re-equilı́brio
2. hL = hR : inserção OK, não causou desbalanceamento
3. hL > hR : inserção OK, não causou desbalanceamento

Dentre os casos destacados acima, o reequilı́brio da subárvore p


em análise é requerido apenas em duas situações, nas quais nodos
precisam ser rearranjados, via operações de rotação simples ou du-
pla, com o objetivo de adequadamente reduzir a altura de uma das
subárvores para restabelecer o equilı́brio.
312 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

No caso de se ter que encurtar subárvore via reduzaEsquerdoIns,


o valor [Link] dirige o processo da seguinte forma:
• se [Link] = 1, a altura da subárvore direita era maior que a da
esquerda, e basta fazer [Link] = 0 e h = true.
• se [Link] = 0, deve-se fazer [Link] = -1, e o processo de reba-
lanceamento deve continuar analisando os demais nodos no
caminho até a raiz;
• se [Link] = -1, então a inserção de nodo no ramo esquerdo
de p vai tornar seu bal = -2, e, nesse caso, uma das rotações
descritas a seguir, simples ou dupla, faz-se necessária. O bal da
raiz da subárvore esquerda de p define a rotação apropriada. E
depois da rotação, o rebalanceamento é considerado concluı́do.
1 private Nodo reduzaEsquerdoIns(Nodo p, Nodo pai) {
2 Nodo p1;
3 switch ([Link]) {
4 case 1: [Link] = 0; h = true; break;
5 case 0: [Link] = -1; break;
6 case -1:
7 p1 = [Link];
8 if ([Link] == -1)
9 p = rotaç~
aoSimplesLRins(p,p1);
10 else p = rotaç~aoDuplaLRins(p,p1);
11 if (pai == null) raiz = p;
12 else if ([Link]([Link])<0)
13 [Link] = p; else [Link] = p;
14 [Link] = 0; h = true; break;
15 }
16 return p;
17 }

Listagem 12.11 Redução de Ramo Esquerdo após Inserção

A reestruturação da subárvore p é realizada por uma das operações


de rotação simples ou dupla do tipo LR (Left-to-Right), determi-
12.4. REBALANCEAMENTO APÓS INSERÇÃO 313

nada pelo bal de p1, que é a raiz da subárvore de p onde houve a


inserção.
1 private Nodo reduzaDireitoIns(Nodo p, Nodo pai) {
2 Nodo p1;
3 switch ([Link]) {
4 case -1: [Link] = 0; h = true; break;
5 case 0: [Link] = 1; break;
6 case 1:
7 p1 = [Link];
8 if ([Link] == 1)
9 p = rotaç~
aoSimplesRLins(p,p1);
10 else p = rotaç~aoDuplaRLins(p,p1);
11 if (pai == null) raiz = p;
12 else if ([Link]([Link])<0)
13 [Link] = p; else [Link] = p;
14 [Link] = 0; h = true; break;
15 }
16 return p;
17 }

Listagem 12.12 Redução de Ramo Direito após Inserção

A redução da altura da subárvore direita do nodo p em análise é


realizada pela operação reduzaDireitoIns da Listagem 12.12, onde
são realizadas rotações do tipo RL (Right-to-Left), e [Link] dirige
o processo da seguinte forma:
• se [Link] = -1, a altura da subárvore esquerda era maior que a
da direita, e basta fazer [Link] = 0 e h = true.
• se [Link] = 0, deve-se fazer [Link] = 1, e o processo de reba-
lanceamento deve continuar analisando os demais nodos no
caminho até a raiz;
• se [Link] = 1, então a inserção de nodo no ramo direito de p vai
tornar seu bal = 2, e, nesse caso, uma das rotações descritas
a seguir, simples ou dupla, faz-se necessária. O bal da raiz da
314 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

subárvore direita de p define a rotação apropriada. E depois


da rotação, o rebalanceamento é considerado concluı́do.
As configurações dos nodos diretamente envolvidos com o nodo
p, que foi identificado como desbalanceado, geram seis contextos
distintos nos quais uma dentre quatro operações de rotação de
nodos deve ser executada, a saber: Rotação Simples LR, Rotação
Simples RL, Rotação Dupla LR e Rotação Dupla RL.
Cada uma dessas configurações que requerem rebalanceamento
é descrita a seguir, onde, nas figuras ilustrativas, x representa o
local onde o novo nodo foi inserido, p é o nodo com | [Link] | = 2,
sendo, portanto, o objeto principal de cada rebalanceamento, p1 a
raiz da subárvore a ser encurtada, e o lado direito de cada figura
mostra o rebalanceamento realizado.
Em cada rebalanceamento, faz-se uma rotação que, no caso de
redução da altura de subárvores esquerdas, são tipo LR, e, para
subárvores direitas, do tipo RL, conforme detalhado a seguir.
1. Configuração I (Rotação Simples LR após Inserção):
Novo nodo inserido em α.

Nesse caso, uma rotação LR envolvendo A e B restabelece


o equilı́brio da subárvore de raiz p, e A torna-se a raiz de p,
conforme mostra rotaç~aoSimplesLRins da Listagem 12.13, onde
12.4. REBALANCEAMENTO APÓS INSERÇÃO 315

os campos bal de A e B, apontados por p1 e p, respectivamente,


são redefinidos da seguinte forma:
A:[Link] = 1 + altura de γ - altura de α + 1 = 0
B:[Link] = altura de γ - altura de β = 0

1 aoSimplesLRins(Nodo p, Nodo p1) {


private Nodo rotaç~
2 [Link] = [Link];
3 [Link] = p;
4 [Link] = 0;
5 [Link] = 0;
6 p = p1;
7 return p;
8 }

Listagem 12.13 Rotação Simples LR

Essa rotação simples é chamada LR porque o nodo B, o que


estava desbalanceado, gira no sentido horário em torno de A,
da esquerda para a direita.

2. Configuração II (Rotação Simples RL após Inserção):


Novo nodo inserido em γ.

A rotação RL envolvendo A e C resolve o desbalanceamento


de p, tornando C a raiz dessa subárvore, conforme mostra o
método rotaç~aoSimplesRLins da Listagem 12.14, onde os cam-
316 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

pos bal de A e C, apontados por p e p1, respectivamente, são


redefinidos da seguinte forma:
A:[Link] = altura de α - altura de β = 0
C:[Link] = altura de γ + 1 - (1 + altura de α) = 0
1 aoSimplesRLins(Nodo p, Nodo p1) {
private Nodo rotaç~
2 [Link] = [Link];
3 [Link] = p;
4 [Link] = 0;
5 p = p1;
6 return p;
7 }

Listagem 12.14 Rotação Simples RL

Nesse caso, ocorre a rotação simples RL de A, o nodo desba-


lanceado, em torno de C, no sentido anti-horário.
3. Configuração III (Rotação Dupla LR após Inserção):
Novo nodo inserido em β.

Nesse caso, o rebalanceamento é mais elaborado, sendo ne-


cessário uma rotação dupla LR, a qual torna o nodo B, que
tinha [Link] = -1 antes da rotação, a nova raiz da subárvore
p, conforme o método rotaç~ aoDuplaLRins da Listagem 12.15,
onde os campos bal de A e C são redefinidos da seguinte forma:
12.4. REBALANCEAMENTO APÓS INSERÇÃO 317

A:[Link] = altura de β +1 - altura de α = 0


C:[Link] = altura de δ - altura de γ = 1

1 private Nodo rotaç~


aoDuplaLRins(Nodo p, Nodo p1) {
2 Nodo p2;
3 p2 = [Link];
4 [Link] = [Link];
5 [Link] = p1;
6 [Link] = [Link];
7 [Link] = p;
8 switch ([Link]) {
9 case 0: [Link] = 0; [Link] = 0; break;
10 case 1: [Link] = 0; [Link] = -1; break;
11 case -1: [Link] = 1; [Link] = 0; break;
12 }
13 p = p2;
14 return p;
15 }

Listagem 12.15 Rotação Dupla LR

Rotação dupla LR é um giro de B para a esquerda em torno


de A seguido de um giro para a direita em torno de C.

4. Configuração IV (Rotação Dupla LR após Inserção):


Novo nodo inserido em γ.
318 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

Nesse caso, a mesma rotação dupla LR da configuração III re-


solve o rebalanceamento, tornando B, que, nesse caso, estava
com [Link] = 1, a nova raiz da subárvore inicialmente apon-
tada por p, conforme mostra o método rotaç~aoDuplaLRins da
Listagem 12.29, onde os campos bal de A e C, apontados por
p1 e p, respectivamente, são redefinidos da seguinte forma:
A:[Link] = altura de β - altura de α = -1
C:[Link] = altura de δ - (altura de γ + 1) = 0.

A diferença da configuração IV para III está nos valores de


redefinição dos campos bal dos nodos A e C envolvidos, deter-
minados conforme o [Link] de B em cada caso.

5. Configuração V (Rotação Dupla LR após Inserção):


Novo nodo inserido em β.
12.4. REBALANCEAMENTO APÓS INSERÇÃO 319

Essa configuração demanda uma rotação dupla RL, em que B,


que estava com [Link] = -1, torna-se a nova raiz da subárvore
p, nos termos do método rotaç~
aoDuplaRLins da Listagem 12.16,
onde os campos bal de A e C são redefinidos da seguinte forma:
A:[Link] = altura de β +1 - altura de α = 0
C:[Link] = altura de δ - altura de γ = 1

1 aoDuplaRLins(Nodo p, Nodo p1) {


private Nodo rotaç~
2 Nodo p2;
3 [Link] = [Link];
4 [Link] = p1;
5 [Link] = [Link];
6 [Link] = p;
7 switch ([Link]) {
8 case 0: [Link] = 0; [Link] = 0; break;
9 case 1: [Link] = -1; [Link] = 0; break;
10 case -1: [Link] = 0; [Link] = 1; break;
11 }
12 p = p2;
13 return p;
14 }

Listagem 12.16 Rotação Dupla RL

Rotação dupla RL, nesse caso, é giro de B para a direita em


320 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

torno de C seguido de giro para a esquerda em torno de A.


6. Configuração VI (Rotação Dupla RL após Insercão):
Novo nodo inserido em γ.

Nesse caso, a mesma rotação dupla RL da configuração V


resolve o rebalanceamento, tornando B, que tinha [Link] = 1,
a nova raiz da subárvore apontada por p, nos termos do método
aoDuplaRLins da Listagem 12.16, onde os campos bal de
rotaç~
A e C são redefinidos da seguinte forma:
A:[Link] = altura de β - altura de α = -1
C:[Link] = altura de δ - (altura de γ + 1) = 0
A diferença de VI para V está nos valores de redefinição dos
campos bal dos nodos A e C envolvidos, determinados con-
forme o [Link] de B que identifica cada caso.

12.5 Exemplo de inserções sucessivas

As operações de rotação simples e dupla requeridas no balancea-


mento das configurações discutidas acima são ilustradas pela in-
serção de itens com chaves 4, 5, 7, 2, 1, 3 e 6, nesta ordem, em
uma árvore AVL inicialmente vazia.
12.5. EXEMPLO DE INSERÇÕES SUCESSIVAS 321

O processo de inserção dessas chaveas passo-a-passo é o seguinte:


• Inserção de nodo com chave 4 produz:

• Inserção de nodo com chave 5:

• Inserção de nodo com chave 7 produz:

onde o nodo 4 fica com bal = -2, e isso requer uma rotação do
tipo II, que produz a árvore rebalanceada:
322 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

• Inserção de nodo com chave 2 produz:

nodo 5 tem bal = -1, portanto balanceado.

• Inserção de nodo com chave 1 produz a árvore:

onde o nodo 4 fica com bal = -2, portanto, desbalanceado.


Para o reequilı́brio, deve-se realizar uma rotação simples LR:
12.5. EXEMPLO DE INSERÇÕES SUCESSIVAS 323

• Inserção de nodo com chave 3 produz:

onde o nodo 5 tem bal = -2, portanto desbalanceado, o que


requer uma rotação dupla LR para produzir:

• Inserção de nodo com chave 6:


324 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

onde nodo 5 tem bal = 2, e, portanto, requer uma rotação du-


pla RL para produzir a árvore final rebalanceada:

12.6 Remoção de itens

A operação de retirada de um item de uma árvore balanceada é


executada em quatro etapas:
• localização, conforme os critérios de uma árvore binária de
pesquisa, do nodo que contém o item a ser removido;
• identificação do item a ser removido de forma a retorná-lo como
resultado da operação;
• localização do nodo da árvore a ser efetivamente removido e
sua remoção;
• promoção do rebalanceamento da árvore.
12.6. REMOÇÃO DE ITENS 325

Essas ações ocorrem na forma e ordem definidas pela operação


retire da Listagem 12.17.

1 public Item retire(String chave) {


2 Nodo a, r, q, p, pai;
3 Item item = null;
4 if ((raiz == null) || (chave == null)) return null;
5 "Localize item a ser retirado";
6 "Localize nodo p a ser retirado e seu pai";
7 "Efetue a retirada do nodo p filho de pai";
8 if (raiz != null) rebalanceieApósRemoç~
ao(p);
9 return item;

Listagem 12.17 Operação [Link]

O refinamento de "Localize item a ser retirado" é apresentado


na Listagem 12.18, onde o caminho percorrido na árvore é devida-
mente salvo em uma pilha, para que se possa percorrê-lo de volta
na fase de rebalanceamento.
1 p = raiz; h = false; pai = null; [Link]();
2 while (p != null) {
3 [Link](p);
4 if ([Link]([Link]) < 0) {
5 pai = p; p = [Link];
6 } else if ([Link]([Link]) > 0) {
7 pai = p; p = [Link];
8 } else break;
9 }
10 if (p == null) return null;
11 item = [Link];

Listagem 12.18 Localização do Item a ser Retirado

Após a execução do código acima, o topo da pilha tem o endereço


do nodo p que contém o item a ser removido, pai, o endereço de seu
nodo-pai e item tem o valor a ser retornado pela operação retire.
326 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

O próximo passo de retire é remover da árvore o nodo associ-


ado ao item retirado. Note que somente pode-se remover nodos
que tenham no máximo um filho, pois o pai do removido precisa
adotar os órfãos, e o nodo-pai tem para isso apenas um apontador.
Por exemplo, se o nodo M da figura abaixo fosse retirado, δ ou
F ficaria desconectado, pois q somente poderia adotar no máximo
um deles.

Assim, no caso de o nodo que contém o item a ser removido ter


dois filhos, deve-se substituir o item desse nodo pelo item armaze-
nado no seu nodo antecessor ou sucessor, os quais somente podem
ter no máximo um filho, pois do contrário não seria antecessor ou
sucessor. E então o nodo antecessor ou sucessor pode ser simples-
mente removido. A remoção de um nodo que tem no máximo um
filho é realizado como em um dos seguintes quatro casos:
12.6. REMOÇÃO DE ITENS 327

(a) (b)

(c) (d)
1 if (([Link] != null) && ([Link] != null)) {
2 q = p; r = [Link];
3 while (r != null) {
4 pai = p; p = r;
5 [Link](r);
6 r = [Link];
7 }
8 [Link] = [Link]; // antecessor n~
ao muda
9 }

Listagem 12.19 Localização de Nodo a ser Removido

O refinamento das ações "Localize o nodo p a ser retirado e


seu pai" é apresentado na Listagem 12.19, no qual continua-se o
empilhamento do caminho de volta à raiz, e após a execução desse
trecho de código, tem-se que:
• p aponta para o nodo a ser efetivamente removido da árvore;
• pai tem o endereço do pai do nodo a ser removido;
• a pilha tem todo caminho do nodo raiz da árvore até p;
• o item do nodo antecessor, que será removido, não é alterado,
porque essa informação é usada no rebalanceamento.
O refinamento de "Efetue a retirada do nodo p filho de pai" é
328 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

apresentado na Listagem 12.20.


1 p = [Link]();
2 if (p == raiz) {
3 if ([Link] == null) raiz = [Link]; else raiz = [Link];
4 } else {
5 if ([Link] == null)
6 if (p == [Link]) [Link] = [Link];
7 else [Link] = [Link];
8 else if (p == [Link]) [Link] = [Link];
9 else [Link] = [Link];
10 }

Listagem 12.20 Retirada do Nodo p

A partir desse ponto, o caminho de descida na árvore até o nodo


que foi removido deve ser percorrido de volta, e, a cada passo,
tem-se uma subárvore p como a representada pela figura

onde L e R representam subárvores que têm alturas hL e hR , res-


pectivamente, tais que | hL −hR | ≤ 2 em consequência da remoção
realizada, podendo requerer rebalanceamento da subárvore apon-
tada por p.

12.7 Rebalanceamento após remoção

O processo de rebalanceamento após remoção do nodo p, con-


tendo o item que foi retirado, e com a pilha contendo o cami-
nho de volta até a raiz, é realizado pela execução do método
ao(p) definido na Listagem 12.21.
rebalanceieApósRemoç~
12.7. REBALANCEAMENTO APÓS REMOÇÃO 329

1 ao(Nodo p) {
private void rebalanceieApósRemoç~
2 Nodo a, pai;
3 h = false;
4 while (![Link]() && !h) {
5 a = p;
6 p = [Link]();
7 if ([Link]()) pai = null;
8 else pai = [Link]();
9 if ([Link]([Link]) < 1) {
10 p = reduzaDireitoRem(p,pai);
11 } else {
12 p = reduzaEsquerdoRem(p,pai);
13 }
14 }
15 }

Listagem 12.21 Rebalanceamento Após Remoção

Note que, para cada nodo visitado no caminho de volta em-


pilhado nas etapas iniciais do retire, pode-se ter que fazer uma
reestruturação da árvore para encurtar sua subárvore mais longa,
a fim de restabelecer o equilı́brio localmente. Esse processo deve
continuar até a raiz da árvore ser atingida ou ter-se a garantia, in-
dicada pela variável global h, de que os demais nodos do caminho
de volta já estão balanceados.
Nesse processo, para cada nodo visitado, a sua subárvore es-
querda ou direita que seja irmã da subárvore da qual um nodo
foi removido pode ter que ser devidamente encurtada. Isso é feito
pelas operações reduzaEsquerdoRem ou reduzaDireitoRem, que estão
detalhadas nas listagens 12.22 e 12.23, respectivamente.
Pela definição de AVL, antes da remoção de um nodo da subárvore
L ou R, filhas de um nodo p, tem-se que [Link] = |hL − hR | ≤ 1,
onde hL e hR são as alturas das respectivas subárvores L e R.
A remoção desse nodo certamente afeta [Link], que, via de con-
330 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

1 private Nodo reduzaEsquerdoRem(Nodo p, Nodo pai) {


2 Nodo p1;
3 switch ([Link]) {
4 case 1: [Link] = 0; break;
5 case 0: [Link] = -1; h = true; break;
6 case -1:
7 p1 = [Link];
8 if (([Link] == -1) || ([Link] == 0))
9 p = rotaç~
aoSimplesLRrem(p,p1);
10 else p = rotaç~aoDuplaLRrem(p,p1);
11 if (pai == null) raiz = p;
12 else if ([Link]([Link]) < 0)
13 [Link] = p;
14 else [Link] = p;
15 if ([Link] !- 0) h = true;
16 break;
17 }
18 return p;
19 }

Listagem 12.22 Encurtamento de Subárvore Esquerda

sequência, pode causar a alteração dos valores dos campos bal de


nodos do percurso de volta até a raiz.
Assim, a cada nodo visitado no caminho do ponto de remoção
até a raiz, deve-se verificar a necessidade de rebalanceamento e
determinar a rotação necessária pelos seguintes critérios:
• Antes da remoção de nodo em R:
1. hL < hR : inserção OK
2. hL = hR : inserção OK
3. hL > hR : necessário reequilı́brio
• Antes da remoção de nodo em L:
1. hL < hR : necessário reequilı́brio
2. hL = hR : inserção OK
12.7. REBALANCEAMENTO APÓS REMOÇÃO 331

3. hL > hR : inserção OK
A remoção de um nodo da subárvore R de p requer a verificação,
via o método reduzaEsquerdoRem, da necessidade de se encurtar a
subárvore L associada, e o valor [Link] existente antes da retirada
do nodo dirige o processo da seguinte forma:
• se [Link] = 0, deve-se fazer [Link] = -1, e o rebalanceamento
termina, i.e., faz-se h = true, porque a remoção não afetou a
altura de p.
• se [Link] = 1, deve-se fazer [Link] = 0, e o processo de rebalan-
ceamento deve continuar com os demais nodos no caminho até
a raiz, pois a remoção pode ter afetado a altura de p.
• se [Link] = -1, então a remoção de nodo ocorrida no ramo di-
reito de p torna [Link] = -2, e, nesse caso, uma das rotações
descritas a seguir, simples ou dupla, faz-se necessária. O bal
da raiz da subárvore esquerda de p define o tipo da rotação
necessária. E depois da rotação, o rebalanceamento é conside-
rado concluı́do.
As rotações no caso de redução da altura da subárvore esquerda
de p são do tipo LR, conforme detalhado a seguir.
Por outro lado, a redução da altura da subárvore direita do
nodo p em análise é realizada pela operação reduzaDireitoRem da
Listagem 12.23, que é muito similar à operação reduzEsquerdoRem
descrita acima, mas, nesse caso, as rotações são RL.
Na verificação da necessidade de encurtar subárvore direita, o
valor [Link] existente antes da retirado do nodo dirige o processo
da seguinte forma:
• se [Link] = 0 deve-se fazer [Link] = 1, e o rebalanceamento ter-
mina, porque a remoção não afetou a altura de p.
• se [Link] = -1, deve-se fazer [Link] = 0 e o processo de reba-
lanceamento deve continuar analisando os demais nodos no
332 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

1 private Nodo reduzaDireitoRem(Nodo p, Nodo pai) {


2 Nodo p1;
3 switch ([Link]) {
4 case -1: [Link] = 0; break;
5 case 0: [Link] = 1; h = true; break;
6 case 1:
7 p1 = [Link];
8 if (([Link] == 1)|| ([Link] == 0))
9 p = rotaç~
aoSimplesRLrem(p,p1);
10 else p = rotaç~aoDuplaRLrem(p,p1);
11 if (pai == null) raiz = p;
12 else if ([Link]([Link]) < 0)
13 [Link] = p;
14 else [Link] = p;
15 h = true;
16 break;
17 }
18 return p;
19 }

Listagem 12.23 Encurtamento de Subárvore Direita

caminho até a raiz, por a remoção pode ter afetado o valor da


altura de p.
• se [Link] = 1, então a remoção de nodo ocorrida no ramo direito
de p vai torná-lo [Link] = 2, e, nesse caso, uma das rotações
descritas a seguir, simples ou dupla, faz-se necessária. O bal
da raiz da subárvore direita de p define a rotação necessária, e
o rebalanceamento é considerado concluı́do.

Os métodos reduzaEsquerdoRem e reduzaDireitoRem podem re-


alizar rotações simples ou duplas, selecionadas conforme a confi-
guração de cada nodo em análise no percurso de retorno do ponto
de remoção até a raiz da árvore. Quando esses métodos são ati-
vados, o nodo raiz da configuração em análise tem |bal| = 2, e as
12.7. REBALANCEAMENTO APÓS REMOÇÃO 333

configurações distintas a ser tratadas são as seguintes:


1. Configuração I (Rotação Simples LR após Remoção):
Figura 12.24 define o contexto em que o nodo p tem bal = -2
e a raiz de sua subárvore esquerda, apontada por p1, tem
bal = -1 ou bal = 0.

Figura 12.24 Configuração Após uma Remoção à Direita de B

Nesse caso, uma rotação simples LR, envolvendo A e B resta-


belece o equilı́brio da subárvore de raiz B, e A, apontada por p,
torna-se a raiz local, conforme o método rotaç~aoSimplesLRrem
da Listagem 12.25, onde os campos bal de A e B, apontados
por p1 e p, respectivamente, são devidamente redefinidos.
As subárvores indicadas na Figura 12.24 têm as alturas a, b e
c, definidas para os valores possı́veis de [Link], e essas alturas
são usadas para computar os novos valores dos campos bal de
A e B, após a rotação. pelas seguintes fórmulas:
A: [Link] = max(b, c) + 1 − a
B: [Link] = c − b
onde max(b, c) = b, pois
• a = b ∨ a > b ⇒ [Link] = c − (1 + a) = −2 ⇒ c = a − 1
• se [Link] = 0: b = a
334 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

1 aoSimplesLRrem(Nodo p, Nodo p1) {


private Nodo rotaç~
2 [Link] = [Link]; [Link] = p;
3 if ([Link] == 0) {
4 [Link] = 1; [Link] = -1;
5 } else {
6 [Link] = 0; [Link] = 0;
7 }
8 p = p1;
9 return p;
10 }

Listagem 12.25 Rotação Simples LR após Remoção

• se [Link] = −1: b = a − 1
• Consequentemente,
se (b = a ∨ b = a − 1) ∧ (c = a − 1) ⇒ max(b, c) = b
Supondo a = h, e com base nos valores possı́veis de [Link]
do nodo A, definem-se as alturas das subárvores envolvidas na
rotação, e os novos valores dos campos [Link] de A e [Link] de
B, a saber:
(a) Se [Link] = 0:
a=h, b=h e c=h-1
então, após a rotação, tem-se:
A: [Link] = 1, B: [Link] = −1
(b) Se [Link] = -1:
a=h, b=h-1 e c=h-1
então, após a rotação, tem-se:
A: [Link] = 0, B: [Link] = 0
2. Configuração II (Rotação Simples RL após Remoção):
Figura 12.26 define o contexto em que o nodo A de endereço
p tem bal = 2, e C, de endereço p1, tem bal = 0 ou bal = 1.
Nesse caso, uma rotação simples RL, envolvendo A e C, resta-
belece o equilı́brio da subárvore de raiz A, e C torna-se a raiz
12.7. REBALANCEAMENTO APÓS REMOÇÃO 335

Figura 12.26 Configuração Após uma Remoção à Esquerda de A

apontada por p, conforme o método rotaç~aoSimplesRLrem da


Listagem 12.27.
1 aoSimplesRLrem(Nodo p, Nodo p1) {
private Nodo rotaç~
2 [Link] = [Link]; [Link] = p;
3 if ([Link] == 0) {[Link] = -1; [Link] = 1;}
4 else {[Link] = 0; [Link] = 0;}
5 p = p1;
6 return p;
7 }

Listagem 12.27 Rotação Simples RL após Remoção

As subárvores α, β e γ indicadas na Figura 12.26 têm suas al-


turas a, b e c definidas conforme os valores de [Link] possı́veis,
e essas alturas são usadas para computar os novos valores dos
campos bal de A e B, após a rotação, pelas seguintes fórmulas:
A: [Link] = b − a
C: [Link] = c − [1 + max(a, b)]
Supondo a = h, tem-se:
• (b = c) ∨ (b < c) ⇒ c + 1 − a = 2 ou c = a + 1 = h + 1
• Se [Link] = 0: b = c
portanto a = h, b = h + 1, c = h + 1 e max(a, b) = h + 1
336 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

• Se [Link] = 1: b = c − 1 ⇒ b = h
portanto a = h, b = h, c = h + 1 e max(a, b) = h.
Com base nos possı́veis valores de bal do nodo A, apontado por
p1, definem-se as alturas das subárvores envolvidas na rotação
e os novos valores dos campos bal de A e C, a saber:
(a) Se [Link] = 0:
a = h , b = h+1 e c = h+1
então, após a rotação, tem-se:
A: [Link] = 1, C: [Link] = −1
(b) Se [Link] = 1:
a = h , b = h e c = h+1
então, após a rotação, tem-se:
A: [Link] = 0, C: [Link] = 0
3. Configuração III (Rotação Dupla LR após Remoção):
Figura 12.28 define o contexto, no qual o nodo C, apontado
por p, tem [Link] = -2, e A, apontado por p1, tem [Link] = 1.

Figura 12.28 Configuração após uma Remoção à Direita de C

Nesse caso, a rotação é do tipo dupla LR, envolvendo A, B


e C, que, para restabelecer o equilı́brio da subárvore de raiz
12.7. REBALANCEAMENTO APÓS REMOÇÃO 337

C, torna B a raiz apontada por p, conforme detalha o método


aoDuplaLRrem da Listagem 12.29.
rotaç~
Os novos valores dos campos bal de A, B e C, após a rotação,
são definidos pelas seguintes fórmulas:
A: [Link] = b − a
B: [Link] = 1 + max(c, d) − [1 + max(a, b)]
C: [Link] = d − c
As alturas a, b, c e d das subárvores na Figura 12.28 são
definidas pelas seguintes as equações:
• [Link] = −2, [Link] = 1 e supõe-se d = h
• [Link] = d − [1 + 1 + max(b, c)] ⇒ max(b, c) = h
• [Link] = 1 + max(b, c) − a ⇒ a = h
• se [[Link] = 0 ∧ max(b, c) = h] ⇒ b = h e c = h
e então após a rotação tem-se:
A: [Link] = 0, B: [Link] = 0 e C: [Link] = 0
• se [[Link] = 1 ∧ max(b, c) = h] ⇒ b = h-1 e c = h
e então após a rotação tem-se:
A: [Link] = −1, B: [Link] = 0 e C: [Link] = 0
• se [[Link] = −1 ∧ max(b, c) = h] ⇒ b = h e c = h-1
e então após a rotação tem-se:
A: [Link] = 0, B: [Link] = 0 e C: [Link] = 1

A Listagem 12.29 implementa as transformações acima as quais


são necessárias para o reequilı́brio do nodo C.
4. Configuração IV (Rotação Dupla RL após Remoção):
A Figura 12.30 define o contexto desse tipo de rotação, na qual
o nodo A, apontado por p, tem [Link] = 2, e C, apontado por
p1, tem [Link] = -1.
Nesse caso, a rotação é do tipo dupla RL (Right-to-Left),
envolvendo A, B e C, que, para restabelecer o equilı́brio da
subárvore de raiz A, torna B a raiz apontada por p.
338 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

1 aoDuplaLRrem(Nodo p, Nodo p1) {


private Nodo rotaç~
2 Nodo p2 = [Link];
3 [Link] = [Link]; [Link] = p1; [Link] = [Link]; [Link] = p;
4 //[Link] = 0; [Link] = 0; [Link] = 0;
5 switch ([Link]) {
6 case 0: [Link] = 0; [Link] = 0; [Link] = 0; break;
7 case 1: [Link] = -1; [Link] = 0; [Link] = 0; break;
8 case -1: [Link] = 0; [Link] = 0; [Link] = 1; break;
9 }
10 p = p2;
11 return p;
12 }

Listagem 12.29 Rotação Dupla LR após Remoção

Figura 12.30 Configuração após uma Remoção à Esquerda de A

Após a rotação, os novos valores dos campos bal de A, B e C


são definidos pelas seguintes fórmulas:
A: [Link] = b − a
B: [Link] = 1 + max(c, d) − (1 + max(a, b))
C: [Link] = d − c
As alturas a, b, c e d das subárvores na Figura 12.30 são
definidas pelas seguintes as equações:
12.7. REBALANCEAMENTO APÓS REMOÇÃO 339

• [Link] = 2, [Link] = -1 e a = h
• [Link] = 1 + 1 + max(b, c) − a ⇒ max(b, c) = h
• [Link] = d − [1 + max(b, c)] ⇒ d = max(b, c) = h
Para determinar as demais alturas, deve-se considerar os possı́veis
valores de [Link]:
• se [[Link] = 0 ∧ max(b, c) = h] ⇒ b = h e c = h
e então após a rotação tem-se:
A: [Link] = 0, B: [Link] = 0 e C: [Link] = 0
• se [[Link] = 1 ∧ max(b, c) = h] ⇒ b = h-1 e c = h
e então após a rotação tem-se:
A: [Link] = −1, B: [Link] = 0 e C: [Link] = 0
• se [[Link] = −1 ∧ max(b, c) = h] ⇒ b = h e c = h-1
e então após a rotação tem-se:
A: [Link] = 0, B: [Link] = 0 e C: [Link] = 1
A Listagem 12.31 implementa as transformações acima as quais
são necessárias para o reequilı́brio do nodo A.

1 aoDuplaRLrem(Nodo p, Nodo p1) {


private Nodo rotaç~
2 Nodo p2;
3 p2 = [Link]; [Link] = [Link]; [Link] = p1;
4 [Link] = [Link]; [Link] = p;
5 switch ([Link]) {
6 case 0: [Link] = 0; [Link] = 0; [Link] = 0; break;
7 case 1: [Link] = -1; [Link] = 0; [Link] = 0; break;
8 case -1: [Link] = 0; [Link] = 0; [Link] = 1; break;
9 }
10 p = p2;
11 return p;
12 }

Listagem 12.31 Rotação Dupla RL após Remoção


340 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

12.8 Exemplo de remoções sucessivas

O exemplo a seguir mostra, passo-a-passo, a remoção de nodos com


chaves 4, 8, 6, 5, 2, 1, 7, 9, 11, 10 e 3, nesta ordem, da seguinte
árvore AVL:

Os passos de remoção são os seguintes:

• Remoção de nodo de chave 4:

Rotação simples LR de 2 e 3:
12.8. EXEMPLO DE REMOÇÕES SUCESSIVAS 341

• Remoção de nodo de chave 8:

Nodo 8 tem dois filhos e não pode ser removido, faz-se então
uma troca de papéis com seu antecessor 7, que pode então ser
removido:

• Remoção de nodo de chave 6:


342 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

Rotação simples RL de 7 e 10:


12.8. EXEMPLO DE REMOÇÕES SUCESSIVAS 343

• Remoção de nodo de chave 5:

Nodo 5 troca de lugar com 3, seu antecessor:

• Remoção de nodo de chave 2:


344 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

Rotação dupla RL com 3, 7 e 10:

• Remoção de nodo de chave 1:

• Remoção de nodo de chave 7:


12.8. EXEMPLO DE REMOÇÕES SUCESSIVAS 345

Nodo 7 troca de lugar com seu antecessor 3, e então é retirado:

Rotação simples RL com 3 e 10:

• Remoção de nodo de chave 9:


346 CAPÍTULO 12. ÁRVORES DE BUSCA BALANCEADAS

• Remoção de nodo de chave 11:

• Remoção de nodo de chave 10:

• Remoção de nodo com chave 3:

12.9 Conclusão

Árvore binária de pesquisa é um tipo de estrutura de dados que


pode permitir recuperação de informação muito eficiente com cus-
tos proporcionais ao logaritmo do número de itens. Entretanto,
cuidados devem ser tomados para evitar a degeneração do desem-
penho de suas operações devido à falta de balanceamento. Uma
solução para evitar essa degeneração de desempenho são as árvores
AVL, que garantem sempre um desempenho mı́nimo logarı́tmico
das suas operações, mas, claramente, as operações de rebalancea-
mento são complexas e podem ser custosas.
Estruturas de Dados Fundamentais Capı́tulo 13: Árvores-B 347

Assim, deve-se então buscar uma solução intermediária que possa


manter a árvore quase-balanceada, em vez de tentar mantê-la com-
pletamente balanceada. Dessa forma, pode-se procurar obter bons
tempos de pesquisa, próximos do tempo ótimo da árvore comple-
tamente balanceada, mas sem pagar alto preço para inserir nodos
ou retirá-los da árvore. Solução nesse sentido são as árvores SBB
e as árvores rubro-negras.

Exercı́cios
1. Como os algoritmos de inserção, pesquisa e remoção de uma
árvore AVL devem ser modificados para funcionar com chaves
repetidas?

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre árvores


AVL são os livros clássicos de Donald Knuth [6, 7], que dão um
tratamento formal e organizado para a matéria.
***
348 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Capı́tulo 13

Árvores B

Le bon sens est la chose du monde mieux partagée1


René Descartes (1596-1650)

Árvores binárias de pesquisa funcionam muito bem para arma-


zenar e pesquisar itens em tabelas que cabem na memória interna,
em particular, árvores balanceadas asseguram um desempenho efi-
ciente das operações de busca, inserção e remoção de itens.
Entretanto, se os itens das tabelas são armazenados em arquivos
gravados em memória externa, como discos magnéticos, o acesso a
esses itens passam a depender de sua localização dentro do dispo-
sitivo externo, sendo que essa operação pode ser um limitante sério
de desempenho. Nesse caso, a minimização do número de acessos
ao arquivo passa a ser o foco das atenções para garantir a eficiência
das operações da estrutura de dados.
Árvores B são uma estrutura de dados, inventada por R. Bayer
e E.M. McCreight em 1970 e publicada em 1972, e, independente e
simultaneamente, por M. Kaufman, para armazenamento e busca
de itens em grandes arquivos em mémoria secundária [7], nos quais
os itens são agrupados em nodos, que constituem as unidades de
transferência de dados entre a memória interna e a externa.
1
Bom senso é a coisa mais bem distribuı́da no mundo

349
350 CAPÍTULO 13. ÁRVORES B

Definição 13.1 : Uma árvore B de ordem m é uma árvore


com as seguintes propriedades:
P1: cada nodo contém no máximo m filhos
P2: cada nodo, exceto a raiz, tem no mı́nimo m/2 filhos
P3: a raiz tem pelo menos 2 filhos, exceto se for uma folha
P4: todas as folhas aparecem em um mesmo nı́vel
P5: nodos com k itens têm k+1 filhos.

A Figura 13.1 exemplifica uma árvore B de ordem 5, onde so-


mente as chaves são mostradas, sendo as demais informações vin-
culadas a cada item omitidas para simplificar a apresentação.

Figura 13.1 Uma Árvore B de Ordem 3

Tipo abstrato ArvoreB


A Listagem 13.2 esquematiza o tipo abstrato de dados ArvoreB
definido pelas operações pesquise, insira e retire.
Com o objetivo de simplificar a presente apresentação, a árvore B
aqui apresentada é suposta armazenada integralmente em memória
interna. Dessa forma, os apontadores usados nos nodos da estru-
tura são apenas referências a objetos do tipo Nodo. No caso de
armazenamento dessa estrutura de dados em arquivos externos,
a presente implementação deve ser devidamente modificada para
351

que os acessos aos nodos localizados nos arquivos e sua atualização


sejam feitos via operações de entrada e saı́da. Os demais elementos
da implementação continuam válidos.
1 public class ArvoreB {
2 private Nodo raiz;
3 private final int m;
4 private boolean depura = false;
5 public ArvoreB(int ordem) {
6 if (ordem < 3) ordem = 3;
7 if (ordem % 2 == 0) ordem++;
8 this.m = ordem - 1;
9 }
10 public Item pesquise(String chave) {...}
11 public void insira(Item item) throws ChInvalida {...}
12 "Métodos privados auxiliares de insira";
13 public Item retire(String chave) {...}
14 "Métodos privados auxiliares de retire";
15 }

Listagem 13.2 Estrutura do TAD ArvoreB

O único erro previsto no uso do tipo ArvoreB é o definido pela


classe de exceção public class ChInvalida extends Exception {}.
A função construtora de ArvoreB permite definir a ordem da
árvore. Observe que, como as árvores de ordem 1 ou 2 não são
de interesse, porque as árvores binárias de pesquisa cumprem bem
seu papel, a presente implementação de árvore B considera uma
ordem ≥ 3 e garante que o número máximo de itens por nodo da
árvore seja sempre um número par2.
O campo interno m da classe ArvoreB contém o número máximo
de itens permitido por nodo, conforme definido pela construtora
da classe, e os itens, sempre armazenados em ordem crescente de
2
A rigor, a ordem da árvore não precisa ser um número ı́mpar, mas essa exigência torna a presente
implementação mais clara e não reduz a aplicabilidade da estrutura.
352 CAPÍTULO 13. ÁRVORES B

1 public class Item {


2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 }

Listagem 13.3 Item de uma Árvore B

suas chaves nos nodos da árvore, são objetos do tipo Item definido
na Listagem 13.3. Cada par formado por um item e seu apontador
de nodo-filho associado é chamado de célula.
Cada nodo da árvore é um objeto do tipo Nodo, o qual está defi-
nido na Listagem 13.4, onde o campo n < m+2 informa o número de
itens nele armazenados. Os itens são indexados de 1..n, enquanto
os nodos-filhos associados a esses itens são indexados de 0..n. As
posições extras de ı́ndice m+1 dos arranjos itens e filhos de cada
nodo são usadas como área de trabalho pela operação insira.

1 class Nodo {
2 int n = 0;
3 Item[] itens;
4 Nodo[] filhos;
5 Nodo(int m) {
6 itens = new Item[m+2]; // indexado de 1..n < m+2
7 filhos = new Nodo[m+2]; // indexado de 0..n < m+2
8 itens[0] = new Item(null);
9 }
10 int localizeItem(String chave) {...}
11 void apague(int i) {...}
12 }

Listagem 13.4 Tipo Nodo da Árvore.B

Todos os nodos devem ser criados com exatamente o mesmo


tamanho máximo, o qual é definido pela ordem da árvore.
O apontador [Link][0] de qualquer nodo p serve para arma-
353

zenar a referência do nodo que é raiz da subárvore contendo itens


com chaves menores que a chave do item [Link][1]. O apontador
[Link][i], para cada 1 < i < p.n, tem sempre a referência à raiz
da subárvore de nodos contendo itens cujas chaves sejam maiores
que a chave de [Link][i] e menores que a de [Link][i+1]. E
o apontador [Link][p.n] armazena a referência à subárvore de
nodos contendo itens com chaves maiores que a de [Link][p.n] e
menores que as chaves em [Link][k+1], onde q é o endereço do
nodo pai de p, i.e., p == [Link][k], para algum k : 0..q.n-1.
A operação localizeItem da Listagem 13.5 retorna o ı́ndice do
primeiro item do vetor itens do nodo corrente cuja chave seja igual
ou menor que a chave dada, varrendo esse vetor do seu fim para
o inı́cio. No caso de menor, a chave procurada está na subárvore
apontada por [Link][i], onde p denota o nodo corrente.
1 int localizeItem(String chave) {
2 int i = this.n;
3 itens[0].chave = chave;
4 while([Link](itens[i].chave) < 0) i--;
5 return i;
6 }

Listagem 13.5 Operação [Link]

Assim, o ı́ndice retornado pelo localizeItem tem a mesma chave


que lhe foi passada como parâmetro ou é a chave imediatamente
menor.
A operação apague da Listagem 13.6 simplesmente remove um
item do nodo corrente e o apontador filhos associado e faz a devida
compactação das células restantes no nodo para não deixar espaços
vagos entremeados.
354 CAPÍTULO 13. ÁRVORES B

1 void apague(int i) {
2 if ((i < 1) || (i > this.n)) return;
3 for (int j = i; j < this.n; j++) {
4 itens[j] = itens[j+1];
5 filhos[j] = filhos[j+1];
6 }
7 this.n--;
8 }

Listagem 13.6 Operação [Link]

13.1 Pesquisa por itens

Um nodo r com m itens e m+1 filhos está representado pela Fi-


gura 13.7, onde K1 < · · · < Kn são as chaves dos itens armazena-
dos no nodo, e os pi, 0 ≤ i ≤ m, são os nodos-filhos associados a
esses itens.

Figura 13.7 Pesquisa

O algoritmo de pesquisa por um item dada sua chave x inicia-


se no nodo apontado pela raiz da árvore B e, para cada nodo r
visitado, procede da seguinte forma:
• se Ki < x < Ki+1, para 1 ≤ i ≤ m, então continue a pesquisa
no nodo pi
13.2. IMPLEMENTAÇÃO DA OPERAÇÃO PESQUISE 355

• se x > Km, então continue pesquisa no nodo pm,


• se x < K1, então continue pesquisa em p0
• se x = Ki para algum 1 ≤ i ≤ m, então o item foi encontrado.
A presente implementação da árvore B não permite itens com
chaves repetidas.

13.2 Implementação da operação pesquise

A operação pesquise implementa o algoritmo descrito acima via


o método privado sobrecarregado pesquise, que, recursivamente,
percorre os nodos da árvore localizados no caminho de descida
desde a raiz até o nodo ao qual o item procurado pertence ou
deveria estar localizado. O item, se localizado, é retornado por
pesquise, e, caso não esteja na árvore, null é retornado.

1 public Item pesquise(String chave) {


2 if (chave == null) return null;
3 return pesquise(chave,raiz);
4 }
5 private Item pesquise(String chave, Nodo p) {
6 if (p == null) return null;
7 if ([Link]([Link][1].chave) < 0)
8 return pesquise(chave,[Link][0]);
9 else {
10 int i = [Link](chave);
11 if ([Link]([Link][i].chave) > 0)
12 return pesquise(chave,[Link][i]);
13 else return [Link][i];
14 }
15 }

Listagem 13.8 Operação [Link]


356 CAPÍTULO 13. ÁRVORES B

13.3 Inserção de itens

A operação de inserção de um novo item na árvore inicia-se com


uma busca da mesma forma que é feita na operação pesquise com
o objetivo de identificar o nodo no qual o novo item deve ser ins-
talado. A partir desse ponto, procede-se da seguinte forma:
• Em nodo com n < m itens, inserir, respeitando a ordenação
das chaves, o novo item nesse nodo, o qual passa a ter n + 1
itens.
• Se o nodo no qual o novo item foi inserido já estava cheio,
i.e., passou a ter n = m + 1 itens, estando portanto com sua
capacidade estourada, deve-se então:
1. criar um novo nodo;
2. remover o item da posição n/2 + 1 do nodo estourado, e
quardá-lo em um objeto pendente, criado para esse fim, e
que possui dois campos: o item removido e o correspon-
dente apontador de nodo-filho;
3. transferir os m/2 itens de maiores chaves do nodo estou-
rado para o novo nodo criado;
4. armazenar o endereço do novo nodo no apontador filhos
associado ao item armazenado no objeto pendente;
5. insira o objeto pendente no nodo-pai do nodo estourado;
6. e, caso o nodo-pai tenha sua capacidade estourada, repetir
esse processo para ele, e assim sucessivamente, e possivel-
mente provocando crescimento da árvore, se o nodo-raiz
também tiver sua capacidade estourada.

Exemplo de inserções sucessivas de itens


Considere a inserção de itens com as chaves 20, 40, 10, 30, 18, 7, 15,
26, 35, 22, 11, 12, 14, 23, 27, 19, 25, 1, 4, 13, 16, 17, 21, 24, 28, 29,
13.3. INSERÇÃO DE ITENS 357

45 e 50, nessa ordem, em uma árvore B de ordem 5, inicialmente


vazia.
Nas árvores apresentadas seguir são mostradas apenas as chaves
dos itens, omitindo-se as demais informações para simplificar as
figuras.

• Árvore vazia:

• Inserção de item com chave 20:

• Inserção de item com chave 40:

• Inserção de item com chave 10:

• Inserção de item com chave 30:


358 CAPÍTULO 13. ÁRVORES B

• Inserção de item com chave 18:

Propriedade P1 da árvore B violada, sendo necessário realizar


divisão do nodo estourado, e o item do meio, o de chave 20,
destacado abaixo, deve passar para o nı́vel acima para tornar-
se a nova raiz:

Inserção de item com chave 18 concluı́da:

• Inserção de item com chave 7:


13.3. INSERÇÃO DE ITENS 359

• Inserção de item com chave 15:

• Inserção de item com chave 35:

• Inserção de item com chave 26:

• Inserção de item com chave 22:


360 CAPÍTULO 13. ÁRVORES B

A propriedade P1 da árvore foi violada, sendo necessário rea-


lizar a divisão do nodo estourado e a transferência do item 30,
destacado abaixo, para o nodo acima:

Inserção do item de chave 22 concluı́da:

• Inserir registro com chave 11:

Propriedade P1 da árvore violada, realizar a divisão de nodos


e mover elemento do meio, i.e., o item 11, para o nodo no nı́vel
acima para concluir a remoção do item de chave 11:
13.3. INSERÇÃO DE ITENS 361

• Inserção de item com chave 12:

• Inserção de item com chave 14:

• Inserção de item com chave 23:


362 CAPÍTULO 13. ÁRVORES B

• Inserção de item com chave 27:

• Inserção de item com chave 19:

Necessário realizar a divisão do nodo com capacidade estou-


rada:

Inserção do item de chave 19 concluı́da:


13.3. INSERÇÃO DE ITENS 363

• Inserção de item com chave 25:

Após realizada a divisão do nodo estourado, tem-se:

E a propriedade P1 da árvore B continua violada, pois o nodo


da raiz teve sua capacidade estourada, sendo nova necessária
divisão:

E o item de chave 20 deve ser usado para construir uma novo


nodo-raiz, finalizando inserção do item com chave 25:
364 CAPÍTULO 13. ÁRVORES B

• Inserção dos itens restantes de chaves 1, 4, 13, 16, 17, 21, 24,
28, 29, 45 e 50 produz a árvore final:

13.4 Implementação da operação insira

A operação insira usa o método privado sobrecarregado de mesmo


nome para efetivamente inserir o item dado na árvore, como mostra
o código na Listagem 13.9.
O processo de inserção pode causar violação de propriedade P1
da árvore e, quando isso ocorrer, um objeto pendente deve ser
movido para cima dentro da estrutura, podendo até ocasionar o
crescimento da árvore pela criação de um novo nodo-raiz.
O objeto do tipo Pendente, que é retornado pelo método sobre-
carregado insira, conforme definido na Listagem 13.10, serve para
descrever os dados de um item excedente de um nodo que teve sua
13.4. IMPLEMENTAÇÃO DA OPERAÇÃO INSIRA 365

1 public void insira(Item item) throws ChInvalida {


2 Pendente pendente;
3 Nodo novoNodo;
4 if ((item == null) || ([Link] == null))
5 throw new ChInvalida();
6 pendente = insira(item,raiz);
7 if (pendente == null) return;
8 novoNodo = new Nodo(m); // crie nova raiz
9 novoNodo.n = 1;
10 [Link][1] = [Link];
11 [Link][1] = [Link];
12 [Link][0] = raiz;
13 raiz = novoNodo;
14 }

Listagem 13.9 Operação [Link]

capacidade estourada e que deve ser movido para o nodo acima na


árvore. Um objeto excedente consiste no item a ser movido e o
associado apontador da subárvore que contém chaves maiores que
a desse item.
1 class Pendente {
2 Item item;
3 Nodo filho;
4 Pendente(Item item, Nodo filho) {
5 [Link] = item;
6 [Link] = filho;
7 }
8 }

Listagem 13.10 Item Pendente de Inserção

O refinamento de "Métodos privados auxiliares de insira", que


está anunciado na classe ArvoreB apresentada na Listagem 13.2,
consiste inicialmente na declaração dos métodos cujos cabeçalhos
366 CAPÍTULO 13. ÁRVORES B

estão detalhados na Listagem 13.11.


1 private Pendente insira(Item item, Nodo p) {...}
2 private Pendente instaleNoNodo(
3 Pendente pendente, Nodo p) {...}
4 private Pendente dividaNodo(Nodo p) {...}

Listagem 13.11 Métodos privados Usados por [Link]

O método insira da Listagem 13.12 inicia-se pela visita ao nodo-


raiz da árvore e busca o ponto de inserção do novo item com base
no valor de sua chave.
1 private Pendente insira(Item item, Nodo p) {
2 int i;
3 Pendente pendente = null;
4 if (p == null) return new Pendente(item,null);
5 if ([Link]([Link][1].chave) < 0)
6 pendente = insira(item, [Link][0]);
7 else {
8 i = [Link]([Link]);
9 if ([Link]([Link][i].chave) > 0)
10 pendente = insira(item, [Link][i]);
11 else return null; // já está na árvore
12 }
13 if (pendente != null) {
14 pendente = instaleNoNodo(pendente,p);
15 }
16 return pendente;
17 }

Listagem 13.12

A busca do ponto de inserção baseia-se no fato de que, em um


dado nodo p, [Link][0] aponta para a subárvore que contém
itens com chaves menores que [Link][1], [Link][p.n] aponta
para a subárvore com chaves maiores que a de [Link][p.n], e
13.4. IMPLEMENTAÇÃO DA OPERAÇÃO INSIRA 367

que cada apontador [Link][i] identifica a subárvore de chaves


maiores que a de [Link][i] e menores que as armazenadas em
[Link][i+1], para 1 ≤ i < p.n.
Uma vez localizado o nodo no qual o item deve ser inserido, um
objeto pendente contendo esse item é criado e, então procede-se
com sua instalação em sua correta posição dentro de seu nodo pelo
método instaleNoNodo da Listagem 13.13.
1 private Pendente instaleNoNodo(Pendente pendente, Nodo p) {
2 String chave = [Link];
3 int i;
4 [Link][0].chave = chave;
5 i = p.n;
6 while ([Link]([Link][i].chave) < 0) {
7 [Link][i+1] = [Link][i];
8 [Link][i+1] = [Link][i];
9 i--;
10 }
11 [Link][i+1] = [Link];
12 [Link][i+1] = [Link];
13 p.n++;
14 if (p.n <= m) return null;
15 return dividaNodo(p);
16 }

Listagem 13.13 Instalação de um Item em um Nodo

Observe que para garantir que sempre haverá espaço, ainda que
temporariamente, para um novo item em qualquer nodo, mesmo
que o nodo já esteja conceitualmente cheio, todo nodo foi conve-
nientemente declarado com uma posição extra. Assim, se, após a
instalação do novo item, o número de itens do nodo for menor ou
igual a m, instaleNodo retorna null para indicar que a operação
terminou com sucesso. Caso contrário, se o número de itens for
superior a m, que é o número máximo permitido, o nodo deve ser
368 CAPÍTULO 13. ÁRVORES B

dividido, via o método dividaNodo da Listagem 13.14.


1 private Pendente divideNodo(Nodo p) {
2 Nodo novoNodo = new Nodo(m);
3 Pendente pendente;
4 int metade = m/2;
5 int meio = metade + 1;
6 p.n = metade;
7 novoNodo.n = metade;
8 for (int i = 1; i <= metade; i++) {
9 [Link][i] = [Link][meio + i];
10 [Link][i] = [Link][meio + i];
11 }
12 [Link][0] = [Link][meio];
13 pendente = new Pendente([Link][meio],novoNodo);
14 return pendente;
15 }

Listagem 13.14 Divisão de umo nodo

Nesse processo de divisão, o item localizado na posição do meio


do vetor de itens do nodo é usado para criar o objeto pendente
que deve ser movido para o nodo acima na hierarquia, e os itens de
chaves superiores a do item que estava no meio são transferidos para
um novo nodo. O apontador filhos[0] do novo nodo é iniciado
com o apontador de filho associado ao item do meio, o qual passa
a ter o endereço do novo nodo.
Observe que o objeto pendente retornado por dividaNodo é tam-
bém repassado a frente por instaleNodo até que o local apropriado
para sua instalação seja encontrado. A partir de então um apon-
tador null passa a ser retornado por esses métodos, para indicar
que a instalação se completou. Note também que se um objeto
pendente diferente de null for retornado à operação insira da Lis-
tagem 13.9, uma nova raiz para a árvore deve ser criada, causando
o crescimento da estrutura.
13.5. REMOÇÃO DE ITENS 369

13.5 Remoção de itens

A operação de remoção de um item de uma árvore B inicia-se com


uma busca pela chave do item, da mesma forma que é feita na
operação pesquise, com o objetivo de identificar o nodo onde o
item a ser removido está instalado.
Caso o nodo encontrado seja um nodo interno, o item encon-
trado não pode ser fisicamente removido, porque isso causaria um
desarranjo na estrutura. Para evitar isso, o item a ser removido
deve ser substituı́do pelo seu antecessor, e este ocupava removido
em seu lugar.
Assim, quando o nodo do item a ser efetivamente removido es-
tiver devidamente identificado, procede-se da seguinte forma:
• Se item a ser removido estiver em nodo-folha com mais de m/2
itens, deve-se simplesmente apagá-lo do nodo, e em seguida
fazer a devida compactação do vetor de itens.
• Entretanto, se o nodo do item a ser removido contiver exata-
mente m/2 itens, a sua remoção causaria violação de proprie-
dade P2 da árvore. Nesse caso, para evitar essa violação, deve-
se fazer o empréstimo de um item do nodo-irmão adjacente que
tenha mais que m/2 itens. Entretanto, se os nodos-irmãos ad-
jacentes possuem apenas m/2 itens cada um, o empréstimo não
resolve, devendo-se fazer a fusão do nodo violado com um de
seus irmãos. E o processo de fusão envolve a transferência de
um item do nodo-pai para o nodo resultante da fusão e isso
pode provocar novas fusões ou empréstimos no percurso de
volta à raiz da árvore.

Em suma, a remoção de um item pode causar rearranjos de itens


370 CAPÍTULO 13. ÁRVORES B

entre nodos e, inclusive de remoção de nodos, podendo até reduzir


a altura da árvore.

Exemplo de remoções sucessivas de itens


Dada a árvore B de ordem 5

mostram-se, a seguir, os principais passos para remoção dos itens


com as chaves 19, 18, 17, 21, 1, 4, 29, 28, 27, 13, 25, 24, 20, 22, 35,
40, 16, 23, 26, 15, 14, 50, 45, 10, 7, 11, 12 e 30.
• Remoção do item de chave 19:
O nodo que contém o item de chave 19 tem quatro itens, e
assim o item pode ser simplesmente removido, produzindo:

• Remoção do item de chave 18:


Como no caso anterior, esse item pode também ser simples-
mente removido, produzindo a árvore:
13.5. REMOÇÃO DE ITENS 371

• Remoção do item de chave 17:


A remoção do item causa a violação da propriedade P2, pois
o mı́nimo de itens permitido por nodo é 2, sendo então ne-
cessário identificar um irmão para empréstimo de um item:

O processo de empréstimo envolve o nodo-pai, porque é ne-


cessário preservar a ordem crescente da chaves dos itens dentro
de seus nodos:
372 CAPÍTULO 13. ÁRVORES B

Realizado o empréstimo de um item do irmão da esquerda,


com a transferência do item 15 do nodo-pai para o nodo vio-
lado e o item 14 para o nodo-pai, tem-se:

Remoção do item com chave 17 concluı́da:

• Remoção do item de chave 21:


13.5. REMOÇÃO DE ITENS 373

• Remoção do item de chave 1:

• Remoção do item de chave 4:

• Remoção do item 29:


374 CAPÍTULO 13. ÁRVORES B

• Remoção do item de chave 28:

• Remoção do item de chave 27:

Nodo que contém o item de chave 26 viola propriedade P2,


mas seu irmão à sua direita pode emprestar-lhe um item, en-
volvendo os itens destacados em:

Assim, a realização do empréstimo causa a transferência do


item 30 do nodo-pai para o nodo violado e do item 35 para o
13.5. REMOÇÃO DE ITENS 375

nodo-pai, produzindo:

Remoção do item com chave 27 concluı́da:

• Remoção do item de chave 13:

A remoção causa a violação de P2, pois o nodo envolvido fi-


cou com apenas um item. Nesse caso, empréstimo de item de
irmão não é possı́vel, porque ambos os irmãos adjacentes têm
exatamente o número mı́nimo de itens. A solução então é a
376 CAPÍTULO 13. ÁRVORES B

fusão do nodo violado com seu irmão à esquerda. Inicialmente,


tem-se:

Passo 1 da fusão com irmão à esquerda produz:

Passo 2 da fusão com irmão à esquerda produz:


13.5. REMOÇÃO DE ITENS 377

Passo 3 da fusão com irmão à esquerda produz:

Fusão concluı́da:

Entretanto, a propriedade P2 do nodo pai acabou sendo vio-


lada, sendo necessário uma nova fusão de nodos, que, no caso,
é a fusão do nodo com o item de chave 14 com o nodo-irmão
à sua direita, que é então iniciada:
378 CAPÍTULO 13. ÁRVORES B

Itens de chaves 14 e 20 são devidamente transferidos para o


irmão selecionado, causando mudança de nodo-raiz:

Remoção de item com chave 13 concluı́da:

• Remoção do item de chave 25:

A célula do item 25 localizada não pode ser removida porque


tem filho, devendo-se substituı́-la por seu antecessor, o de chave
24, cuja célula pode ser então removida:
13.5. REMOÇÃO DE ITENS 379

A substituição do item de chave 25 pelo de seu antecessor pro-


duz a seguinte árvore:

Remoção da célula original do antecessor conclui a remoção do


item de chave 25:

• Remoção do item de chave 24:

Célula de 24 não pode ser removida, sendo necessário localizar


380 CAPÍTULO 13. ÁRVORES B

seu antecessor, que no caso é o item de chave 23:

Substituição do item de chave 24 pelo de seu antecessor 23:

A remoção da célula original do antecessor provoca uma vi-


olação de P2:

Escolha do irmão à esquerda para realizar fusão de nodos:


13.5. REMOÇÃO DE ITENS 381

Realização da fusão com irmão da esquerda:

Remoção do item de chave 24 concluı́da:

• Remoção dos itens de chaves 20 e 22:

• Remoção do item de chave 35:

Célula com item de chave 35 localizada, mas não pode ser re-
382 CAPÍTULO 13. ÁRVORES B

movida, devendo ser substituı́da pela de seu antecessor:

A substituição do item a ser removido pelo seu antecessor pro-


duz:

A remoção da célula original do antecessor produz:

Propriedade P2 da árvore violada, devendo-se fazer empréstimo


de item do irmão à direita:
13.5. REMOÇÃO DE ITENS 383

O empréstimo então produz:

Assim, a remoção do item de chave 35 produz:

• Remoção do item de chave 40:


384 CAPÍTULO 13. ÁRVORES B

Célula com item de chave 40 localizada, mas não pode ser re-
movida, devendo ser substituı́da pela de seu antecessor:

Substituição do item a ser removido pelo seu antecessor:

A remoção da célula original do antecessor produz:

Propriedade P2 da árvore violada requer fusão do nodo vio-


lada com um de seus irmãos:
13.5. REMOÇÃO DE ITENS 385

E a fase 2 da fusão produz:

Remoção do item de chave 40 concluı́da:

• Remoção dos itens de chaves 16, 23 e 26 produz:

Propriedade P2 da árvore violada demanda realização de empréstimo


de um item do irmão à esquerda, que produz:
386 CAPÍTULO 13. ÁRVORES B

• Remoção do item de chave 15:

Propriedade P2 violada demanda realização de empréstimo de


item do irmão à esquerda, produzindo:

• Remoção do item de chave 14:

Propriedade P2 violada demanda realização de sua fusão com


o irmão à sua esquerda, produzindo:
13.5. REMOÇÃO DE ITENS 387

Remoção do item de chave 14 concluı́da:

• Remoção do item de chave 50:

Violada propriedade P2 da árvore, e irmão à esquerda faz


empréstimo para concluir remoção de 50:
388 CAPÍTULO 13. ÁRVORES B

• Remoção do item de chave 45:

Violada propriedade P2 da árvore, e irmão à esquerda pode


fazer empréstimo. A remoção de item de chave 45 então pro-
duz:

• Remoção do item de chave 10:

Violada propriedade P2 da árvore, sendo necessário fusão com


nodo do irmão da direita:
13.6. IMPLEMENTAÇÃO DA OPERAÇÃO RETIRE 389

Remoção do item de chave 10 concluı́da:

• Remoção dos itens de chaves 7, 11, 12 e 30:

13.6 Implementação da operação retire

A operação retire que está definida na Listagem 13.17 delega sua


tarefa ao método privado remova, que, dados a chave de pesquisa,
o endereço do nodo em análise e o de seu nodo-pai, percorre o
caminho da raiz da árvore B até o nodo que contém o item a ser
removido, como é feito na operação pesquise da Listagem 13.8.

1 public Item retire(String chave) {


2 return remova(chave, null, 0, raiz);
3 }

Listagem 13.15
390 CAPÍTULO 13. ÁRVORES B

O método remova, definido na Listagem 13.16, retorna o item


removido ou null, caso item com a dada chave não seja encontrado.

1 private Item remova(


2 String chave, Nodo pai, int k, Nodo p) {
3 Item item = null;
4 if ((chave == null) || (p == null)) return null;
5 if ([Link]([Link][1].chave) < 0) {
6 item = remova(chave,p,0,[Link][0]);
7 checkNodo(pai,k,p);
8 } else if ([Link]([Link][p.n].chave) > 0){
9 item = remova(chave,p,p.n,[Link][p.n]);
10 checkNodo(pai,k,p);
11 } else {
12 int i = [Link](chave);
13 if ([Link]([Link][i].chave) > 0){
14 item = remova(chave,p,i,[Link][i]);
15 checkNodo(pai,k,p);
16 } else {
17 item = [Link][i];
18 if ([Link][i] == null) {
19 [Link](i);
20 checkNodo(pai,k,p);
21 } else {
22 [Link][i] = antecessor([Link][i-1],item);
23 item = remova(chave,p,i-1,[Link][i-1]);
24 checkNodo(pai,k,p);
25 }
26 }
27 }
28 return item;
29 }

Listagem 13.16

Após a remoção do item, é feita, para todo nodo visitado, via o


método checkNodo, a verificação da propriedade da árvore de que
13.6. IMPLEMENTAÇÃO DA OPERAÇÃO RETIRE 391

o nodo continua com pelo menos m/2 itens, e, se for identificada


violação, providências sejam tomadas para restabelecê-la.
O refinamento de "Métodos privados auxiliares de retire" anun-
ciado na Listagem 13.2 é detalhado na Listagem 13.17, onde estão
definidos os cabeçalhos dos métodos privados que auxiliam a remo-
ção de itens.
1 private Item antecessor(Nodo p, Item item) {...}
2 private Item remova(
3 String chave, Nodo pai, int k, Nodo p){...}
4 private void checkNodo(Nodo pai, int k, Nodo p){...}
5 private void transfereDaDireita(
6 Nodo irmao, Nodo pai, int k, Nodo p) {...}
7 private void transfereDaEsquerda(
8 Nodo irmao, Nodo pai, int k, Nodo p) {...}
9 private void fundeComEsquerda(
10 Nodo irmao, Nodo pai, int k, Nodo p) {...}
11 private void fundeComDireita(
12 Nodo irmao, Nodo pai, int k, Nodo p) {...}

Listagem 13.17 Métodos privados Usados por [Link]

O método antecessor da Listagem 13.18 é usado quando o item


a ser removido pertence a um nodo interno, devendo, assim, ser
substituı́do pelo seu item antecessor, que, por definição, sempre
está localizado em nodo-folha.
1 private Item antecessor(Nodo p, Item item) {
2 Item itemDoAntecessor;
3 while ([Link][p.n] != null) {p = [Link][p.n];}
4 itemDoAntecessor = [Link][p.n];
5 [Link][p.n] = item;
6 return itemDoAntecessor;
7 }

Listagem 13.18
392 CAPÍTULO 13. ÁRVORES B

O método checkNodo, detalhado na Listagem 13.19, verifica se a


propriedade P2 da árvore foi ou não violada pela remoção.
1 private void checkNodo(Nodo pai, int k, Nodo p) {
2 Nodo irmaoEsq = null, irmaoDir = null;
3 if (p.n >= m/2) return;
4 if (p == raiz) {
5 if (raiz.n == 0) raiz = [Link][1];
6 return;
7 }
8 if (k == 0) {
9 irmaoDir = [Link][1];
10 if (irmaoDir.n==m/2)fundeComDireita(irmaoDir,pai,1,p);
11 else transfereDaDireita(irmaoDir,pai,k,p);
12 } else if (k == pai.n) {
13 irmaoEsq = [Link][pai.n-1];
14 if (irmaoEsq.n==m/2)fundeComEsquerda(irmaoEsq,pai,k,p);
15 else transfereDaEsquerda(irmaoEsq,pai,k,p);
16 } else {
17 irmaoEsq = [Link][k-1];
18 irmaoDir = [Link][k+1];
19 if ((irmaoEsq.n == m/2) && (irmaoDir.n == m/2))
20 fundeComEsquerda(irmaoEsq,pai,k,p);
21 else {
22 if (irmaoEsq.n >= irmaoDir.n)
23 transfereDaEsquerda(irmaoEsq,pai,k,p);
24 else transfereDaDireita(irmaoDir,pai,k,p);
25 }
26 }
27 }

Listagem 13.19

Em caso de violação, a prioridade é fazer empréstimo de item


de irmão do nodo violado. Não sendo possı́vel, deve-se fazer fusão
do nodo violado com o irmão que tiver menos itens.
No caso de empréstimo, o item do nodo-pai que detém o aponta-
13.6. IMPLEMENTAÇÃO DA OPERAÇÃO RETIRE 393

1 private void transfereDaDireita(


2 Nodo irmao, Nodo pai, int k, Nodo p) {
3 [Link][p.n+1] = [Link][k+1];
4 [Link][p.n+1] = [Link][0];
5 [Link][k+1] = [Link][1];
6 [Link][0] = [Link][1];
7 for (int j = 1; j < irmao.n; j++) {
8 [Link][j] = [Link][j+1];
9 [Link][j] = [Link][j+1];
10 }
11 p.n++; irmao.n--;
12 }

Listagem 13.20

dor para o nodo violado é movido para este nodo, e o último item
do irmão à esquerda ou o primeiro do irmão à direita o substitui
no nodo-pai. O método transferedaDireita da Listagem 13.20 faz
o empréstimo de item do irmão à direita. E a transferência de item
do irmão à esquerda é efetuada pelo método transfereDaEsquerda,
que está detalhado na Listagem 13.21.

1 private void transfereDaEsquerda(


2 Nodo irmao, Nodo pai, int k, Nodo p) {
3 for (int j = p.n; j >= 1; j--) {
4 [Link][j+1] = [Link][j];
5 [Link][j+1] = [Link][j];
6 }
7 [Link][1] = [Link][k];
8 [Link][1] = [Link][0];
9 [Link][0] = [Link][irmao.n];
10 [Link][k] = [Link][irmao.n];
11 p.n++; irmaoo.n--;
12 }

Listagem 13.21
394 CAPÍTULO 13. ÁRVORES B

A fusão de nodos é definida pelos métodos fundeComEsquerda e


fundeComDireita, que estão detalhados nas listagens 13.22 e 13.23,
respectivamente.
1 private void fundeComEsquerda(
2 Nodo irmao, Nodo pai, int k, Nodo p) {
3 [Link][irmao.n+1] = [Link][k];
4 [Link][irmao.n+1] = [Link][0];
5 for (int j = 1, t = irmao.n+2; j <= p.n; j++, t++) {
6 [Link][t] = [Link][j];
7 [Link][t] = [Link][j];
8 }
9 irmao.n += p.n + 1;
10 [Link](k);
11 }

Listagem 13.22

1 private void fundeComDireita(


2 Nodo irmao, Nodo pai, int k, Nodo p) {
3 for (int t=irmao.n,r=irmao.n+p.n+1; t > 0 ; r--,t--) {
4 [Link][r] = [Link][t];
5 [Link][r] = [Link][t];
6 }
7 for (int t = 1; t <= p.n; t++) {
8 [Link][t] = [Link][t];
9 [Link][t] = [Link][t];
10 }
11 [Link][p.n+1] = [Link][k];
12 [Link][p.n+1] = [Link][0];
13 [Link][0] = [Link][0];
14 irmao.n += p.n + 1;
15 [Link](k);
16 }

Listagem 13.23
13.7. ANÁLISE DOS CUSTOS DAS OPERAÇÕES 395

13.7 Análise dos custos das operações

As operações de inserção, remoção e busca em uma árvore B con-


sistem inicialmente em um percurso descendente desde a raiz da
árvore até um nodo-folha. E cada nodo visitado nesse processo
demanda uma pesquisa, sequencial ou binária, dentre os itens do
nodo para identificar a subárvore a ser seguida nesse percurso.
No caso de a árvore B estar armazenada em arquivos, o fator
determinante do custo é o de leitura e gravação de nodos. Do-
nald Knuth, no livro The Art of Computer Programming, vol 3,
página 476, mostra que uma árvore B de ordem m com N chaves
possui as seguintes propriedades [7]:
• o número de nodos nos nı́veis 1, 2, 3, ..., l são, respectivamente,
2, 2dm/2e, 2dm/2e2, · · · , 2dm/2el−1.
• número de itens N é tal que N + 1 ≥ 2dm/2el−1
• o nı́vel máximo l é tal que l ≤ 1 + logdm/2e ( N2+1 )

Os custos das operações de inserção, remoção e busca, no pior


caso, são proporcionais à altura da árvore, que é dada pelo valor
do nı́vel mais baixo, sendo, portanto, sempre ≤ 1 + logdm/2e ( N2+1 ).
A esses custos, deve-se acrescentar o de localizar itens dentro
de cada nodo e os de leitura ou gravação de nodos. Em árvore B
de grande porte, essa parcela de custo pode ser desprezı́vel para
a fixação do custo final, exceto se a ordem da árvore for muito
elevada, quando esses custos adicionais podem torna-se represen-
tativos. Por exemplo, uma árvore de ordem 199 e com um total
de cerca de dois milhões de itens, segundo Knuth [7], tem um nı́vel
máximo l = 3, o que garante um custo de leitura e escrita de dados
no arquivo externo bastante pequeno.
Um outro fator de custo importante é o de divisão de nodos que
pode ocorrer em operações de inserção, quando, no pior caso, o
396 CAPÍTULO 13. ÁRVORES B

número médio de divisões de nodos por inserção seria proporcional


à altura da árvore.

13.8 Variantes de Árvores B*

Variações importantes de Árvore B oferecem soluções otimizadas


para nichos de aplicação especı́ficos. Nessas propostas de variações,
os nodos internos da árvore B são recuperados do arquivo de itens
para formar um ı́ndice de acesso de alta eficiência. Esse ı́ndice é
mantido na memória interna durante a execução do programa e
fornece acesso direto aos itens desejados que estão no arquivo.
As principais estruturas derivadas de árvore B são Árvores B*,
Árvores B+, Árvores 2-3 e Árvores B Binárias [7].

Árvores B*
Definição 13.2 Árvore B* é uma variante da árvore B, na
qual exige-se que cada nodo esteja pelo menos 2/3 completa.

Em B*, os nodos internos da árvore têm somente as chaves dos


itens, e somente as folhas contêm os itens completos.

Árvores B+
Definição 13.3 Árvore B+ é uma implementação de árvore
B em que todos os itens são armazenados no último nı́vel, isto
é, nas folhas, e que:
• os nodos internos contêm somente as chaves dos itens e
os apontadores para filhos;
• o item correspondente a uma chave fica armazenado na
subárvore à esquerda dessa chave;
13.8. VARIANTES DE ÁRVORES B* 397

• os nodos internos formam um ı́ndice cuja organização é


a de uma árvore B;
• as folhas podem ser encadeadas para acesso sequencial.

Tipicamente, uma árvore B+ tem o seguinte formato:

onde, dentro do triângulo, estão os nodos com as chaves dos itens,


e os nodos desenhados em verde armazenam os itens completos e
são encadeados para permitir acesso sequencial à estrutura, como
ilustra o seguinte exemplo:

onde L aponta para a lista encadeada contendo todos os itens.


398 CAPÍTULO 13. ÁRVORES B

Árvores 2-3
Definição 13.4 Árvore 2-3 é caso especial da árvore B, na
qual todo nodo interno tem 2 ou 3 filhos ou subárvores, e os
itens completos ficam nas folhas.

Árvores 2-3 são mais apropriadas para estruturas armazena-


das em memória primária, e têm a vantagem de consumir menos
memória que uma árvore B tradicional, porque os nodos sendo
menores têm menos espaço de reserva para inserção de novos itens.
O exemplo a seguir mostra uma árvore 2-3 com as chaves A, C, D,
E, F, G, J, K, L, N, R, T, V e X formando os nodos internos, e
os externos, apenas indicados nas folhas, que contêm os itens com-
pletos.

Árvores B Binárias
Árvores B binárias são árvores 2-3 armazenadas como árvores biná-
rias, i.e., todo nodo tem apenas uma chave. Para chegar a essa
representação, nodos com duas chaves são divididos em dois: o
primeiro fica com a primeira chave e o apontador de filhos menores
que essa chave, e segundo nodo recebe a segunda chave e os dois
apontadores a ele associados.
13.8. VARIANTES DE ÁRVORES B* 399

Por razões de legibilidade, o primeiro nodo aponta para o se-


gundo nodo de cada divisão por meio de apontadores desenhados
horizontalmente. Isso permite que visualmente a altura de todos
as folhas sejam iguais, mas efetivamente, há alturas que podem ser
o dobro de outras. Por exemplo, considere a seguinte árvore 2-3,
onde os nodos-folhas, desenhados na cor verde, contêm os itens
completos, e os internos têm apenas chaves:

Essa árvore 2-3 pode ser transformada na seguinte árvore B


binária:

Claramente, árvores B binárias têm uma assimetria inerente:


• apontadores à esquerda, sempre verticais, apontam para um
nodo no nı́vel abaixo
• apontadores à direita podem ser verticais ou horizontais.
A eliminação dessa assimetria produz as chamadas árvores B
400 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

binárias simétricas (Symmetric Binary B-trees), ou simplesmente


árvores SBB, que são o tema do próximo capı́tulo.

13.9 Conclusão

Árvores B são um tipo de estrutura de dados que pode permitir re-


cuperação muito eficiente de informação armazenada em memória
secundária com custos proporcionais ao logaritmo do número de
itens e que minimiza o número de acessos à memória secundária.

Exercı́cios
1. Como os algoritmos de inserção, pesquisa e remoção de uma
árvore B devem ser modificados para aceitar diferentes itens
com mesma chave?

Notas Bibliográficas

O texto fundamental para um estudo aprofundado sobre árvores


B são os livros clássicos de Donald Knuth [6, 7], que dão um tra-
tamento formal e organizado para a matéria.
***
Capı́tulo 14

Árvores SBB

O verdadeiro fruto da árvore do conhecimento


é a simplicidade.
Mário Quintana (1906-1994)

Árvores binárias são uma estrutura de dados que permitem im-


plementar dicionários em que os custos médios de suas operações
podem ser proporcionais ao logaritmo do número de itens nela ar-
mazenados, mas têm o defeito de poderem ficar desbalanceadas,
comprometendo seu desempenho.
Árvores binárias perfeitamente balanceadas solucionam esse pro-
blema garantindo sempre um custo ótimo nas operações de pes-
quisa, inserção ou remoção de itens, mas, em geral, é custoso man-
ter esse nı́vel de desempenho quando ocorrem muitas operações de
inserção e remoção.
Árvores AVL são uma alternativa que, embora não apresentem
o custo ótimo das perfeitamente balanceadas para suas operações,
permitem que o rebalanceamento requerido após inserções e remo-
ções seja comparativamente menos custoso.
Uma outra solução para reduzir os custos de rebalanceamento de
árvores binárias de pesquisa, enquanto garante-se um desempenho
satisfatório para suas operações, são as Árvores Binárias Simétricas
ou SBB.
401
402 CAPÍTULO 14. ÁRVORES SBB

Definição 14.1 Árvore SBB (Symmetric Binary B-Tree) é


uma árvore binária estendida de pesquisa em que seus aponta-
dores de subárvores podem ser verticais ou horizontais, e que
obedece sempre às seguintes propriedades:
P1: todos os caminhos da raiz até cada nodo externo pos-
suem o mesmo número de apontadores verticais;
P2: não existem na árvore dois apontadores horizontais su-
cessivos.

A seguinte figura ilustra o aspecto gráfico de uma árvore SBB:

O rebalanceameno de uma SBB consiste em garantir a validade


de suas propriedades após inserções e remoções de itens.
Em árvores SBB, pode-se distinguir dois tipos de alturas:
• Altura vertical h: número de apontadores verticais de qualquer
caminho entre a raiz e um nodo externo.
• Altura real k : número total de apontadores no maior caminho
entre a raiz e um nodo externo.

Para uma árvore SBB com n nodos internos, temos que k, a


altura real da raiz da árvore e que denota o custo da pesquisa no
pior caso, é tal que h ≤ k ≤ 2h.
O tipo abstrato da dados Sbb apresentado na Listagem 14.1 im-
plementa uma árvore SBB, sendo caracterizado pelas operações
tradicionais de um dicionário: pequise, insira e retire.
403

1 public class Sbb {


2 private Nodo raiz;
3 private boolean fim;
4 public Sbb() {}
5 "Métodos auxiliares privados";
6 public Item pesquise(String chave) {...}
7 public void insira(Item x) {...}
8 public Item retire(String chave) {...}
9 }

Listagem 14.1 Tipo Abstrato Sbb

Os métodos auxiliares privados necessários à implementação de


pesquise, insira e retire estão anunciados na Listagem 14.2.

1 private Item pesquise(String chave, Nodo p) {...}


2 private Nodo ee(Nodo pai, boolean d, Nodo r) {...}
3 private Nodo ed(Nodo pai, boolean d, Nodo r) {...}
4 private Nodo dd(Nodo pai, boolean d, Nodo r) {...}
5 private Nodo de(Nodo pai, boolean d, Nodo r) {...}
6 private void insira(Item x,Nodo pai,boolean d,Nodo p){...}
7 private void insereFilho(Item x,Nodo pai,boolean d) {...}
8 private Nodo bayerEEED(Nodo pai, boolean d, Nodo p) {...}
9 private Nodo bayerDEDD(Nodo pai, boolean d, Nodo p) {...}
10 private Item remova(String chave,Nodo pai,boolean d,Nodo p){...}
11 private Item retireNodo(Nodo pai, boolean d, Nodo p) {...}
12 private Nodo esqCurto(Nodo pai, boolean d, Nodo p) {...}
13 private Nodo esqCurtoIeII(Nodo pai,boolean d,Nodo p) {...}
14 private Nodo esqCurtoIII(Nodo pai, boolean d, Nodo p) {...}
15 private Nodo esqCurtoIV(Nodo pai, boolean d, Nodo p) {...}
16 private Nodo dirCurto(Nodo pai, boolean d, Nodo p) {...}
17 private Nodo dirCurtoIeII(Nodo pai, boolean d, Nodo p) {...}
18 private Nodo dirCurtoIII(Nodo pai, boolean d, Nodo p) {...}
19 private Nodo dirCurtoIV(Nodo pai, boolean d, Nodo p) {...}

Listagem 14.2 Métodos Auxiliares de Sbb


404 CAPÍTULO 14. ÁRVORES SBB

Os itens armazenados nos nodos da árvore são objetos do tipo


Item da Listagem 14.3.

1 public class Item {


2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 }

Listagem 14.3 Item Armazenado pelo tipo Sbb

Cada nodo da árvore é um objeto do tipo Nodo que está definido


na Listagem 14.4,

1 class Nodo {
2 final static byte H = 0, V = 1;
3 Item item;
4 Nodo esq, dir;
5 byte bitE = V, bitD = V;
6 Nodo (Item item) {[Link] = item;}
7 }

Listagem 14.4 Nodo de Sbb

onde H e V denotam a orientação horizontal ou vertical dos apon-


tadores esquerdo e direito de cada nodo, dadas pelos campos bitE
e bitD, respectivamente.

14.1 Pesquisa por itens

A operação pesquise, definida na Listagem 14.5, em nada difere


de uma pesquisa em árvore binária de pesquisa, na qual itens com
chaves menores que a do item do nodo-raiz de qualquer subárvore
são armazenados na subárvore esquerda desse nodo-raiz, e os com
chaves maiores, na da direita.
14.2. TRANSFORMAÇÕES DE BAYER 405

1 private Item pesquise(String chave, Nodo p) {


2 int c;
3 if (p == null) return null;
4 c = [Link](chave);
5 if (c > 0)
6 return pesquise(chave,[Link]);
7 if (c < 0)
8 return pesquise(chave,[Link]);
9 return [Link];
10 }

Listagem 14.5 Método Auxiliar pesquise

Para simplificar a interface do usuário do tipo Sbb, a complexi-


dade imposta pela recurvidade da operação pesquise fica encap-
sulada pela declaração desse método como privado e definindo-se
como público o homônimo da Listagem 14.6, cuja interface é mais
simples.
1 public Item pesquise(String chave) {
2 return pesquise(chave,raiz);
3 }

Listagem 14.6 Operação [Link]

A operação pesquise retorna o item encontrado, ou então retorna


null, para indicar que o item com a chave dada não foi encontrado
na árvore.

14.2 Transformações de Bayer

As operações de inserção ou de remoção de itens podem gerar


violações das propriedades P1 e P2 da árvore SBB e, quando isso
ocorrer, a propriedade violada deve ser restaurada por meio de
transformações locais.
406 CAPÍTULO 14. ÁRVORES SBB

Novos nodos são sempre inseridos na árvore após um apontador


vertical de valor nulo que ocorre no nı́vel mais baixo da estrutura, e
esse apontador deve obrigatoriamente tornar-se horizontal. E isso,
dependendo da situação anterior da árvore, pode gerar o apare-
cimento de dois apontadores horizontais sucessivos, o que viola a
propriedade P2, que proibe dois apontadores horizontais sucessi-
vos.
Para exemplificar diversas situações em que o processo de res-
tauração de propriedades da árvore deve ser aplicado, considere a
seguinte árvore:

na qual inicia-se a inserção de um item de chave 6 como filho do


item 10, conforme mostra a figura:

A inserção de 6 à esquerda de 10 causa violação da propriedade


P1, a do número de apontadores verticais, a qual deve ser restabe-
lecida mudando a orientação do apontador 10 → 6 para horizontal:
14.2. TRANSFORMAÇÕES DE BAYER 407

A inserção de um novo nodo com item 12 produz a seguinte


configuração, que também viola a propriedade P1, como mostra a
figura:

A restauração dessa propriedade é via a mudança da orientação de


10 → 12, a qual gera a árvore a seguir, concluindo essa operação:

A inserção de novo nodo com um item de chave 13 gera a con-


figuração:
408 CAPÍTULO 14. ÁRVORES SBB

E o ato de restaurar P1, tornando o apontador 12 → 13 horizontal,


gera a seguinte violação da propriedade P2, com o aparecimento
de dois ponteiros horizontais sucessivos:

Nesse caso, para restaurar P2 deve-se tornar o nodo com chave


12 pai com apontadores verticais dos nodos 10 e 13, gerando a se-
guinte configuração, a qual atende P2, mas isso aumento número
de apontadores verticais, violando P1, como mostra a figura:

Para restaurar P1 da árvore acima, deve-se mudar a orientação


do apontador 14 → 12 para horizontal, assim reduzindo o número
de apontadores verticais no caminho de 14 até as folhas abaixo
14.2. TRANSFORMAÇÕES DE BAYER 409

dele. Entretanto, essa transformação gera uma violação de P2,


envolvendo os apontadores 5 → 14 → 12, que se tornaram hori-
zontais sucessivos:

A propriedade P2, nesse caso, pode ser restabelecida tornando


12 pai de 5 e 14, com consequente crescimento da árvore, como
mostra a seguinte figura:

Em resumo, novos nodos são sempre inseridos junto às folhas,


como filhos horizontais para respeitar P1, mas isso pode gerar
violação da propriedade P2 da SBB. Se isso ocorrer, o nodo central
de uma sequência de 2 apontadores sucessivos deve ser promovido
a pai vertical dos nodos envolvidos, podendo essa transformação
gerar violação da propriedade P1, e esse processo deve ser repetido
enquanto for necessário.
Por outro lado, em operações de retiradas de nodos, dois apon-
tadores horizontais sucessivos podem vir a ocorrer, porque a ori-
entação de algum apontador pode ter sido alterada para se fazer
410 CAPÍTULO 14. ÁRVORES SBB

respeitar propriedades da árvore.

Para ilustrar as transformações que podem ser necessárias quando


nodos são removidos de uma árvore SBB, considere a remoção do
nodo com o item de chave 4 da seguinte árvore:

que gera a seguinte configuração, na qual a propriedade P1 está


violada pelo encurtamento do ramo direito da subárvore de raiz 3.

Para restabelecer P1 deve-se mudar a orientação do apontador


3 → 2 para horizontal, mas isso gera uma violação de P2, envol-
vendo os nodos 1, 2 e 3, pelo aparecimento de dois apontadores
horizontais, além de uma violação de P1, como mostra a figura:
14.2. TRANSFORMAÇÕES DE BAYER 411

A transformação que torna 2 pai vertical de 1 e 3 elimina o


caráter sucessivo dos dois apontadores em 3 → 2 → 1 e resolve as
duas violações, gerando a árvore:

Em resumo, as transformações que restauram propriedades P1


e P2 de uma árvore SBB, que podem ter sido violadas após a
inserção ou remoção de um nodo, dependem da configuração dos
nodos diretamente envolvidos na operação e podem envolver mu-
dança de orientação de apontadores e transformações denominadas
transformações de Bayer, a saber: Esquerda-Esquerda (EE),
Esquerda-Direita (ED), Direita-Direita (DD) ou Direita-Esquerda
(DE), as quais são apresentadas a seguir.
Cada uma dessas tranformações é implementada por um método
privado que recebe como parâmetros o apontador do nodo que dá
acesso aos demais nodos envolvidos na violação, o apontador de
seu pai e a sua posição, esquerda ou direita, em relação ao seu pai.
412 CAPÍTULO 14. ÁRVORES SBB

Transformação EE:
Suponha que o nodo A da figura abaixo acabou de ser acrescentado
à árvore como filho esquerdo horizontal de B, e que isso gere uma
Violação Esquerda-Esquerda, no entorno do ponto de inserção,
onde r, tipicamente, é o apontador esquerdo ou direito do nodo
pai de C:

E para eliminar os dois apontadores horizontais sucessivos presen-


tes nessa configuração, deve-se fazer a transformação Esquerda-
Esquerda descrita na Listagem 14.7, via a chamada ee(pai,d,r),
onde pai é o endereço do pai de r e d informa se r está à esquerda
ou à direita de pai.
1 private Nodo ee(Nodo pai, boolean d, Nodo r) {
2 Nodo v = [Link];
3 [Link] = [Link]; [Link] = [Link];
4 [Link] = Nodo.V; [Link] = r; [Link] = Nodo.V;
5 if (pai != null) {
6 if (d) [Link] = v; else [Link] = v;
7 }
8 else raiz = v;
9 return v;
10 }

Listagem 14.7 Transformação Esquerda-Esquerda


14.2. TRANSFORMAÇÕES DE BAYER 413

A execução de ee(pai,d,r) transforma a configuração

na seguinte estrutura, onde a sequência de apontadores horizontais


foi eliminada:

Transformação ED:

As transformações Esquerda-Direita são realizadas pelo método ed


definido na Listagem 14.8. Essas transformações são necessárias
quando, por exemplo, um nodo B acabou de ser acrescentado à
árvore como filho direito de A no seguinte contexto:

Os dois apontadores sucessivos gerados pela inclusão de B são re-


solvidos pela execução de ed(pai,d,r), onde o parâmetro r aponta
414 CAPÍTULO 14. ÁRVORES SBB

1 private Nodo ed(Nodo pai, boolean d, Nodo r) {


2 Nodo v1 = [Link], v2 = [Link];
3 [Link] = [Link]; [Link] = Nodo.V; [Link] = v1;
4 [Link] = [Link]; [Link] = Nodo.V; [Link] = r;
5 if (pai != null) {
6 if (d) [Link] = v2; else [Link] = v2;
7 }
8 else raiz = v2;
9 return v2;
10 }

Listagem 14.8 Transformação Esquerda-Direita

para um dos filhos do nodo apontado pelo outro parâmetro pai, a


qual transforma a seguinte configuração

na seguinte estrutura, onde a sequência de apontadores horizontais


foi eliminada:

Note que o apontador r tem orientação vertical, pois se assim


14.2. TRANSFORMAÇÕES DE BAYER 415

não fosse já haveria dois apontadores horizontais antes da inserção,


e que, após a transformação acima, a orientação do apontador r
deve ser apropriadamente mudada para horizontal, no contexto de
chamada do método ed, para evitar um aumento no número de
apontadores verticais. Isso não foi feito por ed porque não lhe foi
passada qual a origem de r.

Transformação DD:
Uma transformação Direita-Direita é necessária quando, por exem-
plo, um nodo C acabou de ser acrescentado à árvore como filho
direito de B como mostrado na figura abaixo, gerando dois aponta-
dores horizontais sucessivos. Esses dois apontadores sucessivos são,
nesse caso, resolvidos pelo método dd definido na Listagem 14.9.

1 private Nodo dd(Nodo pai, boolean d, Nodo r) {


2 Nodo v = [Link];
3 [Link] = [Link]; [Link] = Nodo.V;
4 [Link] = r; [Link] = Nodo.V;
5 if (pai != null) {
6 if (d) [Link] = v; else [Link] = v;
7 } else raiz = v;
8 return v;
9 }

Listagem 14.9 Transformação Direita-Direita

A execução de dd(pai,d,r), cujo parâmetro r é um dos filhos do


nodo apontado por pai, transforma a configuração abaixo:
416 CAPÍTULO 14. ÁRVORES SBB

na seguinte estrutura, onde a sequência de apontadores horizontais


foi devidamente eliminada:

Transformação DE:

A transformação Direita-Esquerda é necessária quando, por exem-


plo, um nodo B acabou de ser acrescentado à árvore como filho
esquerdo de C como no seguinte contexto, gerando dois apontado-
res horizontais sucessivos do tipo Direita-Esquerda:

Esses dois apontadores sucessivos são, nesse caso, resolvidos pelo


método de definido na Listagem 14.10, o qual realiza uma trans-
formação Direita-Esquerda.
14.2. TRANSFORMAÇÕES DE BAYER 417

1 private Nodo de(Nodo pai, boolean d, Nodo r) {


2 Nodo v1 = [Link], v2 = [Link];
3 [Link] = [Link]; [Link] = Nodo.V; [Link] = v1;
4 [Link] = [Link]; [Link] = Nodo.V; [Link] = r;
5 if (pai != null) {
6 if (d) [Link] = v2; else [Link] = v2;
7 } else raiz = v2;
8 return v2;
9 }

Listagem 14.10 Transformação Direita-Direita

Ou seja, a execução da chamada de(pai,d,r), cujo parâmetro r


é um dos filhos de nodo apontado por pai, transforma a seguinte
configuração:

na seguinte estrutura, onde a sequência de apontadores horizontais


foi eliminada:

Os métodos ee, ed, dd e de são usados pelas operações insira e


retire.
418 CAPÍTULO 14. ÁRVORES SBB

14.3 Inserção de itens

A operação insira de uma árvore SBB é executada em três etapas:


• localização, conforme os critérios de uma árvore binária de
pesquisa, do nodo ao qual um novo nodo com o novo item
deve ser conectado;
• instalação do novo nodo como filho do nodo localizado acima;
• resolução de possı́veis violações de propriedades da árvore.

O ponto de inserção de um novo nodo na árvore é sempre como


filho de um nodo que tenha no máximo um filho, e o novo nodo
deve ser inserido como filho horizontal para preservar a propriedade
P1, mas pode, nesse processo, ocorrer violação de P2.

Exemplo de inserções sucessivas de itens


As diversas operações de restauração das propriedades definidoras
de árvore SBB são ilustradas pela inserção de nodos com chaves 7,
10, 5, 2, 4, 9, 3, 6, 1, 11, 8, 20 e 0, nesta ordem, em uma árvore
SBB inicialmente vazia.
Os passos das inserções do itens com as chaves acima são os
seguintes:

• Inicialmente tem-se:

• Inserção de item com chave 7:


14.3. INSERÇÃO DE ITENS 419

• Inserção de item com chave 10:

• Inserção de item com chave 5:

• Inserção do item com chave 2:

Gera-se violação da propriedade P2, que é restabelecida por


uma transformação EE, produzindo:
420 CAPÍTULO 14. ÁRVORES SBB

• Inserção do item com chave 4:

• Inserção de item com chave 9:

A violação da propriedade P2 acima deve ser restabelecida


por uma ransformação DE seguida de uma mudança de ori-
entação do apontador da subárvore direita do nodo de chave
5, produzindo:

• Inserção de item com chave 3:


14.3. INSERÇÃO DE ITENS 421

também gera uma violação da propriedade P2, que é restabe-


lecida por uma transformação DE seguida de uma mudança
de orientação do apontador da subárvore esquerda do nodo de
chave 5, produzindo:

• Inserção de item com chave 6:

• Inserção de item com chave 1:


422 CAPÍTULO 14. ÁRVORES SBB

• Inserção de item com chave 11:

• Inserção de item com chave 8:

• Inserção de item com chave 20:

gera uma violação de P2 com o surgimento de dois aponta-


dores horizontais sucessivos para a direita, que devem ser re-
solvidos por uma transformação DD seguida de uma mudança
de orientação do apontador direito do nodo de chave 9 para
horizontal, produzindo:

onde vê-se que surgiu uma nova violação de P2, envolvendo


14.3. INSERÇÃO DE ITENS 423

os nodos de chaves 5, 6 e 11, que deve ser restabelecida por


uma transformação DD, produzindo:

• Inserção do item com chave 0:

gera uma violação de P2, que é resolvida por uma trans-


formação EE seguida da mudança de orientação do apontador
esquerdo do nodo de chave 3, produzindo:

a qual demanda uma transformação EE seguida da mudança


de orientação do apontador esquerdo do nodo de chave 9 para
424 CAPÍTULO 14. ÁRVORES SBB

produzir a árvore final:

14.4 Implementação da inserção

A inserção de um novo item na árvore SBB é realizada pela operação


insira, que encapsula seus detalhes de implementação no método
privado homônino, conforme mostram as listagens 14.11 e 14.12.

1 public void insira(Item x) {


2 if (raiz == null) {
3 raiz = new Nodo(x);
4 } else insira(x, null, false, raiz);
5 }

Listagem 14.11 Operação [Link]

Os parâmetros do método privado insira(x,pai,d,p) são:


• x, o item a ser inserido;
• pai, o endereço do nodo pai do nodo p;
• d, a localização da subárvore de raiz p em relação ao pai, i.e.,
se d for true, p é filho direito de pai, senão, é seu filho esquerdo;
• p, o nodo sendo visitado em busca do local de inserção.

O método insira inicia-se com uma busca em árvore binária do


ponto de inserção, o qual é dado pelo nodo apontado por pai cujo
filho p seja nulo no processo de busca, pois toda inserção deve ser
14.4. IMPLEMENTAÇÃO DA INSERÇÃO 425

1 private void insira(Item x, Nodo pai, boolean d, Nodo p) {


2 if (p == null) insiraFilho(x,pai,d);
3 else {
4 int c = [Link]([Link]);
5 if (c > 0) {
6 insira(x,p,false,[Link]);
7 if (!fim) p = bayerEEED(pai,d,p);
8 } else if (c < 0) {
9 insira(x,p,true,[Link]);
10 if (!fim) p = bayerDEDD(pai,d,p);
11 } else fim = true; // já está na árvore
12 }
13 return;
14 }

Listagem 14.12 Método Homônimo de [Link]

feita em uma folha. A inserção é realizada conforme descrito na


Listagem 14.13.

1 private void insiraFilho(Item x, Nodo pai, boolean d) {


2 Nodo p = new Nodo(x);
3 if (d) {
4 [Link] = p; [Link] = Nodo.H;
5 } else {
6 [Link] = p; [Link] = Nodo.H;
7 }
8 fim = false;
9 }

Listagem 14.13 Instalação de Novo Nodo

Após a inserção do novo nodo, insira verifica se houve violação


das propriedades, desde o ponto de inserção até a raiz da árvore e
as resolve via as transformações das listagens 14.14 e 14.15.
426 CAPÍTULO 14. ÁRVORES SBB

1 private Nodo bayerEEED(Nodo pai, boolean d, Nodo p) {


2 boolean trans = false;
3 if ([Link] == Nodo.H) {
4 if ([Link] == Nodo.H) {
5 p = ee(pai,d,p); trans = true;
6 } else if ([Link] == Nodo.H) {
7 p = ed(pai,d,p); trans = true;
8 };
9 if ((pai != null) && trans) {
10 if (d) [Link] = Nodo.H; else [Link] = Nodo.H;
11 }
12 };
13 return p;
14 }

Listagem 14.14 Tranformações EE e ED

1 private Nodo bayerDEDD(Nodo pai, boolean d, Nodo p) {


2 boolean trans = false;
3 if ([Link] == Nodo.H) {
4 if ([Link] == Nodo.H) {
5 p = dd(pai,d,p);
6 trans = true;
7 } else if ([Link] == Nodo.H) {
8 p = de(pai,d,p);
9 trans = true;
10 }
11 if ((pai != null) && trans) {
12 if (d) [Link] = Nodo.H; else [Link] = Nodo.H;
13 }
14 };
15 return p;
16 }

Listagem 14.15 Tranformações DE e DD


14.5. REMOÇÃO DE ITENS 427

14.5 Remoção de itens

A operação de remoção de um nodo contendo o item com a chave


dada é executada segundo as seguintes etapas:
• Localização na árvore SBB do nodo a ser retirado, segundo
uma busca em árvores binárias de pesquisa.
• Se o nodo encontrado tiver no máximo um filho, proceder com
sua retirada, atribuindo ao seu pai a “guarda”desse filho.
• Por outro lado, se o nodo tiver dois filhos, efetua-se a troca de
sua posição com a de seu antecessor, e, então pode-se retirá-lo
pelo processo do item anterior.
• Se, após a remoção do nodo, a propriedade P1, que trata dos
número de apontadores verticais, tiver sido violada, deve-se
restabelecê-la pelo método esqCurto ou por dirCurto, ao longo
do caminho desde o ponto do nodo retirado até a raiz da árvore.

No caso de subárvore esquerda curta, restabelece-se P1 via um


dos métodos esqCurtoIeII, esqCurtoIII ou esqCurtoIV, conforme
mostra a Listagem 14.16.

1 private Nodo esqCurto(Nodo pai, boolean d, Nodo p) {


2 if ([Link] == Nodo.H)
3 p = esqCurtoIeII(pai,d,p);
4 else if ([Link] == Nodo.V)
5 p = esqCurtoIII(pai,d,p);
6 else p = esqCurtoIV(pai,d,p);
7 if (pai != null) {
8 if (d) [Link] = p; else [Link] = p;
9 } else raiz = p;
10 return p;
11 }

Listagem 14.16 Restauração de Subárvore Esquerda Curta


428 CAPÍTULO 14. ÁRVORES SBB

E no caso de subárvore com seu lado direito mais curto que


o esquerdo, restabelece-se P1 via um dos métodos dirCurtoIeII,
dirCurtoII ou dirCurtoIV, conforme detalha a Listagem 14.17.

1 private Nodo dirCurto(Nodo pai, boolean d, Nodo p) {


2 Nodo p1;
3 if ([Link] == Nodo.H)
4 p = dirCurtoIeII(pai,d,p);
5 else if ([Link] == Nodo.V)
6 p = dirCurtoIII(pai,d,p);
7 else p = dirCurtoIV(pai,d,p);
8 if (pai != null) {
9 if (d) [Link] = p; else [Link] = p;
10 } else raiz = p;
11 return p;
12 }

Listagem 14.17 Restauração de Subárvore Direita Curta

Restauração de esquerdo curto - Caso I e II


Dada uma configuração semelhante a uma das ilustradas abaixo
após a remoção de um nodo:

(caso I) (caso II)


Restaura-se P1 com a mudança de orientação do apontador
esquerdo do nodo raiz da árvore que tem o esquerdo curto, no caso
M, via o método esqCurtoIeII da Listagem 14.18:
14.5. REMOÇÃO DE ITENS 429

(caso I) (caso II)

1 private Nodo esqCurtoIeII(Nodo pai, boolean d, Nodo p)


2 [Link] = Nodo.V;
3 fim = true;
4 return p;
5

Listagem 14.18 Restauração de Esquerdo Curto I ou II

Restauração de esquerdo curto - Caso III


A remoção de um nodo pode gerar uma subárvore esquerda mais
curta que à sua irmã à direita em que o apontador desta subárvore
é vertical, como na figura abaixo:

Restaura-se P1 com a mudança de orientação do apontador di-


reito da subárvore mais longo, no caso, M, via o método esqCurtoIII
da Listagem 14.19:
430 CAPÍTULO 14. ÁRVORES SBB

Entretanto, dependendo da orientação dos apontadores das sub-


árvores do nodo-raiz da subárvore direita de M, pode ocorrer vio-
lação da propriedade P2, requerendo a aplicação de uma trans-
formação de Bayer Direita-Esquerda ou Direita-Direita.
E a seguir, ainda haver violação de P2 pendente de resolução
se o apontador para o nodo M for horizontal. Essa verificação de
pendências deve ser feita pela operação retire em seu caminho de
volta à raiz.
1 private Nodo esqCurtoIII(Nodo pai, boolean d, Nodo p) {
2 [Link] = Nodo.H;
3 if ([Link] == Nodo.H) {
4 p = de(pai,d,p);
5 fim = true;
6 } else {
7 if ([Link] == Nodo.H) {
8 p = dd(pai,d,p);
9 fim = true;
10 }
11 }
12 return p;
13 }

Listagem 14.19 Restauração de Esquerdo Curto III


14.5. REMOÇÃO DE ITENS 431

Restauração de esquerdo curto - Caso IV

Este caso requer um processo de restruturação mais elaborado,


porque o lado esquerdo mais curto é um filho vertical, enquanto
o lado direito correspondente é um filho horizontal, e assim a mu-
dança de orientação de apontadores dos casos anteriores não resolve
o problema, como depreende-se da figura:

A solução é alongar a subárvore mais curta, tornando o nodo


pai da subárvore mais curta filho da raiz da subárvore irmã à di-
reita, conforme descreve o método esqCurtoIV da Listagem 14.20 e
exemplificado pela seguinte figura:

Esses rearranjos na estrutura da árvore podem provocar a ne-


cessidade de transformações do tipo Direita-Esquerda ou Direita-
Direita para resolver as violações de propriedades por ventura ge-
radas.
432 CAPÍTULO 14. ÁRVORES SBB

1 private Nodo esqCurtoIV(Nodo pai, boolean d, Nodo p) {


2 Nodo p1;
3 p1 = [Link]; [Link] = [Link];
4 [Link] = p;
5 if (pai != null) {
6 if (p == [Link]) [Link] = p1; else [Link] = p1;
7 } else raiz = p1;
8 p = p1;
9 if ([Link] == Nodo.H) {
10 [Link] = de(pai,d,[Link]);
11 [Link] = Nodo.V;
12 } else if ([Link] == Nodo.H) {
13 [Link] = dd(pai,d,[Link]);
14 [Link] = Nodo.V;
15 }
16 fim = true;
17 return p;
18 }

Listagem 14.20 Restauração de Esquerdo Curto IV

Restauração de direito curto - Caso I e II


Dada uma configuração semelhante a uma das ilustradas abaixo
após a remoção de um nodo:

(caso I) (caso II)


Restaura-se P1 com a mudança de orientação do apontador di-
reito do nodo raiz da árvore que tem o direito curto, no caso M, via
14.5. REMOÇÃO DE ITENS 433

execução do método dirCurtoIeII da Listagem 14.21, produzindo:

(caso I) (caso II)

1 private Nodo dirCurtoIeII(Nodo pai, boolean d, Nodo p) {


2 [Link] = Nodo.V;
3 fim = true;
4 return p;
5 }

Listagem 14.21 Restauração de Esquerdo Curto I e II

Restauração de direito curto - Caso III


Neste caso a remoção de um nodo em uma subárvore direita gera
uma situação de direito curto, na figura abaixo.
434 CAPÍTULO 14. ÁRVORES SBB

que é resolvida com a mudança de orientação do apontador es-


querdo de M, via o método dirCurtoIII da Listagem 14.22:

1 private Nodo dirCurtoIII(Nodo pai, boolean d, Nodo p) {


2 [Link] = Nodo.H;
3 if ([Link] == Nodo.H) {
4 p = ee(pai,d,p);
5 fim = true;
6 } else {
7 if ([Link] == Nodo.H) {
8 p = ed(pai,d,p);
9 fim = true;
10 }
11 }
12 return p;
13 }

Listagem 14.22 Restauração de Esquerdo Curto III

Nesse caso, retire pode ter que realizar novas transformações


ao longo do caminho de volta à raiz da árvore.

Direito Curto - Caso IV

Alongar a subárvore direita curta requer um processo de restru-


turação mais elaborado, porque o lado direito mais curto é filho
14.5. REMOÇÃO DE ITENS 435

vertical enquanto o lado direito correspondente é filho horizontal,


e assim mudança de orientação de apontadores não é solução:

Nesse caso, deve-se alongar a subárvore mais curta, tornando o


nodo pai da subárvore mais curta filho da raiz da subárvore irmã
à esquerda, conforme exemplificado pela seguinte figura:

Essa transformação é realizada pelo método dirCurtoIV, deta-


lhado na Listagem 14.23, que prevê a realização de outras trans-
formações do tipo Esquerda-Direita e Esquerda-Esquerda.

O parâmetro p passado aos métodos esqCurtoIeII, esqCurtoIII,


esqCurtoIV, dirCurtoIeII, dirCurtoIII e dirCurtoIV é sempre o en-
dereço do nodo-raiz da árvore que tem uma subárvore mais curta.
436 CAPÍTULO 14. ÁRVORES SBB

1 private Nodo dirCurtoIV(Nodo pai, boolean d, Nodo p) {


2 Nodo p1 = [Link];
3 [Link] = [Link];
4 [Link] = p;
5 p = p1;
6 if ([Link] == Nodo.H) {
7 [Link] = ed(p,true,[Link]);
8 [Link] = Nodo.V;
9 } else if ([Link] == Nodo.H) {
10 [Link] = ee(p,true,[Link]);
11 [Link] = Nodo.V;
12 }
13 fim = true;
14 return p;
15 }

Listagem 14.23 Restauração de Esquerdo Curto IV

14.6 Exemplo de remoções sucessivas

As diversas operações de restauração das propriedades definidoras


de árvore SBB são ilustradas pelo exemplo a seguir de remoção de
nodos com as chaves 6, 7, 2, 1, 0, 8, 20, 9, 4, 3, 5, 10 e 11, nesta
ordem, da seguinte árvore:

Os detalhes execução das operacões de remoção dos itens com


as chaves acima e das transformações que se fizerem necessárias
14.6. EXEMPLO DE REMOÇÕES SUCESSIVAS 437

são os seguintes:
• Remoção do item de chave 6:

O nodo com o item de chave 6 é uma folha horizontal, e as-


sim pode ser simplesmente removido e a operação se encerra
imediatamente, produzindo a árvore:

• Remoção do item de chave 7:

A remoção desse item também é direta e não demanda outras


transformações, porque nenhuma propriedade foi violada com
438 CAPÍTULO 14. ÁRVORES SBB

sua remoção:

• Remoção do item de chave 2:

A remoção do nodo de chave 2 gera a seguinte situação de di-


reito curto:

que requer uma transformação do tipo Direito Curto III, a


qual, por sua vez, gera o seguinte caso de esquerdo curto:
14.6. EXEMPLO DE REMOÇÕES SUCESSIVAS 439

que, por uma nova transformação tipo Esquerdo Curto III,


também provoca um novo caso de esquerdo curto:

Por fim, uma transformação do tipo Esquerdo Curto II res-


taura as propriedades da árvore e conclui a remoção do item
de chave 2:
440 CAPÍTULO 14. ÁRVORES SBB

• Remoção do item de chave 1:

Remoção de 1 pode ser concluı́da sem necessidade de trans-


formações adicionais:

• Remoção do item de chave 0:

gera o seguinte caso de esquerdo curto:


14.6. EXEMPLO DE REMOÇÕES SUCESSIVAS 441

Nesse caso, uma transformação do tipo Esquerdo Curto IV,


restaura a árvore e conclui a remoção do item de chave 0,
produzindo:

• Remoção do item de chave 8:

causa o aparecimento de direito curto mostrado a seguir:


442 CAPÍTULO 14. ÁRVORES SBB

que é resolvido pela transformação do tipo Direito Curto III,


que, por sua vez, gera uma violação de P2 tipo direito-esquerdo:

que a após ser resolvida pela transformação DE de Bayer


conclui-se a remoção do item de chave 8 produzindo:
14.6. EXEMPLO DE REMOÇÕES SUCESSIVAS 443

• Remoção do item de chave 20:

Causa o aparecimento de direito curto:

que é resolvido por uma transformação do tipo Direito Curto


III, a qial gera um novo caso de direito curto:

o qual é resolvido por uma transformação Direito Curto III,


444 CAPÍTULO 14. ÁRVORES SBB

que conclui a remoção do item de chave 20, produzindo:

• Remoção do nodo 9:

Nodo com chave 9 não pode ser removido, porque tem dois
filhos, sendo necessário trocar o item 9 de lugar com seu ante-
cessor 5, produzindo:

Agora nodo com item de chave 9 pode ser removido, gerando


um caso de direito curto:
14.6. EXEMPLO DE REMOÇÕES SUCESSIVAS 445

que é resolvido pela transformação do tipo Direito Curto III,


a qual gera um caso de esquerdo curto:

que é resolvido pela transformação do tipo Esquerdo Curto II,


finalizando a remoção do item de chave 9:
446 CAPÍTULO 14. ÁRVORES SBB

• Remoção do item de chave 4:

produz a árvore:

• Remoção do item de chave 3:

gera o seguinte caso de esquerdo curto:

que, por uma transformação do tipo Esquerdo Curto III, pro-


14.6. EXEMPLO DE REMOÇÕES SUCESSIVAS 447

duz a árvore:

Finalmente, uma transformação DE para resolve a violação de


P2 gerada e conclui a remoção do item de chave 3:

• Remoção do item de chave 5:

gera um caso de esquerdo curto:

que por uma transformação do tipo III, conclui a remoção do


item de chave 5:
448 CAPÍTULO 14. ÁRVORES SBB

• Remoção do item de chave 10:

produz a árvore:

• Remoção do item de chave 11:

Finaliza o exemplo:

14.7 Implementação de remoção

A remoção de um item na árvore SBB a partir de sua chave é


realizada pela operação retire apresentada na Listagem 14.24.

1 public Item retire(String chave) {


2 Item item;
3 if ((raiz == null)|| (chave == null)) return null;
4 item = remova(chave,null,false,raiz);
5 return item;
6 }

Listagem 14.24 Operação [Link]


14.7. IMPLEMENTAÇÃO DE REMOÇÃO 449

Os detalhes de implementação do algoritmo de retirada de nodos


da árvore SBB estão encapsulados no método privado remova que
está detalhado na Listagem 14.25.

1 private Item remova(String chave,Nodo pai,boolean d,Nodo p){


2 Nodo a, q, r;
3 Item item;
4 if (p == null) {fim = true; return null;}
5 if ([Link]([Link]) < 0) {
6 item = remova(chave,p,false,[Link]);
7 if (!fim) p = esqCurto(pai,d,p);
8 } else if ([Link]([Link]) > 0) {
9 item = remova(chave,p,true,[Link]);
10 if (!fim) p = dirCurto(pai,d,p);
11 } else { // [Link] desejado foi encontrado
12 if (([Link] != null) && ([Link] != null)) {
13 q = p; r = [Link];
14 while ([Link] != null) {
15 q = r; r = [Link];
16 }
17 item = [Link];
18 [Link] = [Link];
19 [Link] = item;
20 item = remova(chave,p,false,[Link]);
21 if (!fim) p = esqCurto(pai,d,p);
22 } else item = retireNodo(pai,d,p);
23 }
24 return item;
25 }

Listagem 14.25 Método para Busca e Retirada de um Nodo

O método remova tem a clássica estrutura usada para busca em


árvore binária de pesquisa, porque seu primeiro ato é o de localizar
o nodo a ser retirado, o qual deve sempre ser um nodo com no
máximo um filho.
450 CAPÍTULO 14. ÁRVORES SBB

Caso o nodo com o item a ser retirado tenha dois filhos, o al-
goritmo efetua a troca do seu item com o de seu antecessor, que,
por definição, é sempre um nodo com no máximo um filho. Após
a troca, o nodo que tinha o antecessor pode então ser retirado pelo
método privado retireNodo da Listagem 14.26.

1 private Item retireNodo(Nodo pai, boolean d, Nodo p) {


2 Item item = [Link];
3 fim = true;
4 if ([Link] == null) {
5 if (pai != null) {
6 if (d) [Link] = [Link]; else [Link] = [Link];
7 } else raiz = [Link];
8 if ([Link] == null) fim = false;
9 } else if ([Link] == null) {
10 if (pai != null) {
11 if (d) [Link] = [Link]; else [Link] = [Link];
12 } else raiz = [Link];
13 if ([Link] == null) fim = false;
14 }
15 return item;
16 }

Listagem 14.26 Retirada de Nodo com no Máximo um Filho

E após a retirada do nodo desejado, o método remova inicia o


processo de retorno das chamadas recursivas realizadas durante a
busca do nodo com a chave dada, e, a cada retorno, verifica a
ocorrência de Esquerdo Curto ou Direito Curto, via os métodos
esqCurto e dirCurto já apresentados, para manter válidas as pro-
priedades da árvore Sbb.
14.8. ANÁLISE DE COMPLEXIDADE DAS OPERAÇÕES 451

14.8 Análise de complexidade das operações

Em árvores SBB, como já mencionado, é necessário distinguir dois


tipos de alturas: altura vertical h, que é número de apontadores
verticais no caminho entre a raiz e um nodo externo, e a altura
real k, que é número total de apontadores no maior caminho entre
a raiz e um nodo externo.
A altura vertical serve ao propósito de definir as propriedades
da árvore, mas do ponto de vista de análise da complexidade das
operações o que interessa é a sua altura real.
Os algoritmos de pesquisa, inserção e remoção sempre percorrem
a árvore de sua raiz até uma folha, seguindo um caminho contı́nuo
de descida, tendo, portanto, seu custo proporcional à altura real
da árvore, a qual é delimitada por h ≤ k ≤ 2h.
Os custos para localizar itens na árvore e o de restaurar propri-
edades da SBB são proporcionais ao caminhamento de ida e volta
da raiz até a folha incluı́da ou retirada, sendo, portanto, O(lg n).

Teorema 14.1 (Bayer (1972)): A altura real k de uma árvore


SBB com n nodos internos sempre satisfaz a:

log(n + 1) ≤ k ≤ 2 log(n + 2) − 2

Observa-se também que número de comparações em uma pes-


quisa com sucesso na árvore SBB é no:
• melhor caso : C(n) = O(1)
• pior caso : C(n) = O(lg n)
• caso médio : C(n) = O(lg n)

Pode-se mostrar que na prática o caso médio para C(n) é ape-


nas cerca de 2% pior que o C(n) para uma árvore completamente
balanceada [16].
452 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais

14.9 Conclusão

Árvores SBB são um tipo de estrutura de dados que pode per-


mitir recuperação muito eficiente de informação armazenada em
memória secundária com custos proporcionais ao logaritmo do
número de itens.

Exercı́cios
1. Como os algoritmos de inserção, pesquisa e remoção de uma
SBB devem ser modificados para aceitar diferentes itens com
mesma chave?

Notas Bibliográficas

Textos sobre SBB são os livros Projeto de Algoritmos de Nivio


Ziviani [15] e Algorithms de Roberto Sedgwidk [19], entre outros.
***
Capı́tulo 15

Árvores Rubro-Negras

La lecture de tous les bons livres est comme


une conversation avec les plus honnêtes gens des
siècles passés, qui en ont été les auteurs, et même
une conversation étudiée en laquelle ils ne nous
découvrent que les meilleures de leurs pensées.1
René Descartes (1596-1650)

Árvores binárias são estruturas de dados que permitem imple-


mentar dicionários em que os custos de suas operações são propor-
cionais à sua altura. No caso de árvores perfeitamente balanceadas,
a altura é lg n, mas, nas desbalanceadas, a altura pode chegar, no
pior caso, a n, onde n é o número de nodos.
De fato, árvores binárias de pesquisa perfeitamente balancea-
das solucionam esse problema garantindo sempre um custo ótimo
O(lg n) nas operações de pesquisa, inserção ou remoção de itens,
mas, em geral, demandam um esforço computacional adicional
para manter esse nı́vel de desempenho quando a estrutura da árvore
for alterada por operações de inserção e remoção.
Árvores AVL são árvores binárias de pesquisa que, embora não
1
A leitura de bons livros é como um diálogo com as pessoas mais bem qualificadas dos séculos passados,
isto é, seus autores, e também uma conversação de alto nı́vel, na qual eles nos revelam seus melhores
pensamentos.

453
454 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

apresentem o custo ótimo das perfeitamente balanceadas para suas


operações, permitem que o rebalanceamento requerido após in-
serções e remoções seja comparativamente menos custoso.
Uma outra solução para reduzir os custos de rebalanceamento de
árvores binárias de pesquisa, enquanto garante-se um desempenho
satisfatório para suas operações, são as Árvores Binárias Simétricas
ou SBB.
E uma variante das SBBs são as Árvores Rubro-Negras (ARN),
nas quais, como nas SBBs, a altura é no máximo 2 lg(n + 1), onde
n é o número de nodos internos.

Figura 15.1 Uma Árvore Rubro-Negra

Definição 15.1 Árvore Rubro-Negra é uma árvore binária


de pesquisa estendida em que seus nodos possuem um bit extra
para definir sua cor, a qual pode ser Vermelha ou Preta, e que
obedece sempre às seguintes propriedades:
P1: todo nodo interno ou é vermelho ou é preto
P2: toda folha é da cor preta
P3: os filhos de nodo vermelho são necessariamente pretos
455

P4: todos os caminhos da raiz até cada nodo folha possuem


o mesmo número de nodos pretos.

Figura 15.1 ilustra o aspecto gráfico de uma árvore rubro-negra,


na qual as folhas, que são nodos externos, estão representadas por
nodos retangulares.
Alternativamente, uma árvore rubro-negra é apresentada sem
seus nodos externos, mas com a interpretação de que as folhas
omitidas são todas de cor preta, como ilustra a Figura 15.2.

Figura 15.2 Uma Árvore Rubro-Negra com Folhas Omitidas

Árvores rubro-negras têm duas alturas: altura real, que é o com-


primento do maior caminho da raiz até uma folha descendente, con-
tando todos os nodos desse caminho, e altura preta, que considera
nessa contagem apenas os nodos de cor preta.
As propriedades P3 e P4 das árvores rubro-negras asseguram
a validade do seguinte lema:
Lema 15.1 A altura real de uma árvore rubro-negra com n
nodos internos é no máximo 2 lg(n + 1).
456 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

Esse lema garante que as operações tı́picas da árvore rubro-negra


podem ser executadas em tempo O(lg n).
O tipo abstrato de dados Rn apresentado na Listagem 15.3 im-
plementa uma árvore rubro-negra, a qual é caracterizada pelas
operações tradicionais de um dicionário: pequise, insira e retire.

1 public class Rn {
2 private Nodo raiz;
3 public Rn() { }
4 public Nodo pesquise() {...}
5 public void insira(Item item) {...}
6 public Nodo retire(String chave) {...}
7 "Métodos Auxiliares de Condiç~
oes de Contorno";
8 "Métodos Auxiliares de Consertos da Árvore";
9 }

Listagem 15.3 Tipo Abstrato Rn

Os itens armazenados nos nodos dessa árvore são objetos do tipo


Item definido na Listagem 15.4.

1 public class Item {


2 public String chave;
3 public Item(String chave) {[Link] = chave;}
4 public String valor() {return "?";}
5 }

Listagem 15.4 Item de Nodos de Árvore Rubro-Negra

Cada nodo da árvore é um objeto do tipo Nodo, definido na


Listagem 15.5, onde V e P denotam as cores vermelha e preta,
armazenáveis no campo cor, e pai aponta para o nodo pai do
respectivo nodo ou tem valor null no caso de pai do nodo raiz da
árvore. O apontador pai elimina a necessidade de uso de uma pilha
para permitir o caminhamento na árvore de baixo para cima, o qual
457

1 class Nodo {
2 final static byte V = 1, P = 2;
3 Item item;
4 Nodo pai, esq, dir;
5 byte cor;
6 Nodo (Item item, Nodo pai, byte cor) {
7 [Link] = item; [Link] = pai; [Link] = cor;
8 }
9 }

Listagem 15.5 Nodo de Árvore Rubro-Negra

é requerido nas operações de conserto da árvore após inserções e


remoções.
A representação descrita na Listagem 15.5 não prevê a presença
fı́sica de nodos folhas, que são representados por apontadores nulos.
Isso facilita a representação da estrutura, mas cria condições de
contorno que devem ser tratadas caso a caso. Assim, para unificar
seu tratamento, sem prejudicar a representação dos nodos, folhas
são introduzidas virtualmente na visão da estrutura por meio dos
métodos apresentados na Listagem 15.6, que revela o refinamento
de "Métodos Auxiliares de Condições de Contorno".
1 private byte cor(Nodo q) {...}
2 private byte corEsq(Nodo q) {...}
3 private byte corDir(Nodo q) {...}
4 private Nodo esq(Nodo q) {...}
5 private Nodo dir(Nodo q) {...}
6 private boolean filhoEsq(Nodo x) {...}

Listagem 15.6 Métodos Auxiliares de Condições de Contorno

O método cor da Listagem 15.7 encapsula o fato de os nodos


folhas terem sido substituı́dos por null, mas ainda serem vistos
como apontadores para nodos de cor preta, simulando que todo
458 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

caminho descendente na árvore termina com um nodo folha.


1 private byte cor(Nodo q) {
2 if (q == null) return Nodo.P;
3 else return [Link];
4 }

Listagem 15.7 Cor de um Nodo

Similarmente, os apontadores esquerdo e direito virtuais de no-


dos folhas são interpretados como sendo da cor preta, conforme
Listagem 15.8.

1 private byte corEsq(Nodo q) {


2 if ((q == null) || ([Link] == null)) return Nodo.P;
3 else return [Link];
4 }
5 private byte corDir(Nodo q) {
6 if ((q == null) || ([Link] == null)) return Nodo.P;
7 else return [Link];
8 }

Listagem 15.8 Tratamento de Condições de Contorno

1 private Nodo esq(Nodo q) {


2 if ((q == null) || ([Link] == null))
3 return new Nodo(new Item("_"), q, Nodo.P);
4 return [Link];
5 }
6 private Nodo dir(Nodo q) {
7 if ((q == null) || ([Link] == null))
8 return new Nodo(new Item("_"), q, Nodo.P);
9 return [Link];
10 }

Listagem 15.9 Acesso a Subárvores de um Nodo


459

A materialização de nodos folhas virtuais é produzida pelos


métodos esq e dir, que associam um nodo especial a apontado-
res nulos que são representativos de subárvores que são folhas,
conforme detalhado na Listagem 15.9.
Nodos folhas virtuais criados pelos métodos esq ou dir acima
contém o apontador para seu nodo pai, mas, para manter o modelo
despoluı́do, seus nodos pais não armazenam o endereço de seus
eventuais nodos folhas virtuais.
Entretanto, essa informação pode ser recuperada via o método
filhoEsq da Listagem 15.10, onde supõe-se que o nodo raiz é subárvore
esquerda de um nodo virtual e que apontador de subárvore nulo
denota apontador para folha.

1 private boolean filhoEsq(Nodo x) {


2 Nodo p = [Link];
3 if (p == null) return true; // x é filho esquerdo
4 if (x == [Link]) return true; // x é filho esquerdo
5 if (x == [Link]) return false; // x é filho direito
6 if ([Link] == null) return true; // x é filho esquerdo
7 return false; // x é filho direito
8 }

Listagem 15.10 Tratamento de Condições de Contorno

O uso dos métodos cor, corEsq, corDir, esq, dir e filhoEsq per-
mite que árvore implementada pela classe Rn seja vista e tratada
como uma subárvore estendida com todas as suas folhas presentes,
facilitando o tratamento das condições de contorno.
O refinamento de "Métodos Auxiliares de Consertos da Árvore",
listado na Figura 15.3, apresenta os métodos que são necessários
para restaurar propriedades da árvore após operações de inserção
ou remoção de nodos. Os cabeçalhos desses métodos estão anun-
ciados na Listagem 15.11 e são detalhados nas próximas seções.
460 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

1 private void gireParaEsquerda(Nodo p) {...}


2 private void gireParaDireita(Nodo p) {...}
3 private void restaureApósInserç~ao(Nodo p) {...}
4 private Nodo conserteFilhoEsquerdoIns(Nodo p) {...}
5 private Nodo conserteFilhoDireitoIns(Nodo p) {...}
6 private void restaureApósInserç~ao(Nodo p) {...}
7 private Nodo conserteFilhoEsquerdoRem(Nodo p) {...}
8 private Nodo conserteFilhoDireitoRem(Nodo p) {...}
9 private void ao(Nodo p) {...}
restaureApósRemoç~

Listagem 15.11 Métodos Auxiliares de Rn

15.1 Pesquisa por itens

A operação pesquise, definida na Listagem 15.12, em nada difere


de uma pesquisa em árvore binária de pesquisa, na qual itens com
chaves menores que a do item do nodo-raiz de qualquer subárvore
são armazenados na subárvore esquerda desse nodo-raiz, e os com
chaves maiores, na da direita.

1 public Item pesquise(String chave) {


2 Nodo p;
3 if (chave == null) return null;
4 p = raiz;
5 while (p != null) {
6 if ([Link]([Link]) < 0)
7 p = [Link];
8 else if ([Link]([Link]) > 0)
9 p = [Link];
10 else return [Link];
11 }
12 return null;
13 }

Listagem 15.12 Operação pesquise


15.2. ROTAÇÃO DE NODOS 461

O custo da operação de pesquisa em árvore rubro-negra é pro-


porcional à altura real da árvore, que, conforme o Lema 15.1, é no
máximo 2 lg(n + 1), onde n é o número de nodos.

15.2 Rotação de nodos

As operações de inserção e remoção podem requerer recoloração


de nodos, e rotações de nodos podem também ser necessárias de
forma a consertar possı́veis violações de propriedades da árvore.
Rotações são transformações locais que alteram apenas um pe-
queno número de nodos. Elas podem ser Rotação à Esquerda ou
Rotação à Direita, conforme determinado pelo tipo da violação
identificada.
Na rotação à esquerda, parte-se de uma configuração como a
do lado esquerdo da Figura 15.13, onde x indica o nodo pivô da
rotação, a partir do qual determina-se o nodo f, filho de x, envolvido
na operação.

Figura 15.13 Rotação à Esquerda com Pivô B

Essa rotação, implementada pelo método gireParaEsquerda da


Listagem 15.14, faz o nodo x adotar a subárvore β e também o
torna filho esquerdo do nodo f, o qual assume o topo da hierarquia
local, como mostra a configuração do lado direito da Figura 15.13.
462 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

Observe que essa rotação aumenta a altura da subárvore es-


querda dessa hierarquia ao mesmo tempo que reduz a altura da
respectiva subárvore direita, ambas em uma unidade. Isso sig-
nifica que B tem sua altura de preto equilibrada, mas a de D na
Figura 15.13 é uma unidade menor que a de seu irmão.
1 private void gireParaEsquerda(Nodo x) {
2 Nodo f = [Link];
3 [Link] = [Link];
4 if ([Link] != null) [Link] = p;
5 [Link] = [Link];
6 if ([Link] == null) raiz = f;
7 else {
8 if (x == [Link]) [Link] = f;
9 else [Link] = f;
10 }
11 [Link] = x; [Link] = f;
12 return;
13 }

Listagem 15.14 Rotação para a Esquerda (Fonte Cormen[22])

Figura 15.15 Rotação à Direita com Pivô D

Na rotação à direita, ilustrada pela Figura 15.15, parte-se de


uma configuração, onde x indica o nodo pivô da transformação,
a partir do qual determina-se o nodo f, seu filho, envolvido na
15.3. INSERÇÃO DE ITENS 463

operação.
Essa rotação faz o nodo x adotar o filho β de f e também o torna
o filho direito do nodo f, o qual assume o topo da hierarquia local.
Observe que essa rotação aumenta a altura preta da subárvore
direita dessa hierarquia ao mesmo tempo que reduz a altura preta
da respectiva subárvore esquerda, ambas em uma unidade.

1 private void gireParaDireita(Nodo x) {


2 Nodo f = [Link];
3 [Link] = [Link];
4 if ([Link] != null) [Link] = x;
5 [Link] = [Link];
6 if ([Link] == null) raiz = f;
7 else {
8 if (x == [Link]) [Link] = f;
9 else [Link] = f;
10 }
11 [Link] = x; [Link] = f;
12 return;
13 }

Listagem 15.16 Rotação para a Direita (Fonte Cormen[22])

15.3 Inserção de itens

O algoritmo de inserção da Listagem 15.17 tem a mesma estrutura


do de inserção em árvore binária de pesquisa, exceto que, após a
inserção de um novo nodo, consertos na árvore, relativos à reco-
loração e/ou restruturação de nodos, podem ter que ser feitos para
restaurar as propriedades que por ventura tiverem sido violadas.
Novos nodos são sempre inseridos na cor vermelha para evitar
alterar a altura preta da árvore, mas, se, logo após inserção de
um nodo x vermelho, a cor de [Link] também for vermelha, então
464 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

1 public void insira(Item item) {


2 Nodo a, pai, p, x;
3 if (item == null || [Link] == null) return;
4 pai = null;
5 p = raiz;
6 while (p != null) {
7 if ([Link]([Link]) < 0)
8 pai = p; p = [Link];
9 else if ([Link]([Link]) > 0)
10 pai = p; p = [Link];
11 else return; // ignora inserç~
ao
12 }
13 if (pai == null) {
14 raiz = new Nodo(item,pai,Nodo.P);
15 return true;
16 }
17 x = new Nodo(item,pai,Nodo.V);
18 if ([Link]([Link]) < 0)
19 [Link] = x; else [Link] = x;
20 restaureApósInserç~
ao(x);
21 return;
22 }

Listagem 15.17 Operação insira

tem-se uma violação da propriedade 3 da árvore rubro-negra, pois


filho de nodo vermelho deveria ser preto.
Essa violação deve ser resolvida por meio de recoloração e rotação
dos nodos relacionados a x, e, nesse processo, x pode passar a apon-
tar para um outro nodo no caminho hierárquico em direção à raiz
da árvore. E as propriedades definidoras da árvore devem ser re-
verificadas a cada passo nesse caminho de volta à raiz. Assim,
enquanto a cor do pai do nodo x corrente após cada transformação
for vermelha, violações podem ainda estar ocorrendo e novas trans-
formações podem ser necessárias.
15.3. INSERÇÃO DE ITENS 465

O conserto da árvore após cada inserção de nodo vermelho, con-


forme definido na Listagem 15.18, segue exatamente esses passos
para restaurar, em um caminhamento em direção à raiz, iniciado no
ponto de inserção do novo nodo, as propriedades que por ventura
tenham sido violadas.
Nesse caminhamento, x sempre aponta para o nodo cuja altura
preta pode estar em desacordo com a definição da árvore. Esse
processo termina quando o pai do nodo correntemente apontado
por x for preto, porque isso atende à regra de que nodo preto pode
ser filho de vermelho.

1 ao(Nodo x) {
private void restaureApósInserç~
2 Nodo avo, pai, tio;
3 pai = [Link];
4 while ((pai != null) && ([Link] == Nodo.V)) {
5 avo = [Link];
6 if (pai == [Link])
7 x = conserteFilhoEsquerdoIns(x);
8 else x = conserteFilhoDireitoIns(x);
9 pai = [Link];
10 }
11 [Link] = Nodo.P;
12 }

Listagem 15.18 Método restaureApósInserç~


ao

Nas operações de conserto da árvore, decidem-se, com base nas


cores dos nodos tio e irmão do foco x, as transformações a ser
realizadas para restaurar as propriedades.
Para isso, dado o nodo foco x, deve-se considerar os contextos
estruturais, A e B, para os quais os consertos são realizados pe-
los métodos conserteFilhoEsquerdoIns e conserteFilhoDireitoins,
definidos nas listagens 15.19 e 15.20, respectivamente.
466 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

Contexto A: o pai p do nodo foco x é raiz de subárvore es-


querda, sendo assim esse contexto é caracterizado por p == [Link],
onde a = [Link], p = [Link] e t = [Link], sendo p, a e t abrevi-
aturas de pai, avô e tio do nodo apontado por x, respectivamente.
As transformações são:
1. se [Link] == V, fazer a transformação caso 1:
[Link] = P; [Link] = P; [Link] = R; x = a;
e repetir o processo a partir do inı́cio para o novo x.
2. se [Link] == P e x == [Link], fazer a transformação caso 2:
x = [Link]; gireParaEsquerda(x);
repetir o processo a partir do inı́cio para o novo x.
3. se [Link] == P e x == [Link], fazer a transformação caso 3:
[Link] = P; [Link] = R; gireParaDireita(a);
repetir o processo a partir do inı́cio para o novo x, conforme
seu novo contexto.

1 private Nodo conserteFilhoEsquerdoIns(Nodo x) {


2 Nodo p = [Link], a = [Link], t = [Link];
3 if ((t != null) && [Link] == Nodo.V) {
4 [Link] = Nodo.P; [Link] = Nodo.P; // caso 1
5 [Link] = Nodo.V; x = a; // caso 1
6 } else {
7 if (x == [Link]) {
8 x = p; gireParaEsquerda(x); // caso 2
9 } else {
10 [Link] = Nodo.P; // caso 3
11 [Link] = Nodo.V; // caso 3
12 gireParaDireita(a); // caso 3
13 }
14 }
15 return x;
16 }

Listagem 15.19 Método conserteFilhoEsquerdoIns


15.3. INSERÇÃO DE ITENS 467

Observe que o foco dos consertos dado pelo ponteiro x, na Lis-


tagem 15.19, somente muda de nı́vel no caminho de volta à raiz da
árvore, quando ocorre os casos 1 ou 2.

Contexto B: o pai p do nodo foco x é raiz de subárvore di-


reita, sendo assim esse contexto é caracterizado por p == [Link],
onde a = [Link], com p = [Link] e t = [Link], e no qual
demanda-se um processamento similar ao do contexto A, trocando-
se esq por dir, conforme mostra a Listagem 15.20.

1 private Nodo conserteFilhoDireitoIns(Nodo x) {


2 Nodo pai = [Link], avo = [Link], tio = [Link];
3 if ((tio != null) &&([Link] == Nodo.V)) {
4 [Link] = Nodo.P; [Link] = Nodo.P; // caso 1
5 [Link] = Nodo.V; x = avo; // caso 1
6 } else {
7 if (x == [Link]) {
8 x = pai; // caso 2
9 gireParaDireita(p); // caso 2
10 } else {
11 [Link] = Nodo.P; // caso 3
12 [Link] = Nodo.V; // caso 3
13 gireParaEsquerda(avo); // caso 3
14 }
15 }
16 return x;
17 }

Listagem 15.20 Método conserteFilhoDireitoIns

O custo da operação insira é proporcional ao dobro da altura


real da árvore, correspondente ao de localizar o ponto de inserção,
seguido pelos consertos que podem ter que ser feitos, i.e, o custo é
no máximo 4 lg(n + 1).
468 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

15.4 Remoção de itens

O algoritmo de remoção tem a mesma estrutura do seu corres-


pondente em árvores binárias de pesquisa, conforme mostra Lista-
gem 15.21.

1 public Item retire(String chave) {


2 Nodo a, r, x, q, p, pai;
3 Item itemRetirado = null;
4 if ((raiz == null) || (chave == null)) return null;
5 p = raiz; pai = null;
6 while (p != null) {
7 if ([Link]([Link]) < 0) {
8 pai = p; p = [Link];
9 } else if ([Link]([Link]) > 0) {
10 pai = p; p = [Link];
11 } else break;
12 }
13 if (p == null) return null;
14 itemRetirado = [Link];
15 if (([Link] != null) && ([Link] != null)) {
16 q = p; r = [Link];
17 while (r != null) {
18 pai = p; p = r; r = [Link];
19 }
20 [Link] = [Link]; [Link] = itemRetirado;
21 }
22 "Efetua a retirada do nodo p e define foco x";
23 if ((x != null) && ([Link] == Nodo.P)) {
24 restaureApósRemoç~
ao(x);
25 }
26 return itemRetirado;
27 }

Listagem 15.21 Operação [Link]

A diferença está na necessidade consertos na árvore rubro-negra,


15.4. REMOÇÃO DE ITENS 469

envolvendo recoloração e/ou rotação de nodos, para restaurar pro-


priedades que por ventura tenham sido violadas.
Observe que o método retire da Listagem 15.21, nas linhas 1 a
12, localiza o nodo p candidato a ser removido junto com o item
desejado, e que, conforme indicado nas linhas 13 a 22, se o nodo
que contém o item a ser retirado tiver dois filhos, ele não pode
ser removido, e, assim, para a remoção do item desejado, deve-
se transferir para o nodo p as informações contidas em seu nodo
antecessor, e p passa a apontar para esse nodo antecessor, que por
definição tem no máximo um filho, e, assim, pode ser removido. A
partir desse ponto, retire procede-se com as ações apresentadas em
"Efetua a retirada do nodo p e define foco x" da Listagem 15.22.

1 if (p == raiz) {
2 if ([Link] != null) {
3 raiz = [Link]; [Link] = null;
4 } else if ([Link] != null) {
5 raiz = [Link]; [Link] = null;
6 } else raiz = null;
7 x = null;
8 } else {
9 if ([Link] != null) x = [Link]; else x = [Link];
10 if (p == [Link]) [Link] = x; else [Link] = x;
11 if (x != null) [Link] = pai;
12 }
13 if (x == null) x = new Nodo(new Item("_"),pai,Nodo.P);

Listagem 15.22 Remoção do nodo p e definição do foco x

Essas ações identificam o nodo x a partir do qual deve-se pro-


mover os devidos consertos da árvore, caso isso seja necessário. O
nodo x assim computado denota um dos filhos do nodo p removido
ou, caso este não tenha filhos, um nodo folha criado como se fosse
um filho virtual de p, o nodo retirado.
470 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

Se p for um nodo vermelho, sua remoção não afeta a altura preta


de x, e a remoção é finalizada. Entretanto, na retirada de nodo de
cor preta, consertos na árvore precisam ser realizados, pois essa
remoção reduz a altura preta dos caminhos que passavam por x.
Para isso, x, chamado de foco de transformações, deve ser passado
ao método restaureApósRemoç~ao, na linha 25, para dar inı́cio a esses
consertos, os quais buscam, no caminho desde x até a raiz da árvore,
um nodo vermelho que possa ser recolorido para preto.
Essa busca pelo nodo a ser recolorido para a cor preta é reali-
zada pelo método restaureApósRemoç~ao da Listagem 15.23, o qual,
a partir do apontador x, que lhe é passado como parâmetro, verifica
o respeito às propriedades da árvore no caminho de x em direção
à raiz da árvore, fazendo-se consertos à esquerda ou à direita con-
forme x seja subárvore esquerda ou direita, sempre considerando
que x é a raiz de uma subárvore com a altura preta reduzida de
uma unidade em relação a de seu irmão.

1 ao(Nodo x) {
private void restaureApósRemoç~
2 while ((x != null) && (x != raiz) && ([Link] == Nodo.P)){
3 if (filhoEsq(x))
4 x = conserteFilhoEsquerdoRem(x);
5 else x = conserteFilhoDireitoRem(x);
6 };
7 if (x != null) [Link] = Nodo.P;
8 }

Listagem 15.23 Conserto da árvore após remoção

Os consertos realizados pelos métodos conserteFilhoEsquerdoRem


e conserteFilhoDireitoRem atualizam o foco x movendo-o ao longo
do caminho de retorno à raiz da árvore. E esse processo de conser-
tos somente termina quando for encontrado um nodo vermelho x,
que possa ser recolorido para preto, ou quando a raiz for atingida.
15.4. REMOÇÃO DE ITENS 471

Conserto no filho esquerdo

As operações realizadas pelo método conserteFilhoEsquerdoRem da


Listagem 15.24 são acionadas quando o foco de transformações x
for uma subárvore esquerda.

1 private Nodo conserteFilhoEsquerdoRem(Nodo x) {


2 Nodo irmao = dir([Link]);
3 if (cor(irmao) == Nodo.V) {
4 [Link] = Nodo.P; // caso 1
5 [Link] = Nodo.V; // caso 1
6 gireParaEsquerda([Link]); // caso 1
7 irmao = dir([Link]); // caso 1
8 }
9 if ((corEsq(irmao)==Nodo.P)&&(corDir(irmao)==Nodo.P)){
10 if (irmao !=null) [Link] = Nodo.V; // caso 2
11 x = [Link]; // caso 2
12 } else {
13 if (corDir(irmao) == Nodo.P) {
14 [Link] = Nodo.P; // caso 3
15 [Link] = Nodo.V; // caso 3
16 gireParaDireita(irmao); // caso 3
17 irmao = dir([Link]); // caso 3
18 }
19 [Link] = [Link]; // caso 3 e 4
20 [Link] = Nodo.P; // caso 3 e 4
21 [Link] = Nodo.P; // caso 3 e 4
22 gireParaEsquerda([Link]); // caso 3 e 4
23 x = raiz; // caso 3 e 4
24 }
25 return x;
26 }

Listagem 15.24 Conserto de Esquerdo Curto

Nas figuras ilustrativas das ações de conserteFilhoEsquerdoRem


apresentadas a seguir, α, β, γ, δ,  e η são subárvores rubro-negras,
472 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

e x, um nodo preto cuja altura preta tem uma unidade a menos


que a de seu irmão na hierarquia, e que, portanto, ainda demanda
conserto de sua altura, e para isso um ou mais dos seguintes casos
programados na Listagem 15.24 devem ser executados.
Caso 1: caracterizado pelo fato de o irmão de x ser vermelho, e
B, seu pai, ser obrigatoriamente preto, pois vermelho não pode
ser filho de vermelho, como ilustra a Figura 15.25(a).

(a) (b)

Figura 15.25 Rotação à Esquerda com Pivô B

Nesse caso, deve-se realizar uma rotação de nodos para a es-


querda tendo B como pivô e a mudança de cores de B e D,
transformando a configuração (a) em (b).
Note que o nodo C, o novo irmão de x após a rotação, tem uma
altura preta uma unidade acima da de A, porque sua altura é
igual a de D, e o foco x tem sempre altura menor, e isso requer
consertos vinculados ao presente foco x a ser efetuados segundo
as configurações descritas nos casos 2, 3 ou 4.
Caso 2: caracterizado pelo fato de o irmão de x e seus respectivos
filhos serem pretos, como ilustrado na Figura 15.26(a). A cor
do nodo pai de x pode ser vermelha ou preta. Nesse caso,
para reequilibrar A e D, basta mudar a cor do irmão de x para
vermelho, assim, reduzindo sua altura de uma unidade.
15.4. REMOÇÃO DE ITENS 473

(a) (b)

Figura 15.26 Acertando alturas de x


 e seu irmão

Essa etapa de conserto é dada como concluı́da, mas, como


altura preta de B foi reduzida de uma unidade, nova iteração
de consertos deve iniciar-se com B sendo o novo foco x.
Caso 3: caracterizado pelo fato de o filho esquerdo do irmão di-
reito de x ser de cor vermelha e o direito de cor preta, como
ilustra a Figura 15.27(a). Esse caso é acionado quando caso 2
não for elegı́vel.

(a) (b)

Figura 15.27 Rotação à Esquerda com Pivô B

Nesse caso, faz-se uma rotação para a direita tendo o nodo


474 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

irmão de x como pivô, e trocam-se as cores desse irmão e seu


filho esquerdo, como ilustrado na Figura 15.27(b).
Essa transformação não altera alturas pretas, mas permite que
o nodo pai de x possa ser colorido de preto, como ilustrado na
Figura 15.28, na qual é feita uma rotação para a esquerda com
o pivô B, que é colorido de preto, e D, o irmão de x, recebe a
cor original de B, produzindo Figura 15.28(b), tornando todas
as alturas pretas corretas.
Caso 4: caracterizado por uma situação em que o filho direito do
irmão de x é vermelho, como ilustra a Figura 15.28(a).

(a) (b)

Figura 15.28 Rotação à Esquerda com Pivô B

Nesse caso, para aumentar a altura de preto dos caminhos que


passam por x, basta fazer uma rotação para a esquerda tendo B
como pivô, seguida das mudanças das cores de B e E para preto,
tornando todas as alturas pretas corretas.

Conserto no filho direito

Os consertos que devem ser realizados após a remoção de um nodo,


quando o foco x for raiz de subárvore direita, são realizados pelo
15.4. REMOÇÃO DE ITENS 475

método conserteFilhoDireitoRem, definido na Listagem 15.29, e


que tem o código do método descrito conserteFilhoEsquerdoRem,
onde ocorrências de esq são sistematicamente trocadas por dir e
vice-versa.
1 private Nodo conserteFilhoDireitoRem(Nodo x) {
2 Nodo irmao = esq([Link]);
3 if (cor(irmao) == Nodo.V) { // caso 1
4 [Link] = Nodo.P; [Link] = Nodo.V;
5 gireParaDireita([Link]);
6 irmao = esq([Link]);
7 }
8 if ( (corDir(irmao)==Nodo.P) && (corEsq(irmao)==Nodo.P)){
9 [Link] = Nodo.V; // caso 2
10 x = [Link];
11 } else {
12 if (cor([Link]) == Nodo.P) { // caso 3
13 [Link] = Nodo.P; [Link] = Nodo.V;
14 gireParaEsquerda(irmao);
15 irmao = esq([Link]);
16 }
17 [Link] = [Link]; [Link] = Nodo.P;
18 [Link] = Nodo.P; // caso 4
19 gireParaDireita([Link]);
20 x = raiz;
21 }
22 return x;
23 }

Listagem 15.29 Conserto de direito curto

O custo da operação retire é proporcional ao dobro da altura


real da árvore. Isso correspondente ao custo de localizar o nodo
a ser removido, seguido pelo dos consertos que podem ter que ser
feitos no caminho de volta à raiz, i.e, o custo total é no máximo
4 lg(n + 1).
476 CAPÍTULO 15. ÁRVORES RUBRO-NEGRAS

15.5 Conclusão

Nas árvores AVLs, a altura h é tal que


lg(n + 1) < h < lg(n + 2) − 0.328.
Nas árvores rubro-negras, h ≤ 2 lg(n+1), portanto, em ambas o
custo de suas operações caracterı́sticas de um dicionário é O(lg n).
Portanto, ambas têm a mesma complexidade nas operações de pes-
quisa, inserção e remoção.
Há contudo pequenas diferenças de desempenho: como tipica-
mente lg(n + 2) < 2 lg(n + 1), busca em AVL é na prática mais
rápida que em ARN, mas as operações de rebalanceamento das
AVLs são mais complexas, por exemplo, nas AVLs pode-se ter que
percorrer todo o caminho de volta até a raiz realizando rotações,
enquanto que nas ARNs, tipicamente, três rotações poderiam ser
suficientes.
Essas observações sugerem que AVLs sejam recomendadas em
aplicações em que os dados da estrutura são estáveis e há intensas
operações de busca. Por outro lado, se as operações de modificação
da estrutura da árvore são as mais frequentes, o melhor é usar ARN.

Exercı́cios
1. Como os algoritmos de inserção, pesquisa e remoção de nodos
de uma ARN devem ser modificados para funcionar sem o
campo pai de cada nodo?

Notas Bibliográficas

Um texto indispensável sobre ARN é o livro Algoritmos: Teoria


e Prática de T. Cormen el alii [22], o qual apresenta em claros
detalhes o conceito e implementação dessa importante estrutura de
dados. Os algoritmos de árvores rubro-negras apresentados neste
Estruturas de Dados Fundamentais Bibliografia 477

capı́tulo são fortemente baseados nesse excelente trabalho de T.


Cormen et alii.
Outros livros-textos são Algorithms de Roberto Sedgwidk [19]
e Estruturas de Dados e Algoritmos em Java de Michael T.
Goodrich e Roberto Tomassia [13], entre outros.
478 PROGRAMAÇÃO MODULAR Estruturas de Dados Fundamentais
Bibliografia

[1] Adam Drozdek. Estrutura de Dados e Algoritmos em C++.


Thomson, 2002.
[2] Alfred V, Aho, John E. Hopcroft e Jeffrey D. Ullman. The De-
sign and Analysis of Computer Algorithms. Addison-Wesley
Publishing Company, 1974.
[3] Alfred V. Aho, J.E. Hopcroft e J.D. Ullman, J.D. Data Struc-
ture and Algorithms. Addison-Wesley, 1983.
[4] Bruno R. Preiss, Estruturas de Dados e Algoritmos. Editora
Campus, 2000.
[5] Clésio S. Santos e Paulo A. Azeredo. Tabelas: Organização
e Pesquisa. Editora Sagra Luzzatto, 2001.
[6] Donald Knuth. The Art of Computer Programming, Vo-
lume 1: Fundamental Algorithms. Addison-Wesley, Second
Edition, 1973.
[7] Donald Knuth. The Art of Computer Programming. Volume
3: Sorting and Searching. Addison-Wesley, Second Edition,
1973.
[8] Ken Arnold e James Gosling. The Java Programming Lan-
guage. Addison-Wesley, Third Edition, 2000.
[9] Liskov, B.; Zilles, S. Programming with Abstract Data Type.
ACM Sigplan Notices, march, 1974.
479
480 BIBLIOGRAFIA

[10] Liskov, B. et alii. CLU Reference Manual, Springer-Verlag,


Berlin, 1981.
[11] Liskov, B. et alii. Abstraction and Specification in Program
Development, MIT Press and McGraw-Hill, New York, 1986.
[12] Liskov, B. Data Abstraction and Hierarchy, ACM Sigplan
Notices, 23,5 (May, 1988).
[13] Michael T. Goodrich e Roberto Tamassia. Estrutura de Da-
dos e Algoritmos em Java. Bookman, 2a Edição, 2001.
[14] Nivio Ziviani. Projeto de Algoritmos com Implementação
em Pascal e C. Editora Thompson, 2004. (Capı́tulos 1,3 e 5
[15] Nivio Ziviani. Projeto de Algoritmos com Implementações
em Java e C++. Editora Thompson, 2007.
[16] Nivio Ziviani e F.W. Trompa. A Look at Symmetric Binary
B-Trees. INFOR Canadian Journal of Operational Research
and Information Processing 20(2), 65-81, 1982.
[17] Roberto S. Bigonha e Mariza A. S. Bigonha. Estruturas de
Dados Fundamentais. Notas de Aula, 2017.
[18] Roberto S. Bigonha. A Linguagem Java, Série Programação
Modular, Quarta Edição, 2020, Belo Horizonte.
[19] Robert Sedgewick. Algorithms. Second Edition, Addison-
Wesley, 1988.
[20] Robert Sedgewick. Algorithms in C++. Addison-Wesley,
1992. (Cap. 17).
[21] Sara Baase. Computer Algorithms – Introduction to Design
and Analysis., 2nd Ed., Addison-Wesley, 1988.
BIBLIOGRAFIA 481

[22] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest e


Clifford Stein. Algoritmos: Teoria e Prática. Editora Cam-
pus, 2002.
[23] Thomas Standish. Data Structures in Java. Addison-Wesley,
1998.
[24] N. Wirth. Algoritmos e Estruturas de Dados. Prentice-Hall
do Brasil Ltda, 1989. (Capı́tulo 3, Seção 4.5 e Seção 4.7).
Índice Remissivo

Árvores Árvores de Pesquisa


altura, 178 Árvores binárias, 281
caminhamento iterativo, 186 AVL, 281
caminhamento recursivo, 182 B, 281
florestas, 178 Digital, 214
grau, 178 Patricia, 226, 281
livres, 175 Patricia binária, 231
nı́vel, 178 Patricia m-ária, 267
ordenadas, 177 Rubro-Negras, 281
orientadas, 177 SBB, 281
representação, 184 Trie, 220, 281
Árvores B, 349 Citações
Árvores Balanceadas Albert Einstein, 75, 157, 213,
AVL, 301 301
Árvores Binárias Antoine de Saint-Exupéry, 145
definição, 179 Apeles, 55
estendidas, 193 B. Liskov, 74
iteradores, 189 Donald Knuth, 85, 129, 143,
Árvores Rubro-Negras, 453 155, 173, 196, 211, 280,
altura preta, 455 300, 347, 400
altura real, 455 E.M. Landis, 301
rotações, 461 G.M Adel’son-Vel’skii, 301
Árvores SBB, 401 Leonardo da Vinci, 1, 131
altura horizontal, 402 Mário Quintana, 401
altura vertical, 402 Mahatma Gandhi, 197
483
484 ÍNDICE REMISSIVO

Nivio Ziviani, 452 notação ω, 46


René Descartes, 349, 453 notação Θ, 43
Robert Sedgewick, 452, 477 notação O, 28, 29
Santossh Kalwar, 175 notação o, 44
Thomas Cormen, 477 Linguagens
William of Ockham, 87 CLU, 74
Williams J.W.J., 211 Listas
Complexidade definição, 87
algoritmos não-recursivos, 9 duplamente encadeadas, 114
algoritmos recursivos, 16 genéricas, 98
assintótica, 27, 28, 33 invertidas, 121
busca exaustiva, 39 lineares, 87
dominação assintótica, 29 máquina de estado, 107
força bruta, 39 sequenciais, 89, 94
função de complexidade, 3 simplesmente encadeadas, 102
função de custo, 3 tratamento de falhas, 94
custo médio, 8 Modularidade, 59
melhor caso, 7 abstração, 59
pior caso, 5 encapsulação, 60
Dicionário, 72 interface, 60
módulo, 59
Filas
Ordens de complexidade, 36
circulares, 146
cúbica, 38
encadeadas, 151
constante, 36
sequenciais, 146
exponencial, 39
Filas de Prioridades, 197
linear, 37
árvore completa, 198
linear logarı́tmica, 37
heap, 200
logarı́tmica, 37
Limites assintóticos polinomial, 38
notação Ω, 45 quadrática, 38
ÍNDICE REMISSIVO 485

Pesquisa
binária, 83
sequencial, 77
Pilhas
encadeadas, 136
genéricas, 140
heterogêneas, 135
sequenciais, 132
Tabela de Dispersão
análise de custo, 165, 172
colisão aberta, 166
colisão encadeada, 161
colisão inevitável, 160
função hash, 158
hash linear, 166
Tabelas
dicionário, 77
ficha de dados, 75
ordenadas, 80
pesquisa, 75
registro, 75
Tipos abstratos, 63
dicionário, 72
encapsulação, 64
ficha de dados, 66
gap semântico, 56
lacuna semântica, 55, 56
ocultação de informação, 64

Você também pode gostar