Você está na página 1de 73

10Mais manipulação de dados com árvores

Nissocapítulo

Noções básicas sobre tamanho, altura e profundidade em uma estrutura de


árvore
Entendendo a ordem de inserção na árvore de pesquisa binária
Atravessando árvores em várias ordens
Implementando a árvore de busca binária
Mesclando, dobrando e equilibrando árvores

No capítulo 5, você aprendeu sobre a lista encadeada individualmente, que prova-


velmente é a estrutura de dados imutável mais amplamente usada. Embora a lista
seja uma estrutura de dados eficiente para muitas operações, ela possui algumas
limitações. A principal deficiência é que a complexidade de acessar os elementos
cresce proporcionalmente com o número de elementos. Por exemplo, a procura
de um determinado elemento pode exigir o exame de todos os elementos, caso o
elemento procurado seja o último da lista. Entre outras operações menos eficien-
tes estão a classificação, o acesso a elementos por seu índice e a localização do ele-
mento máximo ou mínimo. Por exemplo, para encontrar o elemento máximo (ou
mínimo) em uma lista, é preciso percorrer toda a lista. Neste capítulo, você apren-
derá sobre uma nova estrutura de dados que resolve esses problemas: a árvore
binária.
Este capítulo começa com alguma teoria sobre árvores binárias. Alguns leitores
podem pensar que as árvores binárias são um assunto bem conhecido que todos
os programadores dominariam. Se é você e já domina as árvores binárias, pode
pular direto para os exercícios, que provavelmente parecerão fáceis. Para o resto
de vocês, estejam preparados para exercícios um pouco mais difíceis do que os
dos capítulos anteriores. Se não conseguir encontrar a resposta de um exercício,
você pode ver a solução. Mas eu recomendo que você retorne ao exercício mais
tarde e tente resolvê-lo novamente. Lembre-se de que os exercícios geralmente se
baseiam nos anteriores; portanto, se você não entender a solução de um exercí-
cio, provavelmente terá dificuldade em resolver os exercícios seguintes.

10.1 A árvore binária

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:

Um único elemento, como 56.2, "hi" , 42 , e -2 na figura 10.1


Um elemento com uma ramificação (direita ou esquerda) como T na figura
10.1
Um elemento com duas ramificações (direita e esquerda), como a , 1 ou $

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é.

10.2 Compreendendo árvores balanceadas e desbalanceadas

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.

Nesta figura, a árvore à direita é, na verdade, uma lista encadeada individual-


mente. Uma lista encadeada isoladamente pode ser vista como um caso especial
de uma lista totalmente desbalanceada.árvore.

10.3 Observando o tamanho, altura e profundidade das


árvores

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:

O número de elementos é chamado de tamanho .


O número de níveis, sem contar a raiz, é chamado de altura .

Na figura 10.2 , todas as três árvores têm tamanho 7. A primeira (perfeitamente


balanceada) tem altura 2, a segunda (imperfeitamente balanceada) tem altura 3 e
a terceira (desbalanceada) tem altura 6.

A palavra altura também é usada para caracterizar elementos individuais. Refere-


se ao comprimento do caminho mais longo de um elemento para uma folha. A al-
tura da raiz é a altura da árvore, e a altura de um elemento é a altura da subár-
vore que tem esse elemento como raiz.

A profundidade de um elemento é o comprimento do caminho desde a raiz até o


elemento. O primeiro elemento, também chamado de raiz , tem profundidade 0.
Na árvore perfeitamente balanceada da figura 10.2 , os elementos 2 e 6 têm pro-
fundidade 1, e os elementos 1, 3, 5 e 7 têm profundidade de2.
10.4 Árvores vazias e a definição recursiva

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

uma árvore vazia


Um elemento raiz com dois galhos que são eles próprios árvores

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.

10.5 Árvores frondosas

Binárioas árvores às vezes são representadas de maneira diferente, conforme


mostrado na figura 10.3 . Nesta representação, uma árvore é representada por ra-
mos que não possuem valores. Somente os nós terminais contêm valores. Como os
nós terminais são chamados de folhas , essas árvores são chamadas de árvores
frondosas .
Figura 10.3 Uma árvore frondosa contém valores apenas nos nós terminais, que são chamados de folhas.

A representação da árvore frondosa às vezes é preferida porque facilita a imple-


mentação de algumas funções. Neste livro, porém, considerarei apenas as árvores
clássicas, não as árvores frondosas.árvores.

10.6Árvores binárias ordenadas ou árvores binárias de busca

Umárvore binária ordenada, também chamada de Árvore Binária de Busca , ou


simplesmente BST, tem as seguintes características:

Contém elementos que podem ser ordenados.


Todos os elementos em uma ramificação têm um valor menor que o elemento
raiz.
Todos os elementos na outra ramificação têm um valor maior que o elemento
raiz.
A mesma condição vale para todas as subárvores.
Por convenção, os elementos com valores menores que a raiz estão no ramo es-
querdo e os elementos com valores maiores no ramo direito. A Figura 10.4 mostra
um exemplo de árvore ordenada.

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.

NOTA Uma consequência importante da definição de árvores binárias ordenadas


é que elas nunca podem conterduplicatas.

Árvores ordenadas são particularmente interessantes porque permitem a recupe-


ração rápida de elementos. Para descobrir se um elemento está contido na árvore,
você
1. Compare o elemento procurado com a raiz. Se forem iguais, está feito.
2. Se o elemento procurado tiver um valor menor que a raiz, prossiga recursiva-
mente com o ramo esquerdo.
3. Se o elemento procurado tiver um valor maior que a raiz, prossiga recursiva-
mente com o ramo direito.

Comparado a uma pesquisa em uma lista encadeada individualmente, você pode


ver que a pesquisa em uma árvore binária ordenada e perfeitamente balanceada
leva um tempo proporcional à altura da árvore binária.árvore. Isso significa que
uma busca leva um tempo proporcional a log2( n ), sendo n o tamanho (número
de elementos) da árvore. Por outro lado, o tempo de pesquisa em uma lista enca-
deada individualmente é proporcional ao número de elementos.

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.

A realidade às vezes é diferente. Se os objetos inseridos na árvore puderem ser


iguais do ponto de vista de ordenação da árvore, mas diferentes com base em ou-
tros critérios, você provavelmente desejará substituir a raiz pelo elemento que
está inserindo. Este é o caso mais frequente, como você verá em breve:

Se o elemento a ser inserido for inferior à raiz, insira-o recursivamente no


ramo esquerdo.
Se o elemento a ser inserido for maior que a raiz, insira-o recursivamente no
ramo direito.

Esse processo leva a uma observação interessante: o equilíbrio da árvore depende


da ordem em que os elementos são inseridos. É óbvio que inserir elementos orde-
nados produz uma árvore totalmente desbalanceada. Por outro lado, muitas or-
dens de inserção produzem árvores idênticas. A Figura 10.5 mostra as possíveis
ordens de inserção que podem resultar na mesma árvore.
Figura 10.5 Diferentes ordens de inserção podem produzir a mesma árvore. Um conjunto de dez elementos
pode ser inserido em uma árvore em 3.628.800 ordens distintas, mas isso produz apenas 16.796 árvores dis-
tintas. Essas árvores variam de perfeitamente equilibradas a totalmente desequilibradas. As árvores ordena-
das são eficientes para armazenar e recuperar dados aleatórios, mas não são boas para armazenar e recupe-
rar dados pré-ordenados.

A parte direita da figura tenta representar toda a ordem possível deinserções:

O primeiro elemento a ser inserido é 3. Inserir qualquer outro elemento pri-


meiro resulta em uma árvore diferente.
O segundo elemento deve ser 1 ou 8. Mais uma vez, inserir qualquer outro se-
gundo elemento resulta em uma árvore diferente. Se 1 for inserido, o próximo
elemento inserido deve ser 0, 2 ou 8. Se 8 for inserido, o próximo elemento in-
serido deve ser 6, 10 ou 1. Depois que 6 for inserido, o próximo elemento pode
ser 0, 2, 10 (desde que ainda não tenham sido inseridos), 5 ou 7. E assim por
diante....
10.8 Ordem de travessia de árvore recursiva e não recursiva

Dadouma árvore específica conforme representada na figura 10.5 , um caso de


uso comum é percorrê-la, visitando todos os elementos um após o outro. Esse é
normalmente o caso ao mapear ou dobrar árvores e, em menor grau, ao pesqui-
sar a árvore por um valor específico. Ao estudar as listas no capítulo 5, você
aprendeu que há duas maneiras de percorrê-las: da esquerda para a direita ou da
direita para a esquerda. As árvores oferecem muito mais abordagens e entre elas
faremos uma distinção entre recursivas e não recursivas.

10.8.1 Percorrendo árvores recursivas

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, +)

Aplicadas recursivamente a toda a árvore, essas ordens resultam em percorrer a


árvore dando prioridade à altura, levando aos caminhos de travessia mostrados
na figura 10.6 . Esse tipo de percurso é geralmente chamado de profundidade pri-
meiro , em vez do mais lógicoaltura primeiro . Ao falar sobre a árvore inteira, al-
tura e profundidade referem-se à altura da raiz e à profundidade da folha mais
profunda, conforme você aprendeu na seção 10.3. Esses dois valores são iguais.

10.8.2 Ordens de travessia não recursivas

Outra maneira de atravessar uma árvore é primeiro visitar um nível completo e


depois ir para o próximo nível. Novamente, isso pode ser feito da esquerda para a
direita ou da direita para a esquerda. Esse tipo de travessia é chamado de pri-
meiro nível . (Quando se trata de procurar um elemento em vez de percorrer a ár-
vore, geralmente échamada de pesquisa em largura .) Um exemplo de travessia de
primeiro nível é mostrado emfigura 10.7 .
Figura 10.7 A ordem de travessia de primeiro nível consiste em visitar todos os elementos de um determi-
nado nível antes de ir para o próximo nível.

10.9 Implementação da árvore de busca binária

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

classe selada Tree<out A: Comparable<@UnsafeVariance A>> { ②

diversão abstrata isEmpty(): Booleano

objeto interno Vazio : Árvore<Nada>() { ③

substituir fun isEmpty(): Boolean = true

override fun toString(): String = "E" ④


}

classe interna T<saída A: Comparable<@UnsafeVariance A>>(


val interna esquerda: Tree<A>, ⑥
valor val interno: A,
val interna à direita: Tree<A>): Tree<A>() {

substituir fun isEmpty(): Boolean = false

substituir fun toString(): String =


"(T $esquerda $valor $direita)" ④
}

objeto complementar {

operador divertido <A: Comparável<A>> invocar(): Árvore<A> =


Vazio ⑧
}
}
① A classe Tree é parametrizada e o tipo de parâmetro deve ser Comparable.

② A classe Tree é covariante em A, mas a interface Comparable é contravariante, en-


tão você deve lidar com isso.

③ A árvore vazia é representada por um objeto singleton parametrizado com


Nothing.

④ As funções toString são implementações mínimas para ajudar a ver o conteúdo de


uma árvore.

⑤ A subclasse T representa uma árvore não vazia.

⑥ Todas as propriedades são internas para que não possam ser acessadas direta-
mente de outros módulos.

⑧ A função de chamada retorna o singleton vazio.

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.

10.9.1 Entendendo a variância e as árvores

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.

O problema vem do fato de que o Tree parâmetro de tipo deve implementar a


Comparable interface. Esta não é a interface Java Comparable , mas uma inter-
face Kotlin específica:

public interface Comparável<em T> {


operador público fun compareTo(outro: T): Int
}

Como você pode ver, Comparable é contravariante em T , o que significa que o


parâmetro de tipo T ocorre apenas na in posição. Isso normalmente tornaria im-
possível tornar a Tree classecovariante. Você poderia lidar com isso, mas o força-
ria a lançar explicitamente o Empty objeto singleton em Tree<T> cada vez que
precisasse usá-lo. Você ainda seria capaz de transformar essa conversão em uma
função como

operador divertido <A: Comparável<A>> invocar() = Vazio como Árvore<A>

Você receberia um aviso do compilador sobre a conversão não verificada. Qual


solução usar depende de você, mas o uso de @UnsafeVariance é mais limpo e,
como permite fazer Tree covariante, também é mais útil.

Agora você precisa de uma maneira de construir árvores adicionando elementos


a uma árvore existente, começando com a vazia.
Exercício 10.1

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:

operador divertido plus(elemento: @UnsafeVariance A): Tree<A>

Se você preferir definir uma função abstrata na Tree classe pai e implementá-la
em cada subclasse, bem... apenas tente.

Solução

A função usa correspondência de padrões para selecionar uma implementação


com base no tipo de this árvore. Se for Empty , retorna uma árvore nova T (a
árvore não vazia) construída a partir do elemento a ser adicionado como valor e
duas árvores vazias como ramos. Se a árvore de parâmetros não estiver vazia,
três casos são possíveis:
O elemento a adicionar é inferior à raiz. Neste caso, uma nova árvore é cons-
truída com a mesma raiz e o mesmo ramo direito, sendo que o novo ramo es-
querdo é o resultado da inserção do elemento no ramo esquerdo original.
O elemento a adicionar é superior à raiz. Neste caso, uma nova árvore é cons-
truída com a mesma raiz e o mesmo ramo esquerdo, sendo que o novo ramo
direito é o resultado da inserção do elemento no ramo direito original.
O elemento a somar é igual à raiz. Neste caso, a função retorna uma nova ár-
vore construída com o elemento a ser adicionado como raiz e os dois ramos
originais:

operador fun plus(elemento: @UnsafeVariance A): Tree<A> = when (this) {


Vazio -> T(Vazio, elemento, Vazio)
é T -> quando {
elemento < this.value -> T(esquerda + elemento, this.value, direita)
elemento > this.value -> T(esquerda, this.value, direita + elemento)
else -> T(this.left, elemento, this.right)
}
}

Isso é diferente do que acontece em muitas implementações (o Java TreeSet ,


por exemplo) que não mudam se você tentar inserir um elemento igual a um ele-
mento já existente no conjunto. Embora esse comportamento possa ser aceitável
para elementos mutáveis, é muito menos aceitável quando os elementos são imu-
táveis. Você pode pensar que é uma perda de tempo e espaço de memória cons-
truir uma nova instância de T com o mesmo ramo esquerdo, o mesmo ramo di-
reito e uma raiz igual à raiz atual porque você pode retornar this . Retornar
this seria equivalente a retornar
T(esta.esquerda, este.valor, esta.direita)

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.

10.9.2 E quanto a uma função abstrata na classe Tree?

Definindouma função abstrata na Tree classe pai com diferentes implementa-


ções em cada subclasse pode ser vista como uma opção melhor por programado-
res orientados a objetos. Mas não vai funcionar. Veja como seria a implementação
no Empty objeto:

override fun <A: Comparable<A>> plus(element: Nothing): Tree<Nothing> =


T(vazio, elemento, vazio)

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

KotlinGenericNamepermite sobrecarga do operador. Ao usar a palavra- opera-


tor chavee nomeando a função plus , você pode chamar a operadora usando o
+ sinal:

val árvore = Árvore<Int>() + 5 + 2 + 8

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:

val tree: Tree<Int> = Tree() + 5 + 2 + 8

10.9.4 Recursão em árvores

Vocêpode estar se perguntando se você deve implementar recursão segura para a


pilha para a função auxiliar porque a plus funçãoé recursivo. Como eu disse an-
teriormente, não há necessidade de fazer isso com uma árvore balanceada por-
que a altura (que determina o número máximo de etapas de recursão) geralmente
é muito menor que o tamanho. Mas você viu que nem sempre é assim, principal-
mente se os elementos a serem inseridos estiverem ordenados. Isso poderia even-
tualmente resultar em uma árvore com apenas um galho, que teria sua altura
igual ao seu tamanho (menos um) e transbordaria a pilha.

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

A solução usando a vararg é semelhante à que você usou para List :

operador divertido <A: Comparável<A>> invocar(vararg az: A): Árvore<A> =


az.foldRight(Vazio, { a: A, árvore: Árvore<A> -> árvore.mais(a) })

Para fazer o mesmo para uma lista Kotlin, você precisa alterar o tipo do argu-
mento sem alterar nada na implementação:

operador divertido <A: Comparável<A>> invocar(az: Lista<A>): Árvore<A> =


az.foldRight(Vazio, { a: A, árvore: Árvore<A> -> árvore.mais(a) })

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 :

operador divertido <A: Comparável<A>> invocar(lista: Lista<A>): Árvore<A> =


list.foldLeft(Empty as Tree<A>, { tree: Tree<A> ->
{ a: A ->
árvore.mais(a)
}
})

Usar foldLeft e foldRight produz árvores diferentes porque a ordem de in-


serção dos elementos não será a mesma.

Exercício 10.3

Uma operação muito utilizada em árvores consiste em verificar se um elemento


específico está presente na árvore. Implemente uma contains função que exe-
cute essa verificação. Aqui está sua assinatura:

fun contém(a: @UnsafeVariance A): Booleano

Solução

O que você precisa fazer é comparar o parâmetro com a árvore value (o valor na
raiz da árvore):

Se o parâmetro for menor, aplique recursivamente a comparação ao ramo


esquerdo.
Se for maior, aplique recursivamente a comparação ao ramo direito.
Se o value e o parâmetro forem iguais, retorne true .

Como de costume, você definiria esta função na Tree classe, usando a


@UnsafeVariance anotaçãono parâmetro para desabilitar a verificação de
variância:

fun contém(a: @UnsafeVariance A): Boolean = when (this) {


está vazio -> falso
é T -> quando {
a <valor -> esquerda.contém(a)
a > valor -> direito.contém(a)
senão -> valor == a
}
}

Você também pode ter resolvido o exercício com uma implementação como esta:

fun <A: Comparable<@UnsafeVariance A>> contém(a: A): Boolean =


quando isso) {
está vazio -> falso
é T -> a == valor || esquerda.contém(a) || direito.contém(a)
}

Isso é mais simples e perfeitamente correto, embora um pouco mais lento se a


pesquisa implicar olhar recursivamente para o ramo certo. Mas há outra dife-
rença mais importante: essa implementação permite testar elementos de um tipo
diferente do elemento da árvore. Isso ocorre porque o A parâmetro de tipo dessa
implementação não é o A parâmetro da Tree classe. Está sombreando o Tre-
e parâmetro de tipo. Isso fica muito mais claro se você renomear o parâmetro:

divertido <B: Comparável<@UnsafeVariance B>> contém(b: B): Booleano =


quando isso) {
está vazio -> falso
é T -> b == valor || esquerda.contém(b) || direito.contém(b)
}

Com esta implementação, você pode escrever algo como Tree(1, 2,


3).contains1("2") sem causar um erro de compilação. Se isso é melhor ou não,
depende de você.

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:

tamanho divertido abstrato (): Int


altura divertida abstrata(): Int

Você deve tentar encontrar uma solução melhor, como fez para o tamanho da
lista no capítulo 8.

Solução

A Empty implementação da função size retorna 0. E como eu disse anterior-


mente, a Empty implementação da height funçãoretorna -1. A implementação
da size função na T classe retorna 1 mais o tamanho de cada ramificação. A im-
plementação da height função retorna 1 mais o máximo height dos dois
ramos:

importar kotlin.math.max

...

substituir fun size(): Int = 1 + left.size() + right.size()


override fun height(): Int = 1 + max(left.height(), right.height())

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.

Mas essas funções são ineficientes. Em primeiro lugar, o comprimento e a altura


são calculados em cada chamada. Esses resultados devem ser memorizados. Mas
o pior é que essas funções podem causar um StackOverflowException se a ár-
vore for enorme e mal balanceada. Uma solução muito melhor é criar duas pro-
priedades abstratas na Tree classe pai:

tamanho do val abstrato: Int

altura do val abstrato: Int

Em seguida, você pode implementar essas propriedades em cada subclasse:


objeto interno Vazio : Árvore<Nada>() {

substituir o tamanho do val: Int = 0

substituir val altura: Int = -1

...

classe interna T<saída A: ...>(substituir val esquerda: Árvore<A>,


substituir valor val: A,
substituir val à direita: Tree<A>): Tree<A>() {

substituir val size: Int = 1 + left.size + right.size

substituir val height: Int = 1 + max(left.height, right.height)

...
}

Lembre-se de que a criação de propriedades públicas por padrão cria automatica-


mente funções de acesso. As únicas particularidades são que essas funções pos-
suem o mesmo nome da propriedade, e o getter não usa parênteses.

Exercício 10.5

Escreva max e min funções para calcular os valores máximos e mínimos contidos
em uma árvore.
Dica

Pense no que as funções devem retornar na Empty classe.

Solução

Em uma árvore vazia, não há valores mínimos ou máximos. A solução é retornar


a Result<A> e as Empty implementações devem retornar Result.empty() .

A implementação da T classe é um pouco complicada. Para a max função, a solu-


ção é retornar o max do ramo direito. Se o ramo direito não estiver vazio, será
uma chamada recursiva. Se o ramo direito estiver vazio, você receberá um ar-
quivo Result.Empty . Você então sabe que o valor máximo é o valor da árvore
atual, então você pode chamar a orElse funçãono valor de retorno da
right.max() função:

override fun max(): Result<A> = right.max().orElse { Result(value) }

Lembre-se que a orElse funçãoavalia seu argumento preguiçosamente, o que


significa que leva um, { () -> Result<A>> } mas a () -> parte pode ser omi-
tida. A min função é totalmentesimétrico:

override fun min(): Result<A> = left.min().orElse { Result(value) }


10.9.5 Removendo elementos de árvores

Diferentelistas encadeadas individualmente, as árvores permitem recuperar um


elemento específico, como você viu quando desenvolveu a contains função no
exercício 10.3. Isso também deve possibilitar a remoção de um elemento especí-
fico de uma árvore.

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:

Se a < this.value , remova de left .


Se a > this.value , remova de right .
Caso contrário, mescle os ramos esquerdo e direito, descartando a raiz e re-
torne o resultado.

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:

fun removeMerge(ta: Árvore<@UnsafeVariance A>): Árvore<A>

Desta vez, se this a árvore estiver vazia, a função retorna seu parâmetro. Caso
contrário, o seguinte algoritmo é usado:

Se ta estiver vazio, retorne this ( this não pode estar vazio).


Se ta.value < this.value , mesclar ta no ramo esquerdo.
Se ta.value > this.value , mesclar ta no ramo direito.

Aqui está a implementação:

fun removeMerge(ta: Tree<@UnsafeVariance A>): Tree<A> = when (this) {


Vazio -> ta
é T -> quando (ta) {
Vazio -> isto
é T -> quando {
ta.value < value -> T(left.removeMerge(ta), value, right)
else -> T(esquerda, valor, direita.removeMerge(ta))

}
}
}

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:

fun remove(a: @UnsafeVariance A): Tree<A> = when(this) {


Vazio -> isto
é T -> quando {
a < valor -> T(esquerda.remove(a), valor, direita)
a > valor -> T(esquerda, valor, direita.remove(a))
else -> left.removeMerge(right)
}
}

10.9.6Mesclando árvores arbitrárias

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 é

abstract fun merge(tree: Tree<@UnsafeVariance A>): Tree<A>

Solução

A Empty implementação retorna seu parâmetro:

override fun merge(tree: Tree<Nothing>): Tree<Nothing> = tree

A T implementação da subclasse usa o seguinte algoritmo, em que “ this ” signi-


fica a árvore na qual a função é definida:

Se a árvore de parâmetros estiver vazia, retorne this .


Se a raiz do parâmetro for maior que a this raiz, remova o ramo esquerdo da
árvore de parâmetros e mescle o resultado com this o ramo direito. Em se-
guida, mescle o resultado com o ramo esquerdo do parâmetro.
Se a raiz do parâmetro for inferior à this raiz, remova o ramo direito da ár-
vore de parâmetros e mescle o resultado com this o ramo esquerdo. Em se-
guida, mescle o resultado com o ramo direito do parâmetro.
Se a raiz do parâmetro for igual à this raiz, mescle o ramo esquerdo do parâ-
metro com this o ramo esquerdo e mescle o ramo direito do parâmetro com
this o ramo direito.
Aqui está a T implementação da subclasse:

override fun merge(tree: Tree<@UnsafeVariance A>): Tree<A> = when (tree) {


Vazio -> isto
é T -> quando {
árvore.valor > este.valor ->
T(esquerda, valor, direita.merge(T(vazio, árvore.valor, árvore.direita)))
.merge(árvore.esquerda)
tree.value < this.value ->
T(esquerda.merge(T(árvore.esquerda, árvore.valor, Vazio)), valor, direita)
.merge(árvore.direita)
senão ->
T(left.merge(tree.left), valor, right.merge(tree.right))
}
}

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:

T(left.merge(tree.left), tree.value, right.merge(tree.right))

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)

Figura 10.17 O resultado final após mesclar a última árvore vazia


Você pode ver nessas figuras que a fusão de duas árvores resulta em uma árvore
com um tamanho (número de elementos) que pode ser menor que a soma dos ta-
manhos das árvores originais porque os elementos duplicados são removidos
automaticamente.

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.

Sobre dobrar árvores

Dobrandouma árvore é semelhante a dobrar uma lista; consiste em transformar


uma árvore em um único valor. Por exemplo, em uma árvore de valores numéri-
cos, o cálculo da soma de todos os elementos pode ser representado por meio de
uma dobra. Mas dobrar uma árvore é muito mais complicado do que dobrar uma
lista.

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

Considerando apenas a ordem em que os elementos são processados, você pode


reconhecer o seguinte:

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:

(((1 + 3) + 2) + ((5 + 7) + 6)) + 4

(( 4 + 2) + ((5 + 7) + 6)) + 4 usado: 1, 3

( 6 + ((5 + 7) + 6)) + 4 usado: 1, 3, 2

( 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.

10.10.1 Dobragem com duas funções

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:

diversão abstrata <B> foldEsquerda(identidade: B,


f: (B) -> (A) -> B,
g: (B) -> (B) -> B): B

Solução

A implementação na Empty subclasse é direta. Ele retorna o identity elemento.


A T implementação da subclasse é um pouco mais difícil. O que você precisa fa-
zer é calcular recursivamente a dobra para cada ramificação e depois combinar
os resultados com a raiz. O problema é que cada dobra de ramificação retorna um
B , mas a raiz é um A , e você não tem função de A para B à sua disposição. A so-
lução para isso pode ser a seguinte:

Dobre recursivamente o ramo esquerdo e o ramo direito, dando dois B valo-


res.
Combine esses dois B valores com a g função e, em seguida, combine o resul-
tado com a raiz e retorne o resultado.
Esta poderia ser uma 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))

Simples? Não exatamente. O problema é que a g função é uma função de B para


B para B , então você pode facilmente trocar os argumentos:

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

fun main(args: Array<String>) {


val resultado = Árvore(4, 2, 6, 1, 3, 5, 7)
.foldLeft(List(),
{ list: List<Int> -> { a: Int -> list.cons(a) } })
{ x -> { y -> y.concat(x) } }
println(resultado)
}
produz o seguinte resultado com a primeira solução

[7, 5, 3, 1, 2, 4, 6, NIL]

e o seguinte resultado com a segunda solução:

[7, 5, 6, 3, 4, 1, 2, NIL]

Qual é o resultado certo? De fato, ambas as listas, embora em ordens diferentes,


representam a mesma árvore. A Figura 10.18 representa os dois casos.

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))

Como existem muitas ordens de passagem, existem muitas implementações possí-


veis que fornecem resultados diferentes com não comutativosoperações.

10.10.2 Dobragem com uma única função

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

Escreva três funções para dobrar uma árvore: foldInOrder , foldPreOrder,


e foldPostOrder . Aplicados à árvore da figura 10.18 , os elementos devem ser
processados ​da seguinte forma:

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

Aqui estão as assinaturas de função:


diversão abstrata <B> foldInOrder(identity: B, f: (B) -> (A) -> (B) -> B): B
diversão abstrata <B> foldPreOrder(identidade: B, f: (A) -> (B) -> (B) -> B): B
diversão abstrata <B> foldPostOrder(identity: B, f: (B) -> (B) -> (A) -> B): B

Solução

Todas Empty as implementações retornam identity . As implementações na


T classe são tãosegue:

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)

10.10.3 Escolhendo uma implementação de dobra

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:

list.foldRight(List()) { i -> { l -> l.cons(i) } }

Você também pode usar foldLeft , mas a função seria um pouco mais complexa:

list.foldLeft(List()) { l -> { i -> l.reverse().cons(i).reverse() } }

Isso não é surpreendente, se você observar a foldRight implementação. fol-


dRight poderia ser implementado usando foldLeft e reverse .

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.

Exercício 10.10 (difícil)

Crie uma função que combine duas árvores e uma raiz para criar uma nova ár-
vore. Sua assinatura no objeto companheiro será

operador divertido <A: Comparável<A>> invocar(esquerda: Árvore<A>,


a: A, direita: Árvore<A>): Árvore<A>
Esta função deve permitir que você reconstrua uma árvore idêntica à árvore ori-
ginal usando qualquer uma destas três funções de dobragem: foldPreOrder ,
foldInOrder , e foldPostOrder .

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.

Você também vai precisar de um adicionalfunção (chamada mapEmpty ) em Re-


sult , que retorna Success se Result for Empty e Failure caso contrário.
Você encontrará esta funçãona com.fpinkotlin.common.Result classe.

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

fun <A: Comparável<A>> lt(primeiro: A, segundo: A, terceiro: A): Boolean =


lt(primeiro, segundo) && lt(segundo, terceiro)
Então você pode definir a ordered função que implementa a comparação de
árvore:

divertido <A: Comparável<A>> ordenado(esquerda: Árvore<A>,


um: Um,
direita: Tree<A>): Booleano =
(left.max().flatMap { lMax ->
right.min().map { rMin ->
lt(lMax, a, rMin)
}
}.getOrElse(left.isEmpty() && right.isEmpty()) ||
esquerda.min()
.mapEmpty()
.flatMap {
right.min().map { rMin -> lt(a, rMin)
}
}.getOrElse(falso) ||
direita.min()
.mapEmpty()
.flatMap {
left.max().map { lMax -> lt(lMax, a)
}
}.getOrElse(falso))

O primeiro teste (antes do primeiro || operador) retorna true se ambas as árvo-


res não estiverem vazias e a esquerda max , a e a direita min estiverem ordena-
das. O segundo e terceiro testes lidam com os casos em que as árvores esquerda
ou direita estão vazias (mas não ambas). o Result.mapEmpty a função retorna
Success<Any> se Result for Empty , e uma falha caso contrário. Usando esta
função, escrevendo a invoke funçãoé simples:
operador
fun <A: Comparável<A>> invocar(esquerda: Árvore<A>,
a: A, direita: Árvore<A>): Árvore<A> =
quando {
ordenado(esquerda, a, direita) -> T(esquerda, a, direita)
ordenado(direita, a, esquerda) -> T(direita, a, esquerda)
else -> Árvore(a).merge(esquerda).merge(direita)
}

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 :

// resumo em Tree e esta implementação em T:


substituir fun <B> foldLeft(identidade: B, f: (B) -> (A) -> B): B =
toListPreOrderLeft().foldLeft(identity, f)

// 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.

Sobre o mapeamento de árvores

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

O uso de uma das funções de dobra facilita o processo.


Solução

Você tem várias implementações possíveis usando as várias funções de dobra.


Aqui está um exemplo que pode ser definido no Tree classe:

fun <B: Comparável<B>> map(f: (A) -> B): Árvore<B> =


foldInOrder(Empty) { t1: Tree<B> ->
{ i: A ->
{ t2: Árvore<B> ->
Árvore(t1, f(i), t2)
}
}
}

Sobre equilibrar árvores

ComoEu disse anteriormente que as árvores funcionam bem se forem balancea-


das, o que significa que todos os caminhos da raiz até um elemento folha têm
quase o mesmo comprimento. Em uma árvore perfeitamente balanceada, a dife-
rença de comprimentos não excederá 1, o que acontece se o nível mais profundo
não estiver cheio. (Apenas árvores perfeitamente balanceadas de tamanho 2^ n +
1 têm todos os caminhos desde a raiz até um elemento folha do mesmo
comprimento.)

O uso de árvores desbalanceadas pode levar a um desempenho ruim porque as


operações podem precisar de tempo proporcional ao tamanho da árvore em vez
de log2( tamanho ). Mais dramaticamente, árvores desbalanceadas podem causar
um estouro de pilha ao usar operações recursivas. Aqui estão duas maneiras de
evitar esse problema:

Equilibre as árvores desequilibradas


Use árvores de auto-equilíbrio

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.

10.12.1 Rotação de árvores

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

Escreva rotateRight e rotateLeft funções para girar uma árvore em ambas


as direções. Tenha cuidado para preservar a ordem dos ramos: os elementos es-
querdos sempre devem estar abaixo da raiz e os elementos direitos sempre de-
vem estar acima da raiz. Declare funções abstratas na classe pai. Deixe-os protegi-
dos, pois eles só serão usados ​dentro da Tree classe. Aqui estão as assinaturas na
classe pai:

diversão abstrata protegida rotateRight(): Tree<A>


diversão abstrata protegida rotateLeft(): Tree<A>

Solução

As Empty implementações retornam this . Na T aula, estes são os passos para a


rotação correta:

1. Teste o ramo esquerdo para ver se está vazio.


2. Se o ramo esquerdo estiver vazio, retorne this porque girar para a direita
consiste em promover o elemento esquerdo a raiz. (Você não pode promover
uma árvore vazia.)
3. Se o elemento esquerdo não estiver vazio, ele se tornará a raiz, então um novo
T será criado com left.value a raiz. O ramo esquerdo do elemento es-
querdo torna-se o ramo esquerdo da nova árvore. Para o ramo direito, cons-
trua uma nova árvore com a raiz original como raiz, o ramo direito da es-
querda original como o ramo esquerdo e o direito original como o ramo
direito.
A rotação à esquerda é simétrica:

override fun rotateRight(): Tree<A> = when (left) {


Vazio -> isto
é T -> T(esquerda.esquerda, esquerda.valor,
T(esquerda.direita, valor, direita))
}

override fun rotateLeft(): Tree<A> = when (right) {


Vazio -> isto
é T -> T(T(esquerda, valor, direita.esquerda),
direita.valor, direita.direita)
}

A explicação parece complexa, mas é simples. Compare o código com as figuras


10.19 e 10.20 para ver o que está acontecendo. Se você tentar girar uma árvore vá-
rias vezes, chegará a um ponto em que um galho está vazio e a árvore não poderá
mais ser girada na mesma direção.

Exercício 10.13

Para equilibrar a árvore, você também precisará de funções para transformar


uma árvore em uma lista ordenada. Escreva uma função para transformar uma
árvore em uma lista em ordem da direita para a esquerda (o que significa em or-
dem decrescente).

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:

fun toListInOrderRight(): List<A>

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):

substituir fun toListInOrderRight(): List<A> =


right.toListInOrderRight()
.concat(Lista(valor))
.concat(left.toListInOrderRight())

Infelizmente, esta função transborda a pilha se a árvore estiver mal balanceada.


Como você precisa desta função para balancear uma árvore, seria triste se ela não
pudesse funcionar com uma árvore desbalanceada.

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()) ②
}
}

① Adiciona a árvore à lista de acumuladores.

② Gira a árvore até que o ramo esquerdo esteja vazio

E aqui está a função principal da Tree classe:

fun toListInOrderRight(): List<A> = unBalanceRight(List(), this)

a unBalancedRight funçãogira a árvore para a direita até que o ramo esquerdo


esteja vazio. Em seguida, ele chama a si mesmo recursivamente para fazer a
mesma coisa para todas as subárvores certas, depois de adicionar o valor da ár-
vore à lista do acumulador. Eventualmente, o parâmetro da árvore é encontrado
vazio e a função retorna a listaacumulador.

10.12.2 Usando o algoritmo Day-Stout-Warren

oO algoritmo Day-Stout-Warren é um método simples para balancear eficiente-


mente as árvores de busca binária:
1. Transforme a árvore em uma árvore totalmente desequilibrada.
2. Em seguida, aplique rotações até que a árvore esteja totalmente equilibrada.

Transformar a árvore em totalmente desbalanceada é uma simples questão de fa-


zer uma lista em ordem e criar uma nova árvore a partir dela. Como você deseja
criar a árvore em ordem crescente, você terá que criar uma lista em ordem de-
crescente e depois começar a girar o resultado para a esquerda. Você pode esco-
lher o caso simétrico.

Aqui está o algoritmo para obter uma árvore totalmente balanceada:

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:

fun log2nlz(n: Int): Int = quando (n) {


0 -> 0
else -> 31 - Integer.numberOfLeadingZeros(n)
}

Aqui está o que o Javadoc para o numberOfLeadingZeros método diz:


Retorna o número de bits zero que precedem o bit um de ordem mais alta
(“mais à esquerda”) na representação binária de complemento de dois do
int valor especificado. Retorna 32 se o valor especificado não tiver um bit
em sua representação de complemento de dois, ou seja, se for igual a zero.

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

Implemente a balance funçãopara equilibrar totalmente qualquer árvore. Esta


função será definida no objeto companheiro e terá como parâmetro a árvore a ser
balanceada.

Dica

Essa implementação é baseada em várias funções auxiliares: uma função frontal


cria a árvore totalmente desbalanceada chamando a toListInOrderRight fun-
ção. A lista resultante é dobrada à esquerda em uma árvore (totalmente desbalan-
ceada), que será mais fácil de balancear.

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:

Usando correspondência de padrões, desde que a Result classee a Tree tur-


maestão no mesmo módulo (porque Result as subclasses são internal )
Ao adicionar uma isSuccess funçãona Result classe

Escolha a solução de sua preferência. copiei a Result aulaneste módulo de capí-


tulo (juntamente com outras classes necessárias). Uma implementação funcional
pode ser

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) }
retornar quando (x) {
é Resultado.Sucesso -> desdobrar(Pair(a.segundo, x), f)
senão -> um
}
}
return Result(a).let { desdobrar(Pair(it, it), f).second.getOrElse(a) }
}

Esta função échamado unfold por analogia para List.unfold ou


Stream.unfold . Ele faz o mesmo trabalho, exceto que o tipo de resultado da
função é o mesmo que seu tipo de entrada, mas esquece a maioria dos resultados,
mantendo apenas os dois últimos. Isso o torna mais rápido e usa menos memória.
Você também precisará definir funções internas para acessar o valor e os ramos
de uma árvore.

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:

fun <A : Comparable<A>> isUnBalanced(tree: Tree<A>): Boolean =


quando (árvore) {
Vazio -> falso
é T -> Math.abs(árvore.esquerda.altura - árvore.direita.altura) >
(árvore.tamanho - 1) % 2
}

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

val resumo interno esquerdo: Tree<A>

resumo interno val direito: Tree<A>

As Empty implementações lançam uma exceção quando acessadas. Isso porque


você está definindo propriedades e não funções. você não pode escrever

substituir valor val: Nothing =


throw IllegalStateException("Nenhum valor em Vazio")
substituir val à esquerda: Tree<Nothing> =
throw IllegalStateException("Nenhum espaço vazio")
substituir val à direita: Tree<Nothing> =
throw IllegalStateException("Sem direito em Vazio")

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:

substituir valor val: Nothing por preguiçoso {


throw IllegalStateException("Nenhum valor em Vazio")
}
substituir val à esquerda: Tree<Nothing> por lazy {
throw IllegalStateException("Nenhum espaço vazio")
}
substituir val à direita: Tree<Nothing> por lazy {
throw IllegalStateException("Sem direito em Vazio")
}

OBSERVAÇÃO É sua responsabilidade garantir que essas funções nunca sejam


chamadas.

Na T classe, você precisa modificar o construtor:

interno
class T<out A: Comparable<@UnsafeVariance A>>(substituir val left: Tree<A>,
substituir valor val: A,
substituir val à direita: Tree<A>):
Árvore<A>() {

Substituindo as propriedades no construtor, você está automaticamente dando a


elas a mesma visibilidade ( internal ) que as propriedades substituídas, e não a
visibilidade padrão ( public ). Agora você pode escrever o balanceamento
principalfunções:

fun <A: Comparável<A>> balance(árvore: Árvore<A>): Árvore<A> =


balanceHelper(tree.toListInOrderRight().foldLeft(Empty) {
t: Árvore<A> -> { a: A ->
T(vazio, a, t)
}
})

fun <A: Comparável<A>> balanceHelper(árvore: Árvore<A>): Árvore<A> = quando {


!tree.isEmpty() && tree.height > log2nlz(tree.size) -> quando {
Math.abs(árvore.esquerda.altura - árvore.direita.altura) > 1 ->
balanceHelper(balanceFirstLevel(árvore))
else -> T(balanceHelper(árvore.esquerda),
tree.value, balanceHelper(tree.right))
}
senão -> árvore
}

private fun <A: Comparable<A>> balanceFirstLevel(tree: Tree<A>): Tree<A> =


desdobrar(árvore) { t: Árvore<A> ->
quando {
isUnBalanced(t) -> quando {
tree.right.height > tree.left.height ->
Resultado(t.rotateLeft())
else -> Resultado(t.rotateRight())
}
senão -> Resultado()
}
}

10.12.3 Balanceamento automático de árvores

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

Transforme a árvore que você desenvolveu para torná-la balanceada automatica-


mente em inserções, fusões e remoções.

Solução

A solução óbvia é chamar balance após cada operação que modifica a árvore
conforme o código a seguir:

operador fun plus(a: @UnsafeVariance A): Tree<A> =


saldo(maisDesequilibrado(a))

diversão privada plusUnBalanced(a: @UnsafeVariance A): Tree<A> = plus(this, a)

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:

operador divertido plus(a: @UnsafeVariance A): Tree<A> {

fun plusUnBalanced(a: @UnsafeVariance A, t: Árvore<A>): Árvore<A> =


quando (t) {
Vazio -> T(Vazio, a, Vazio)
é T -> quando {
a < t.value -> T(plusUnBalanced(a, t.left), t.value, t.right)
a > t.value -> T(t.left, t.value, plusUnBalanced(a, t.right))
senão -> T(t.esquerda, a, t.direita)
}
}

return plusUnBalanced(a, this).let {


quando {
it.height > log2nlz(it.size) * 100 -> balance(it)
senão -> isso
}
}
}

O desempenho da solução de balanceamento pode parecer longe do ideal, mas é


um compromisso. Criar uma árvore de uma lista ordenada de 100.000 elementos
levaria 2,5 segundos e produziria uma árvore perfeitamente balanceada de altura
16. Substituir o valor 100 por 20 na plusUnBalanced funçãodobra o tempo sem
nenhum benefício, e substituí-lo por 1.000 multiplica oTempopor5.

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.

Você também pode gostar