Escolar Documentos
Profissional Documentos
Cultura Documentos
Nissocapítulo
Dadosas árvores são estruturas nas quais, ao contrário das listas, cada elemento
está vinculado a mais de um elemento. Em algumas árvores, cada elemento (às
vezes chamado denode ) pode ser vinculado a um número variável de outros ele-
mentos. Na maioria das vezes, porém, eles estão vinculados a um número fixo de
elementos. Nas árvores binárias, como o nome sugere, cada elemento está vincu-
lado a dois elementos. Esses links são chamadosramos . Em árvores binárias, es-
ses ramos são chamados de ramos esquerdo e direito. A Figura 10.1 mostra um
exemplo de árvore binária.
Figura 10.1 Uma árvore binária é uma estrutura recursiva composta de uma raiz e dois ramos. A ramificação
esquerda é um link para a subárvore esquerda e a ramificação direita é um link para a subárvore direita. Os
elementos terminais possuem ramos vazios (não representados na figura) e são chamados de folhas.
A árvore representada na figura não é comum porque seus elementos são de tipos
diferentes. É uma árvore de Any . Na maioria das vezes, você lidará com árvores
de um tipo mais específico, como árvores de números inteiros.
Na figura 10.1 , você pode ver que uma árvore é uma estrutura recursiva. Cada
galho leva a uma nova árvore (muitas vezes chamada desubárvore ). Você tam-
bém pode ver que algumas ramificações levam a um único elemento. Isso não é
um problema porque esse único elemento é, na verdade, uma árvore com galhos
vazios. Tais elementos terminais são freqüentemente chamados de folhas . Ob-
serve também o T elemento: ele tem um ramo esquerdo, mas não o direito. Isso
ocorre porque o ramo direito está vazio e não é representado. A partir disso, você
pode inferir a definição de uma árvore binária. Uma árvore é uma das seguintes:
Cada ramificação contém uma (sub)árvore. Uma árvore em que todos os elemen-
tos têm dois ramos ou nenhum ramo é chamada de árvore completa . A árvore na
figura 10.1 não está cheia, mas a subárvore esquerdaé.
Binárioas árvores podem ser mais ou menos equilibradas. Uma árvore perfeita-
mente balanceada é uma árvore na qual os dois ramos de todas as subárvores
contêm o mesmo número de elementos. A Figura 10.2 mostra três exemplos de ár-
vores com os mesmos elementos. A primeira árvore está perfeitamente equili-
brada e a última árvore está totalmente desequilibrada. Árvores binárias perfeita-
mente balanceadas às vezes são chamadas de árvores perfeitas .
Figura 10.2 As árvores podem ser mais ou menos equilibradas. A árvore superior é perfeitamente equilibrada
porque os dois ramos de todas as subárvores contêm o mesmo número de elementos. A árvore à direita é
uma lista encadeada individualmente, que é um caso especial de uma árvore totalmente desbalanceada.
UMAA árvore pode ser caracterizada pelo número de elementos que contém e
pelo número de níveis nos quais esses elementos estão distribuídos:
DentroNa seção 10.1, eu disse que uma árvore é composta de um elemento raiz
com zero, um ou dois ramos. Esta é uma definição ingênua que não permite todos
os cálculos necessários. Em particular, não representa árvores vazias. Podemos,
no entanto, incluir árvores vazias alterando ligeiramente a definição. Uma árvore
é ou
De acordo com essa nova definição recursiva, cada elemento que parece não ter
ramos na figura 10.2 tem na verdade duas árvores vazias como seus ramos es-
querdo e direito. Elementos que parecem ter um único galho têm, na realidade,
uma árvore vazia como segundo galho. Por convenção, ramos vazios não são re-
presentados em diagramas. Outra convenção mais importante é que a altura e a
profundidade de uma árvore vazia são iguais a –1. Você verá que isso é necessário
para algumas operações comobalanceamento.
Figura 10.4 Um exemplo de uma árvore de busca ordenada ou binária. Todos os elementos em um ramo (por
convenção, o ramo esquerdo) têm um valor menor que o elemento raiz, e todos os elementos no outro ramo
(por convenção, o ramo direito) têm um valor maior que o elemento raiz. As mesmas características se apli-
cam a todas as subárvores.
Uma consequência direta disso é que uma busca recursiva em uma árvore biná-
ria perfeitamente balanceada nunca transborda a pilha. Como você viu no capí-
tulo 4, o tamanho padrão da pilha permite de 1.000 a 3.000 passos recursivos.
Como uma árvore binária perfeitamente balanceada de altura 1.000 contém
2^1.000 elementos, você nunca terá memória principal suficiente para tal árvore.
Esta é uma boa notícia. Mas a má notícia é que nem todas as árvores binárias são
perfeitamente balanceadas. Como a árvore binária totalmente desbalanceada é
uma lista encadeada individualmente, ela tem o mesmo desempenho e o mesmo
problema em relação à recursão da lista. Isso significa que, para obter o máximo
das árvores, você precisa encontrar uma maneira de equilibrareles.
10.7 Ordem de inserção e estrutura das árvores
oA estrutura de uma árvore (ou seja, quão bem balanceada ela é) depende da or-
dem de inserção de seus elementos. A inserção é feita da mesma forma que a
pesquisa:
Se a árvore estiver vazia, crie uma nova árvore com o elemento como raiz e
duas árvores vazias como ramos esquerdo e direito.
Se a árvore não estiver vazia, compare o elemento a ser inserido com a raiz. Se
forem iguais, está feito. Não há nada para inserir porque você só pode inserir
um elemento abaixo ou acima da raiz.
Considere o ramo esquerdo da árvore da figura 10.5 . Este ramo é em si uma ár-
vore composta pela raiz 1, o ramo esquerdo 0 e o ramo direito 2. Você pode per-
correr esta árvore em seis ordens:
1, 0, 2
1, 2, 0
0, 1, 2
2, 1, 0
0, 2, 1
2, 0, 1
Você pode ver que três dessas ordens são simétricas com as outras três: 1, 0, 2 e 1,
2, 0 são simétricas. Você começa da raiz e depois visita os dois ramos, da esquerda
para a direita ou da direita para a esquerda. O mesmo vale para 0, 1, 2 e 2, 1, 0,
que diferem apenas na ordem dos ramos, e novamente para 0, 2, 1 e 2, 0, 1. Consi-
derando apenas a direção da esquerda para a direita (porque a outra direção é
exatamente a mesma, como se fosse vista em um espelho), restam três ordens,
que recebem o nome da posição da raiz:
Pré-encomenda (1, 0, 2 ou 1, 2, 0)
Em ordem (0, 1, 2 ou 2, 1, 0)
Pós-ordem (0, 2, 1 ou 2, 0, 1)
Figura 10.6 A travessia em profundidade consiste em percorrer a árvore dando prioridade à altura. Isso pode
ser aplicado com três pedidos principais: pré-pedido, em pedido e pós-pedido.
Esses termos são cunhados após a posição do operador em uma operação. Para
ver melhor a analogia, imagine a raiz (1) substituída por um sinal de mais (+). Isso
lhe daria
Prefixo (+, 0, 2 ou +, 2, 0)
Infixo (0, +, 2 ou 2, +, 0)
Posfixo (0, 2, + ou 2, 0, +)
Você pode implementar uma árvore binária da mesma forma que uma lista enca-
deada individualmente, com uma cabeça (chamada value ) e duas caudas (os ra-
mos, chamados left e right ). Nesta seção, você definirá uma Tree classe abs-
tratacom duas subclasses nomeadas T e Empty . T representa uma árvore não
vazia, enquanto Empty , sem surpresa, representa a árvore vazia.
Assim como o List , a árvore vazia pode ser representada por um objeto single-
ton, graças ao fato de que o Kotlin lida com a variância. Este objeto singleton será
um arquivo Tree<Nothing> . A Listagem 10.1 representa a implementação mí-
nima Tree .
Listagem 10.1 Tree A implementação mínima
objeto complementar {
⑥ Todas as propriedades são internas para que não possam ser acessadas direta-
mente de outros módulos.
Esta classe é bastante simples, mas é inútil enquanto você não tiver como cons-
truir uma árvore real. Mas antes de adicionar isso, há um problema que você
deve levar em consideração.
DentroNo capítulo 5, você aprendeu como a variância pode ser aplicada às listas.
A variação também se aplica às árvores. Fazer uma covariante de árvore em seu
parâmetro parece fazer sentido. Afinal, a Tree<Int> também deve ser utilizável
onde a Tree<Number> é esperado. Isso é o que você obtém ao criar a covariante
da árvore por meio do uso da palavra- out chave. Por outro lado, você não deve
ser capaz de adicionar um Number a um arquivo Tree<Int> . Se isso fosse possí-
vel, a Tree turmaseria contravariante em seu parâmetro de tipo.
Definir uma plus funçãopara inserir um valor em uma árvore. Como de cos-
tume, a Tree estrutura é imutável e persistente, então uma nova árvore com o
valor agregado deve ser construída, deixando a árvore original intocada. Chamar
essa função plus permite usar o + operador para adicionar um elemento a uma
árvore.
Dica
Se o elemento a adicionar for igual à raiz, deve-se devolver uma nova árvore com
o valor inserido como raiz e deixar os dois ramos originais inalterados. Caso con-
trário, um valor menor que a raiz deve ser adicionado ao ramo esquerdo e um va-
lor maior que a raiz deve ser adicionado ao ramo direito. Você deve criar uma
função na Tree classe paicom a seguinte assinatura:
Se você preferir definir uma função abstrata na Tree classe pai e implementá-la
em cada subclasse, bem... apenas tente.
Solução
Se é isso que você pretendia, retornar a árvore original seria uma boa otimização.
Isso funcionaria, mas seria tedioso obter o mesmo resultado que a mutação de um
elemento de árvore. Você teria que remover o elemento antes de inserir um ele-
mento igual com algumas propriedades alteradas. Você encontrará este caso de
uso ao implementar um mapa emcapítulo 11.
Isso não funciona porque usar o element parâmetro do tipo Nothing (in
T(Empty, element, Empty) ) sempre falha porque Nothing não pode ser ins-
tanciado. Embora você saiba que element não é do tipo Nothing , não pode es-
pecificar o tipo porque os parâmetros de tipo não são permitidos para objetos. Por
esse motivo, você precisa definir a função na Tree classeusando correspondência
de padrões. No Empty caso, o parâmetro ainda é do tipo A , e sem
problemassurge.
10.9.3 Sobrecarga de operadores
Não há como evitar a especificação do tipo de argumento, mesmo que seja indi-
cado à esquerda do sinal de igual. isso não vaicompilar:
Por enquanto, porém, você não precisa lidar com isso. Em vez de implementar
operações recursivas seguras em pilha, você encontrará uma maneira de balan-
cear automaticamente as árvores. A árvore simples em que você está trabalhando
é apenas para aprendizado. Nunca será usado na produção. Mas as árvores balan-
ceadas são mais complexas de implementar, então é mais fácil começar com a ár-
vore desbalanceada simples.
Exercício 10.2
Muitas vezes é útil construir uma árvore a partir de algum outro tipo de coleção.
Os dois casos mais úteis são construir uma árvore a partir de um List e cons-
truir uma árvore a partir de um array. Implemente uma função para cada um
desses casos. Use o List tipo definido nos capítulos anteriores, não o List tipo
Kotlin.
Solução
Para fazer o mesmo para uma lista Kotlin, você precisa alterar o tipo do argu-
mento sem alterar nada na implementação:
O List tipo que você definiu nos capítulos 5 e 8 tem, deste ponto de vista, uma
pequena diferença: a foldRight função é curry e, além disso, a
List.foldRight funçãonão é seguro para pilhas. Nesse caso, é muito melhor
usar foldLeft :
Exercício 10.3
Solução
O que você precisa fazer é comparar o parâmetro com a árvore value (o valor na
raiz da árvore):
Você também pode ter resolvido o exercício com uma implementação como esta:
Exercício 10.4
Escreva duas funções para calcular o size e height de uma árvore. Aqui estão
suas possíveis assinaturas na Tree classe:
Você deve tentar encontrar uma solução melhor, como fez para o tamanho da
lista no capítulo 8.
Solução
importar kotlin.math.max
...
Com base nisso, você pode ver porque a altura de uma árvore vazia precisa ser
igual a –1. Se fosse 0, a altura seria igual ao número de elementos no caminho em
vez do número de segmentos.
...
...
}
Exercício 10.5
Escreva max e min funções para calcular os valores máximos e mínimos contidos
em uma árvore.
Dica
Solução
Exercício 10.6
Escreva uma remove funçãoque remove um elemento de uma árvore. Esta fun-
ção receberá um elemento como parâmetro. Se este elemento estiver presente na
árvore, ele será removido e a função retornará uma nova árvore sem este ele-
mento. Esta nova árvore respeitará os requisitos de que todos os elementos no
ramo esquerdo devem ser inferiores à raiz e todos os elementos no ramo direito
devem ser superiores à raiz. Se o elemento não estiver na árvore, a função retor-
nará a árvore inalterada. A assinatura da função será
Árvore<A> remove(A a)
Dica
Você precisará definir uma função para mesclar duas árvores com a particulari-
dade de que todos os elementos de uma são maiores ou menores que todos os ele-
mentos da outra. Certifique-se de lidar com a variação conforme necessário.
Solução
Quando a árvore é Empty , a função não pode remover nada e retornará this .
Caso contrário, aqui está o algoritmo que você precisará implementar:
A mesclagem é uma mesclagem simplificada porque você sabe que todos os ele-
mentos do ramo esquerdo são inferiores a todos os elementos do ramo direito.
Primeiro você deve definir a merge funçãocom a seguinte assinatura:
Desta vez, se this a árvore estiver vazia, a função retorna seu parâmetro. Caso
contrário, o seguinte algoritmo é usado:
}
}
}
Observe que as raízes das duas árvores não podem ser iguais porque as duas ár-
vores a serem mescladas devem ser os ramos esquerdo e direito da árvore origi-
nal. Agora você pode escrever o remove função:
DentroNa seção anterior, você usou uma função de mesclagem restrita que só po-
deria mesclar árvores se todos os valores em uma árvore fossem menores do que
todos os valores da outra árvore. A fusão de árvores é o equivalente à concatena-
ção de listas. Você precisa de uma função mais geral para lidar com a fusão de ár-
vores arbitrárias.
Exercício 10.7 (difícil)
Até agora, você apenas uniu árvores nas quais todos os elementos de uma árvore
eram maiores do que todos os elementos da outra. Escreva uma merge funçãoque
mescla árvores arbitrárias. sua assinatura é
Solução
Com esta implementação, a raiz da árvore não será substituída pela raiz da ár-
vore de parâmetros se ambas as raízes forem iguais. Isso pode ou não correspon-
der às suas necessidades. Se você deseja que a raiz mesclada substitua a raiz ini-
cial, altere a última linha da implementação para isto:
Esse algoritmo é ilustrado pelas figuras 10.8 a 10.17. Nessas figuras, os galhos va-
zios são explicitamente representados. Como lembrete, “ this ” significa a árvore
na qual a função é definida.
Figura 10.8 As duas árvores a serem mescladas. À esquerda está a this árvore e à direita está a árvore de
parâmetros.
Figura 10.9 A raiz da árvore de parâmetros é maior que a raiz da this árvore. Mescle o ramo direito da
this árvore com a árvore de parâmetros com o ramo esquerdo removido. (A operação de mesclagem é re-
presentada pela caixa pontilhada.)
Figura 10.10 Sendo iguais as raízes de cada árvore a ser mesclada, você usa this value para o resultado da
mesclagem. O ramo esquerdo será o resultado da fusão dos dois ramos esquerdos, e o ramo direito será o re-
sultado da fusão dos dois ramos direitos.
Figura 10.11 Para o ramo esquerdo, mesclar com uma árvore vazia é trivial e retorna a árvore original (raiz 4
e dois ramos vazios). Para o ramo direito, a primeira árvore tem ramos vazios e tem 6 como raiz, e a segunda
árvore tem 7 como raiz, então você remove o ramo esquerdo da árvore com 7 raízes e usa o resultado para
mesclar com o ramo direito vazio da árvore de 6 raízes. A ramificação esquerda removida será mesclada com
o resultado da mesclagem anterior. A árvore com 6 raízes à direita vem da árvore com 7 raízes, onde foi
substituída por uma árvore vazia.
Figura 10.12 As duas árvores a serem mescladas têm raízes iguais (6), então você mescla os galhos (esquerda
com esquerda e direita com direita). Como a árvore a ser mesclada tem os dois galhos vazios, na verdade não
há nada a fazer.
Figura 10.13 Mesclar uma árvore vazia resulta na árvore a ser mesclada. Duas árvores permanecem com a
mesma raiz para se fundir.
Figura 10.14 Mesclar duas árvores com a mesma raiz é simples: mescle direita com direita e esquerda com es-
querda e use os resultados como os novos galhos.
Figura 10.15 A junção à esquerda é trivial porque as raízes são iguais e ambas as ramificações da árvore a se-
rem mescladas estão vazias. Do lado direito, a árvore a ser mesclada tem uma raiz inferior (4), então você re-
move o ramo direito (E) e mescla o que resta com o ramo esquerdo da árvore original.
Figura 10.16 Mesclando duas árvores idênticas (não precisa de explicação)
Por outro lado, a altura do resultado é maior do que você poderia esperar. A fusão
de duas árvores de altura 3 pode resultar em uma árvore de altura 5. É fácil ver
que a altura ideal não deve ser maior que log2(tamanho). A altura ideal é a menor
potência de dois maior que o tamanho resultante. Neste exemplo, os tamanhos
das duas árvores originais eram 7 e suas alturas eram 3. O tamanho da árvore
mesclada é 9 e a altura ideal seria 4 em vez de 5. Em um exemplo tão pequeno,
isso pode não ser uma questão. Mas quando você está mesclando árvores grandes,
pode acabar com árvores mal balanceadas, resultando em desempenho abaixo do
ideal e até possivelmente em um estouro de pilha ao usarfunções recursivas.
Calcular a soma dos elementos em uma árvore de inteiros é trivial porque a adi-
ção é associativa em ambas as direções e comutativa. As seguintes expressões têm
os mesmos valores:
* (((1 + 3) + 2) + ((5 + 7) + 6)) + 4
* 4 + ((2 + (1 + 3)) + (6 + (5 + 7)))
* (((7 + 5) + 6) + ((3 + 1) + 2)) + 4
* 4 + ((6 + (7 + 5)) + (2 + (3 + 1)))
* (1 +(2 + 3)) + (4 + (5 + (6 + (7))))
* (7 + (6 + 5)) + (4 + (3 + (2 + 1)))
Examinando essas expressões, você pode ver que elas representam alguns resul-
tados possíveis de dobrar a seguinte árvore usando adição:
4
/ \
/ \
2 6
/ \ / \
1 3 5 7
Pós-pedido deixado
Encomenda restante
Direito pós-encomenda
direito de pré-encomenda
esquerda em ordem
direito em ordem
Esquerda e direita significam começar pela esquerda e começar pela direita, res-
pectivamente. Você pode verificar isso calculando o resultado de cada expressão.
Por exemplo, a primeira expressão pode ser reduzida da seguinte forma:
( 6 + ( 12 + 6)) + 4 usado: 1, 3, 2, 5, 7
( 6 + 18 ) + 4 usado: 1, 3, 2, 5, 7, 6
24 + 4 usado: 1, 3, 2, 5, 7, 6
28 usado: 1, 3, 2, 5, 7, 6, 4
Existem outras possibilidades, mas essas seis são as mais interessantes. Embora
sejam equivalentes para adição, podem não ser para outras operações, como adi-
cionar caracteres a strings ou adicionar elementos a listas.
oproblema ao dobrar uma árvore é que a abordagem recursiva será de fato bi-re-
cursiva. Você pode dobrar cada ramificação com a operação especificada, mas
precisa de uma maneira de combinar os dois resultados em um.
Isso o lembra da paralelização de dobramento de lista que você aprendeu no capí-
tulo 8? Sim, você precisa de uma operação adicional. Se a operação necessária
para dobrar Tree<A> for uma função de B para A para B , você precisará de
uma função adicional de B para B para B mesclar os resultados da esquerda e da
direita.
Exercício 10.8
Escreva uma foldLeft funçãoque dobra uma árvore, dadas as duas funções des-
critas anteriormente. Sua assinatura na Tree aulaé o seguinte:
Solução
sobrepor
fun <B> foldLeft(identity: B, f: (B) -> (A) -> B, g: (B) -> (B) -> B): B =
g(direita.dobraEsquerda(identidade, f, g))(f(esquerda
.foldLeft(identity, f, g))(this.value))
sobrepor
fun <B> foldLeft(identity: B, f: (B) -> (A) -> B, g: (B) -> (B) -> B): B =
g(*esquerda*.dobraEsquerda(identidade, f, g))(f(*direita*
.foldLeft(identity, f, g))(this.value))
Por que isso é um problema? Se você dobrar uma árvore com uma operação co-
mutativa, como a adição, o resultado não mudará. Mas se você usar uma operação
que não seja comutativa, terá problemas. O resultado final é que as duas soluções
fornecerão resultados diferentes. Por exemplo, esta função
[7, 5, 3, 1, 2, 4, 6, NIL]
[7, 5, 6, 3, 4, 1, 2, NIL]
Figura 10.18 Lendo a árvore da esquerda para a direita e da direita para a esquerda. Embora ambas as listas
estejam em ordens diferentes, elas representam a mesma árvore.
Esta não é a mesma diferença que foldLeft e foldRight para a List classe. A
dobra da direita para a esquerda é, na verdade, uma dobra à esquerda da lista in-
vertida. Uma dobra direita pareceria
sobrepor
fun <B> foldRight(identity: B, f: (A) -> (B) -> B, g: (B) -> (B) -> B): B =
g(f(this.value)(left.foldRight(identity, f, g)))(right
.foldRight(identidade, f, g))
Isso étambém é possível dobrar com uma única função tomando um parâmetro
adicional, o que significa, por exemplo, uma função de B para A para B para B .
Mais uma vez, haverá muitas implementações possíveis, dependendo da ordem
de travessia.
Exercício 10.9
Em ordem: 1 2 3 4 5 6 7
Pré-encomenda: 4 2 1 3 6 5 7
Pós-ordem: 1 3 2 5 7 6 4
Solução
substituir fun <B> foldInOrder(identity: B, f: (B) -> (A) -> (B) -> B): B =
f(esquerda.foldInOrder(identidade, f))(valor)(direita
.foldInOrder(identidade, f))
substituir fun <B> foldPreOrder(identidade: B, f: (A) -> (B) -> (B) -> B): B =
f(valor)(esquerda.foldPreOrder(identidade, f))(direita
.foldPreOrder(identidade, f))
substituir fun <B> foldPostOrder(identity: B, f: (B) -> (B) -> (A) -> B): B =
f(esquerda.foldPostOrder(identidade, f))(direita
.foldPostOrder(identidade, f))(valor)
você temagora escritas cinco funções de dobra diferentes. Qual deles você deve
escolher? Para responder a essa pergunta, vamos considerar quais propriedades
uma função de dobra deve ter.
Existe uma relação entre a forma como uma estrutura de dados é dobrada e a
forma como ela é construída. Você pode construir uma estrutura de dados come-
çando com um elemento vazio e adicionando elementos um por um. Este é o in-
verso de dobrar. Idealmente, você deve ser capaz de dobrar uma estrutura
usando parâmetros específicos, permitindo que você transforme a dobra em uma
função de identidade. Para uma lista, isso seria o seguinte:
Você também pode usar foldLeft , mas a função seria um pouco mais complexa:
Você pode fazer o mesmo com a dobragem de árvores? Para conseguir isso, você
precisará de uma nova maneira de construir árvores reunindo uma árvore es-
querda, uma raiz e uma árvore direita. Dessa forma, você poderá usar qualquer
uma das três funções de dobramento tomando apenas um parâmetro de função.
Crie uma função que combine duas árvores e uma raiz para criar uma nova ár-
vore. Sua assinatura no objeto companheiro será
Dica
Você terá que lidar com dois casos de forma diferente. Se as árvores a serem mes-
cladas estiverem ordenadas, o que significa que o valor máximo da primeira é
menor que a raiz da segunda e o valor mínimo da segunda é maior que a raiz da
primeira, você pode montar os três usando o T construtor. Caso contrário, você
deve recorrer a outra maneira de construir o resultado.
Solução
Você tem várias maneiras de implementar essa função. Uma delas é definir uma
função que teste as duas árvores para verificar se elas estão ordenadas. Para isso,
você pode primeiro definir funções para retornar o resultado da comparação de
valores:
fun <A: Comparável<A>> lt(primeiro: A, segundo: A): Booleano = primeiro < segundo
Se as árvores não estiverem ordenadas, você testa a ordem inversa antes de retor-
nar ao algoritmo normal de inserção/mesclagem. Agora você pode dobrar uma ár-
vore e obter a mesma árvore que ooriginal (desde que você use a função correta).
Você encontrará os seguintes exemplos no código de teste que acompanha este
livro:
árvore.foldInOrder(Árvore<Int>(),
{ t1 -> { i -> { t2 -> Árvore(t1, i, t2) } } })
tree.foldPostOrder(Tree<Int>(),
{ t1 -> { t2 -> { i -> Árvore(t1, i, t2) } } })
tree.foldPreOrder(Tree<Int>(),
{ i -> { t1 -> { t2 -> Árvore(t1, i, t2) } } })
Você também pode definir uma função de dobra usando apenas uma função com
dois parâmetros, como fez para List . O truque é primeiro transformar a árvore
em uma lista, conforme mostrado neste exemplo de foldLeft :
// Na árvore:
substituir fun toListPreOrderLeft(): List<A> =
left.toListPreOrderLeft().concat(right.toListPreOrderLeft()).cons(value)
Esta pode não ser a implementação mais rápida, mas ainda podeserútil.
Gostalistas, árvores podem ser mapeadas, mas mapear funções para árvores é um
pouco mais complicado do que mapear funções para listas. Aplicar uma função a
cada elemento de uma árvore pode parecer trivial, mas não é. O problema é que
nem todas as funções preservam a ordem. Adicionar um determinado valor a to-
dos os elementos de uma árvore de inteiros será bom, mas usar a função f( x ) = x
* x será muito mais complicado se a árvore contiver valores negativos porque
aplicar a função no local não resultará em uma árvore de busca binária.
Exercício 10.11
Defina uma map função para árvores. Tente preservar a estrutura da árvore, se
possível. Por exemplo, mapear uma árvore de números inteiros elevando os valo-
res ao quadrado pode produzir uma árvore com uma estrutura diferente, mas
mapear adicionando uma constante não deveria.
Dica
Uma vez que você tenha uma maneira de equilibrar as árvores, é fácil fazer com
que as árvores se auto-balanceem iniciando automaticamente o processo de ba-
lanceamento após cada operação que poderia alterar a estrutura da árvore.
Antes davocê pode balancear árvores, você precisa saber como alterar gradual-
mente a estrutura de uma árvore. A técnica utilizada é chamada de rotação da ár-
vore e está ilustrada nas figuras 10.19 e 10.20.
Figura 10.19 Girando uma árvore para a direita. Durante a rotação, a linha entre 2 e 3 é substituída por uma
linha entre 2 e 4, de modo que o elemento 3 se torna o elemento esquerdo de 4.
Figura 10.20 Girando uma árvore para a esquerda. O elemento esquerdo de 6 se torna 4 (anteriormente o pai
de 6), então o elemento 5 se torna o elemento direito de 4.
Exercício 10.12
Solução
Exercício 10.13
NOTA Se quiser experimentar mais exercícios, não hesite em definir uma fun-
ção para ordem da esquerda para a direita, bem como funções para pré-ordem e
pós-ordem.
Aqui está a assinaturapara a toListInOrderRight função:
Solução
Isso é simples e está mais relacionado a listas do que a árvores. Empty implemen-
tações retornam uma lista vazia. Você pode pensar na seguinte implementação
(na T classe):
O que você precisa é de uma versão corecursiva segura para pilha. Isso usa uma
função auxiliar corecursiva para balancear a árvore, que pode ser colocada no
objeto companheiro, com a função principal na Tree classe. Aqui está a função
auxiliar:
tailrec privado
fun <A: Comparável<A>> unBalanceRight(acc: List<A>,
árvore: Árvore<A>): Lista<A> =
quando (árvore) {
Vazio -> acc
é T -> quando (árvore.esquerda) {
Vazio -> unBalanceRight(acc.cons(tree.value),
árvore.direita) ①
é T -> unBalanceRight(acc,
tree.rotateRight()) ②
}
}
1. Gire a árvore para a esquerda até que o resultado tenha ramificações tão
iguais quanto possível. Isso significa que os tamanhos das ramificações serão
iguais se o tamanho total for ímpar e diferirão em 1 se o tamanho total for par.
O resultado será uma árvore com dois ramos totalmente desequilibrados de ta-
manho quase igual.
2. Aplique o mesmo processo recursivamente ao ramo direito. Aplique o pro-
cesso simétrico (girando para a direita) no ramo esquerdo.
3. Pare quando a altura do resultado for igual a log2( tamanho ). Para isso, você
precisará da seguinte função auxiliar:
Este método está intimamente relacionado com o logaritmo base 2. Para to-
dos os int valores positivos x:
andar(log2(x)) = 31 - númeroOfLeadingZeros(x)
ceil(log2(x)) = 32 - numberOfLeadingZeros(x - 1)
Exercício 10.14
Dica
Você também precisará de uma função para testar se uma árvore está totalmente
balanceada ou não, e uma para rotacionar recursivamente uma árvore. Aqui está
como você pode querer implementar a função para girar uma árvore no objeto
companheiro:
divertido <A> desdobrar(a: A, f: (A) -> Resultado<A>): A {
tailrec fun <A> desdobrar(a: Pair<Resultado<A>, Resultado<A>>,
f: (A) -> Resultado<A>): Pair<Resultado<A>, Resultado<A>> {
val x = a.second.flatMap { f(it) }
return x.map { desdobrar(Pair(a.segundo, x), f) }.getOrElse(a)
}
return Result(a).let { desdobrar(Pair(it, it), f).second.getOrElse(a) }
}
Infelizmente, isso não funcionará porque a função auxiliar não é recursiva. Para
torná-lo recursivo, você precisa de uma maneira de determinar se a Result é um
Success . Você pode determinar isso de duas maneiras:
Solução
Primeiro, você precisa definir a função de utilidade que testa se uma árvore está
desbalanceada. Para que seja balanceado, a diferença entre as alturas de ambos
os galhos deve ser 0 se o tamanho total dos galhos for par, e 1 se o tamanho for
ímpar:
Então você deve criar funções para acessar o valor e as ramificações em uma ár-
vore. Você pode fazer isso usando a mesma técnica usada para a altura e o tama-
nho. Definir propriedades abstratas na Tree classe:
valor de valor abstrato interno: A
Fazer isso fará com que a exceção seja lançada assim que as propriedades forem
inicializadas, ou seja, assim que o objeto for criado. Se você se lembra do que
aprendeu no capítulo 9, sabe que deve usar a inicialização preguiçosa:
interno
class T<out A: Comparable<@UnsafeVariance A>>(substituir val left: Tree<A>,
substituir valor val: A,
substituir val à direita: Tree<A>):
Árvore<A>() {
o balance A função funciona bem para a maioria das árvores, mas você não pode
usá-la com grandes árvores desbalanceadas porque elas transbordam a pilha.
Você pode contornar isso usando balance apenas em árvores pequenas e total-
mente desbalanceadas ou em árvores parcialmente balanceadas de qualquer ta-
manho. Isso significa que você deve equilibrar uma árvore antes que ela se torne
muito grande. A questão é se você pode fazer o balanceamento automático após
cada modificação.
Exercício 10.15
Solução
A solução óbvia é chamar balance após cada operação que modifica a árvore
conforme o código a seguir:
Isso funciona para árvores pequenas (que, de fato, não precisam ser balancea-
das), mas não funcionará para árvores grandes porque é muito lento. Uma solu-
ção é equilibrar apenas parcialmente as árvores. Por exemplo, você pode execu-
tar a função de balanceamento somente quando a altura for 100 vezes a altura
ideal de uma árvore totalmente balanceada:
Resumo
As árvores são estruturas de dados recursivas nas quais um elemento está vin-
culado a uma ou várias subárvores. Em algumas árvores, cada nó pode ser vin-
culado a um número variável de subárvores. Na maioria das vezes, porém,
eles estão vinculados a um número fixo de subárvores.
Nas árvores binárias, cada nó está vinculado a duas subárvores. Esses links
são chamados de ramificações e essas ramificações são chamadas de ramifica-
ções esquerda e direita. As árvores de busca binárias permitem uma recupera-
ção muito mais rápida de elementos comparáveis.
As árvores podem ser mais ou menos equilibradas. As árvores totalmente ba-
lanceadas fornecem o melhor desempenho, enquanto as árvores totalmente
desbalanceadas têm o mesmo desempenho que as listas.
O tamanho de uma árvore é o número de elementos que ela contém; sua al-
tura é o caminho mais longo na árvore.
A estrutura da árvore depende da ordem de inserção dos elementos da árvore.
As árvores podem ser percorridas em muitas ordens diferentes (pré-ordem,
em ordem, pós-ordem) e em ambas as direções (da esquerda para a direita ou
da direita para a esquerda).
As árvores podem ser facilmente mescladas sem atravessá-las.
As árvores podem ser mapeadas ou giradas, bem como dobradas de várias
maneiras.
As árvores podem ser balanceadas para melhor desempenho e para evitar es-
touro de pilha em operações recursivas.