Escolar Documentos
Profissional Documentos
Cultura Documentos
11 Solving Problems With Advanced Trees - The Joy of Kotlin
11 Solving Problems With Advanced Trees - The Joy of Kotlin
avançadas
Nissocapítulo
A árvore rubro-negra é uma árvore auto-equilibrada de uso geral com alto de-
sempenho. É adequado para uso geral e conjuntos de dados de qualquer
tamanho.
O heap da esquerda é uma árvore específica adequada para implementar filas
de prioridade.
Você também aprenderá como implementar mapas usando árvores que armaze-
nam tuplas de chave/valor. E você verá como criar filas de prioridade para ele-
mentos não comparáveis.
11.1Melhor desempenho e segurança de empilhamento com
árvores autobalanceadas
Com estruturas de dados imutáveis, uma nova estrutura é criada para cada mu-
dança. Conseqüentemente, você precisa definir um processo de balanceamento
que não envolva transformar a árvore em uma lista antes de reconstruir uma ár-
vore totalmente desbalanceada e finalmente balanceá-la. Duas maneiras de otimi-
zar esse processo são
Você poderia tentar inventar uma solução, mas outros já fizeram isso há muito
tempo. Um dos projetos de árvore de auto-equilíbrio mais eficientes é a árvore ru-
bro-negra. Esta estrutura foi inventada em 1978 por Guibas e Sedgewick. 1 Em
1999, Chris Okasaki publicou uma versão funcional do algoritmo da árvore rubro-
negra. 2 A descrição foi ilustrada por uma implementação em Standard ML e
uma implementação Haskell foi adicionada posteriormente. É esse algoritmo que
você implementará no Kotlin.
ored-black tree é uma árvore de busca binária com algumas adições à sua estru-
tura e um algoritmo de inserção modificado que também equilibra o resultado.
Infelizmente, Okasaki não descreveu a remoção, que é um processo muito mais
complexo. Mas em 2014, os autores Germane e Might descreveram esse método
ausente. 3
Em uma árvore rubro-negra, cada árvore (incluindo subárvores) possui uma pro-
priedade adicional que representa sua cor. Observe que pode ser qualquer cor ou
até mesmo qualquer propriedade que represente uma escolha binária. Além
disso, a estrutura é exatamente a mesma da árvore binária, conforme a listagem a
seguir.
pacote com.fpinkotlin.advancedtrees.listing01
import com.fpinkotlin.advancedtrees.listing01.Tree.Color.B ①
import com.fpinkotlin.advancedtrees.listing01.Tree.Color.R
importar kotlin.math.max
resumo interno
class Vazio<saída A: Comparável<@UnsafeVariance A>>:
Árvore<A>() { ④
objeto complementar {
operador
fun <A: Comparable<A>> invoke(): Tree<A> = E ⑨
// Vermelho
objeto interno R: Color() { ⑩
substituir fun toString(): String = "R"
}
// Preto
objeto interno B: Color() {
substituir fun toString(): String = "B"
}
}
}
③ As funções isTR e isTB testam se uma árvore é não vazia e vermelha ou não vazia
e preta, respectivamente.
④ Empty, uma classe abstrata, permite implementar funções nesta classe em vez de
usar correspondência de padrões na classe pai Tree.
⑤ Propriedades que não fazem sentido na classe Empty estão lançando exceções
preguiçosamente.
A contains função não foi representada, nem as outras funções como fold... ,
map , e assim por diante, porque não são diferentes das versões de árvore padrão.
Como você verá, apenas o plus e minus funções sãodiferente.
11.1.2 Adicionando um elemento à árvore rubro-negra
Uma árvore vazia é preta. (Isso não muda, então não há necessidade de veri-
ficá-lo.)
As subárvores esquerda e direita de uma árvore vermelha são pretas. Não é
possível encontrar dois vermelhos sucessivos ao descer a árvore.
Cada caminho da raiz para uma subárvore vazia tem o mesmo número de
pretos.
A Figura 11.5 ilustra a inserção do elemento 5. Como agora você tem dois elemen-
tos vermelhos sucessivos, a árvore deve ser balanceada tornando 3 o filho es-
querdo de 4 e 4 o filho direito de 2.
(TB (TR (TR axb) yc) zd) ➞ (TR (TB axb) y (TB czd))
(TB (TR ax (TR byc)) zd) ➞ (TR (TB axb) y (TB czd))
(TB ax (TR (TR byc) zd)) ➞ (TR (TB axb) y (TB czd))
(TB ax (TR por (TR czd))) ➞ (TR (TB axb) y (TB czd))
(cor T axb)➞ (cor T axb)
Cada expressão entre parênteses corresponde a uma árvore. A letra T indica uma
árvore não vazia e B e R indicam as cores preta e vermelha. Letras minúsculas
são espaços reservados para qualquer valor que possa ser válido no local corres-
pondente. Cada padrão esquerdo (aqueles à esquerda da seta, ➞) é aplicado em
ordem decrescente, o que significa que, se uma correspondência for encontrada, o
padrão direito correspondente será aplicado como a árvore resultante. Essa
forma de apresentar as coisas é semelhante à when expressão, com a última linha
sendo o caso padrão.
Exercício 11.1
Escreva uma add função protegida que executa uma adição regular de um ele-
mento e, em seguida, substitui as chamadas do construtor por chamadas para a
balance função. Em seguida, escreva a blacken funçãoe, finalmente, escreva a
plus funçãona classe pai, chamando blacken o resultado de add . Todas essas
funções devem ser privadas ou protegidas, exceto a plus função, que será
pública.
Solução
para a balance função, que pode ser implementado na Tree classe, os padrões
podem ser representados com when expressões, usando os aliases de tipo para
encurtar o código:
Cada when caso implementa um dos padrões listados na seção anterior (mostra-
dos como comentários). Se você quiser compará-los, provavelmente é muito mais
fácil fazê-lo em um editor de texto do que na página impressa.
a add funçãoé semelhante ao que você fez na árvore de pesquisa binária padrão,
com a exceção de que a balance funçãosubstitui o T construtor. A add função
pode ser implementada como uma função abstrata na Tree classecom implemen-
tações em Empty e T . Aqui está a implementação na Empty classe:
Nas aulas anteriores, você viu que não era possível implementar alguma função
no Empty singleton porque o A parâmetro não estava acessível (o singleton foi
parametrizado com Nothing ). Usando uma Empty classe abstrataparametrizado
A e estendido com o E<Nothing> objeto singleton, você pode implementar fun-
ções na Empty classe enquanto ainda tem um objeto singleton para representar
árvores vazias.
árvoresde números inteiros geralmente não são úteis (embora às vezes sejam).
Um uso importante das árvores de pesquisa binária são os mapas , também cha-
mados de dicionários ou matrizes associativas. Os mapas são coleções de pares
chave/valor que permitem a inserção, remoção e recuperação rápida de cada par.
Os mapas são familiares aos programadores e o Kotlin oferece várias implementa-
ções, entre as quais as mais comuns são os tipos Map e os . MutableMap Mas
o MutableMap não pode ser usado em um ambiente multiencadeado sem fornecer
alguns mecanismos de proteção que são difíceis de projetar e usar corretamente.
O Map tipo, por outro lado, está protegido contra este tipo de problemas. Mas não
é eficiente porque não depende do compartilhamento de dados, então um novo
mapa deve ser criado para cada inserção ou remoção.
Exercício 11.2
Dica
Você deve usar um delegado. A partir desse delegado, todas as funções podem ser
implementadas em uma linha de código. O único problema é escolher como você
armazenará os dados no mapa. Isso deve ser fácil. Você pode ter que mudar a
plus funçãotipo de argumento, e você pode preferir fazer o mesmo para o tipo
de retorno da get função. Você também precisará adicionar à Tree classeuma
get função para obter um elemento. Esta função terá a seguinte assinatura:
Esta função não retorna seu parâmetro, mas um Result do elemento igual a ele
se a árvore contiver tal elemento ou um vazio result caso contrário. Defina tam-
bém uma isEmpty funçãona Tree classe. Em seguida, defina uma
MapEntry classepara representar o par chave/valor e armazenar as instâncias
desse componente em uma árvore.
Solução
objeto complementar {
operador
fun <K: Comparable<K>, V> invoke(pair: Pair<K, V>): MapEntry<K, V> =
MapEntry(pair.first, Result(pair.second))
objeto complementar {
Por exemplo, pode ser necessário localizar o objeto com a chave máxima ou mí-
nima. Outra possível necessidade é dobrar o mapa, talvez para obter uma lista
dos valores contidos. Aqui está um exemplo de uma foldLeft função de
delegação:
Note que os folding maps geralmente ocorrem em casos de uso específicos que
merecem ser abstraídos dentro da Map classe.
Exercício 11.3
Escreva uma values funçãona Map classeque retorna uma lista dos valores con-
tidos no mapa em ordem crescente de chaves.
Dica
Você pode ter que criar uma nova função de dobra na Tree classee delegar a ele
da Map classe.
Solução
Você poderia inverter o resultado, mas isso não seria eficiente. Uma solução
muito melhor é adicionar uma foldInReverseOrder função à Tree classe. Re-
lembre a foldInOrder função:
substituir fun <B> foldInOrder(identity: B, f: (B) -> (A) -> (B) -> B): B =
f(esquerda.foldInOrder(identidade, f))(valor)(direita.foldInOrder(identidade, f))
o Map A classe é útil e relativamente eficiente, mas tem uma grande desvantagem
em comparação com os mapas aos quais você pode estar acostumado: as chaves
devem ser comparáveis. Embora os tipos usados para chaves sejam geralmente
comparáveis, como inteiros ou strings, e se você precisar usar um tipo não com-
parável para as chaves?
Exercício 11.4
Implemente uma versão Map que funcione com chaves não comparáveis.
Dica
Você precisa modificar duas coisas: primeiro, a MapEntry classedevem ser com-
paráveis, embora a chave não seja. Em segundo lugar, valores não iguais podem
ser mantidos em entradas de mapa iguais, portanto, as colisões devem ser resolvi-
das mantendo ambas as entradas em colisão.
Solução
Em segundo lugar, você deve usar uma implementação diferente para a compa-
reTo função. Uma possibilidade é comparar as entradas do mapa com base em
uma comparação de chave hash-código. Observe que você precisa alterar um
pouco o parâmetro de tipo da classe, fazendo com que a classe chave seja exten-
dida Any . Por padrão, ele estende Any? , um tipo anulável, então você precisa li-
dar com nulos na compareTo implementação. Pagar other.hashCode() pode-
ria, de outra forma, lançar um NullPointerException :
...
Em seguida, você deve lidar com as colisões que ocorrem quando duas entradas
de mapa têm chaves diferentes com o mesmo código hash. Nesses casos, você
deve manter os dois. A solução mais simples é armazenar as entradas do mapa
em uma lista. Para fazer isso, você deve modificar a Map classe. Primeiro, o dele-
gado da árvore (no construtor) terá um tipo modificado:
Observe que as funções values e foldLeft não compilam mais. Você pode cor-
rigir isso como um exercício adicional. Parece difícil, mas não é. Siga os tipos. Se
você tiver problemas, encontrará as soluções no código disponível no repositório
do GitHub ( https://github.com/pysaumont/fpinkotlin ).
Com essas modificações, a Map classepode ser usado com chaves não compará-
veis. Usar uma lista para armazenar as tuplas chave/valor pode não ser a imple-
mentação mais eficiente, pois a busca em uma lista leva tempo proporcional ao
número de elementos. Mas, na maioria dos casos, a lista conterá apenas um ele-
mento, portanto, a pesquisa retornará rapidamente. Pela mesma razão, não é útil
otimizar a busca pela primeira ocorrência correspondente aos key critérios da
get função. Se você quiser fazer isso, você pode usar uma dobra de escape (dobra
com um parâmetro “zero”) em vez de usar filter mais headSafe , que percor-
reria primeiro toda a lista antes de pegar o primeiro elemento. Se você não se
lembra como fazê-lo, consulte o exercício 8.13.
Comovocê sabe, uma fila é uma espécie de lista com um protocolo de acesso espe-
cífico. As filas podem ser de terminação única, como a lista encadeada individual-
mente que você usou com tanta frequência nos capítulos anteriores. Nesse caso, o
protocolo de acesso é o último a entrar, primeiro a sair (LIFO). Uma fila também
pode ser dupla, permitindo que oprotocolo de acesso primeiro a entrar, primeiro
a sair (FIFO). Mas também existem estruturas de dados com protocolos mais espe-
cializados. Entre eles está a fila de prioridade .
Como uma fila de prioridade contém elementos comparáveis, isso a torna uma
boa opção para uma estrutura semelhante a uma árvore. Mas, do ponto de vista
do usuário, a fila de prioridade é vista como uma lista com cabeça (o elemento
com maior prioridade, ou seja, o menor valor) e cauda (o resto dafila).
ofila de prioridade tem muitos casos de uso diferentes. Um que vem à mente rapi-
damente é a classificação. Você pode inserir elementos em uma fila de prioridade
em ordem aleatória e recuperá-loseles classificados. Esse não é o principal caso de
uso dessa estrutura, mas pode ser útil para classificar pequenos conjuntos de
dados.
Nesse cenário, a fila atua tanto como um buffer quanto como uma forma de reor-
denar os elementos. Isso geralmente implica variação limitada de tamanho, por-
que os elementos são removidos da fila mais ou menos na mesma velocidade em
que são inseridos. Isso é verdade se o consumidor consumir elementos aproxima-
damente no mesmo ritmo em que são produzidos pelos oito threads. Se não for
esse o caso, pode ser possível usar vários consumidores.
Paraatender aos requisitos de uma fila de prioridade, você usará o heap esquerdo
descrito por Okasaki. 5 Okasaki define o heap da esquerda como uma árvore or-
denada por heap com uma propriedade adicional da esquerda:
Uma árvore ordenada por heap é uma árvore na qual cada ramificação de um
elemento é maior ou igual ao próprio elemento. Isso garante que o elemento
mais baixo da árvore seja sempre o elemento raiz, tornando instantâneo o
acesso ao valor mais baixo.
Com a propriedade leftist, para cada elemento, o posto do ramo esquerdo é
maior ou igual ao posto do ramo direito.
A classificação de um elemento é o comprimento do caminho certo (também
chamado de coluna direita) para um elemento vazio. A propriedade leftist ga-
rante que o caminho mais curto de qualquer elemento para um elemento va-
zio seja o caminho certo. Uma consequência disso é que os elementos sempre
são encontrados em ordem crescente ao longo de qualquer caminho descen-
dente. A Figura 11.8 mostra um exemplo de árvore à esquerda.
Figura 11.8 Uma árvore esquerdista ordenada por heap, mostrando que cada ramo de um elemento é maior
ou igual ao próprio elemento e cada posto do ramo esquerdo é maior ou igual ao posto do ramo direito
correspondente.
oa classe principal do heap esquerdo é chamada Heap e deve ser uma implemen-
tação de árvore. A Listagem 11.4 mostra a estrutura básica. A principal diferença
das árvores que você desenvolveu até agora é que funções como right , left , e
head (equivalente ao que foi chamado value nos exemplos anteriores) retornam
a Result em vez de um valor bruto. Observe também que o rank é calculado pe-
los chamadores do construtor em vez do próprio construtor. Esta é uma escolha
de design desmotivada para mostrar outra maneira de fazer as coisas. Como os
construtores são privados, a diferença não vazará para fora da Heap classe.
objeto complementar {
Exercício 11.5
O requisito é que, se o valor for menor do que qualquer elemento no heap, ele
deve se tornar a raiz do novo heap. Caso contrário, a raiz do heap não deve mu-
dar. Além disso, os outros requisitos sobre classificação e comprimento do cami-
nho certo devem ser respeitados.
Dica
A função para criar um heap a partir de um único elemento é simples. Crie uma
nova árvore com classificação 1, o elemento de parâmetro como a cabeça e os
dois montes vazios como os ramos esquerdo e direito:
Criar um heap mesclando dois heaps é um pouco mais complicado. Para isso, você
precisará de uma função auxiliar adicional que crie um heap a partir de um ele-
mento e dois heaps:
protegido
divertido <A : Comparável<A>> merge(head: A,
primeiro: Heap<A>,
segundo: Heap<A>): Heap<A> =
quando {
first.rank >= second.rank -> H(second.rank + 1, first, head, second)
else -> H(first.rank + 1, second, head, first)
}
Se um dos heaps a serem mesclados estiver vazio, você retorna o outro. Caso con-
trário, você calcula o resultado da mesclagem. Com essas funções definidas, é fá-
cilpara criar o plus função:
Defina uma tail função que retorne o que resta após a remoção da cabeça. Essa
função, como a head função, retorna um Result para torná-la segura quando
for chamada em uma fila vazia. Aqui está sua assinatura na Heap classe pai:
Solução
Exercício 11.7
Você pode querer uma mensagem mais explícita quando nenhum elemento for
encontrado. Você não pode usar o valor de the index na Empty implementação
porque já está decrementado. Para superar isso, existem muitas soluções possí-
veis. Considere isso como um adicionalopcionalexercício.
Istoàs vezes será útil transformar a Heap em uma lista ordenada. Esta parece ser
uma tarefa fácil: basta retirar os elementos da pilha, um de cada vez, e adicioná-
los a uma lista. Mas este é um caso específico de uma operação de dobra mais
geral.
Exercício 11.8
Crie uma função pop que extraia um elemento do heap, retornando uma opção
de um par contendo a cabeça e o final do heap. Se a pilha estiver vazia, Opti-
on será None . Em seguida, escreva uma função criando uma lista classificada de
um heap.
Dica
Solução
Aqui está uma pop implementação possível. Primeiro defina uma função abstrata
na Heap classe:
Exercício 11.9
Dica
fun <A, S> desdobrar(z: S, getNext: (S) -> Opção<Par<A, S>>): List<A> {
tailrec fun desdobrar(acc: List<A>, z: S): List<A> {
val proximo = getPróximo(z)
retornar quando (próximo) {
é Option.None -> acc
é Option.Some ->
desdobrar(acc.cons(próximo.valor.primeiro), próximo.valor.segundo)
}
}
return desdobrar(Lista.Nil, z).reverse()
}
Solução
Para tornar esta função genérica, você precisa substituir todas as referências a
List<A> por B :
List.Nil deve ser substituído por uma B identidade, que deverá ser passada
para a função como um parâmetro adicional.
acc.cons(next.value.first) é a implementação de uma função do tipo
(List<A>) → A → (List<A>) que é usada para criar a lista. Em uma versão
genérica, a implementação dessa função é desconhecida em tempo de compila-
ção, portanto será passada como um parâmetro adicional.
A chamada para reverse antes de retornar a lista é específica List e deve
ser removida:
Parainserir elementos em uma fila de prioridade, você deve ser capaz de compa-
rar suas prioridades. Mas a prioridade nem sempre é uma propriedade dos ele-
mentos; nem todos os elementos implementam o Comparable interface. Os ele-
mentos que não implementam essa interface ainda podem ser comparados
usando um Comparator , então você pode fazer isso para a fila de prioridade?
Exercício 11.10
Modifique a Heap classepara que possa ser usado com Comparable elementos ou
com um arquivo Comparator .
Solução
com
Então você deve alterar a declaração das subclasses de acordo e adicionar uma
propriedade à Heap classepor segurar o Comparator . Como o comparador é op-
cional, essa propriedade conterá um Result<Comparator> potencialmente va-
zio. Aqui está a propriedade abstrata na Heap classe:
Você fará o mesmo na H classe, exceto que usará o construtor existente (se hou-
ver) para o valor padrão:
Esta função executa uma conversão de um de seus argumentos, mas você sabe
que não há risco de ClassCastException sendo jogado. Isso porque você garan-
tiu que nenhum heap poderia ser criado sem um comparador se o parâmetro de
tipo não estendesse Comparable . a plus funçãotambém deve ser modificado da
seguinte forma:
Resumo
As árvores podem ser balanceadas para melhor desempenho e para evitar es-
touro de pilha em operações recursivas.
A árvore rubro-negra é uma estrutura de árvore de auto-equilíbrio que o li-
berta de se preocupar com o equilíbrio da árvore.
Os mapas podem ser implementados delegando a uma árvore que armazena
tuplas de chave/valor.
Mapas com chaves não comparáveis devem lidar com colisões para armazenar
elementos com a mesma representação de chave.
As filas de prioridade são estruturas que permitem que os elementos sejam re-
cuperados por ordem de prioridade.
As filas de prioridade podem ser implementadas usando um heap à esquerda,
que é uma árvore binária ordenada por heap.
Filas de prioridade de elementos não comparáveis podem ser construídas
usando um comparador adicional.
1
Leo J. Guibas e Robert Sedgewick, “Uma estrutura dicromática para árvores ba-
lanceadas,” Foundations of Computer Science (1978),
http://www.computer.org/csdl/proceedings/focs/1978/5428/00/542800008- abs.html
.
2
Chris Okasaki, Estruturas de Dados Puramente Funcionais (Cambridge University
Press, 1999).
3 Kimball Germane e Matthew Might, “Functional Pearl: Deletion, The curse of the
red-black tree,” JFP 24, 4 (2014): 423–433;
http://matt.might.net/papers/germane2014deletion.pdf .
4
Kimball Germane e Matthew Might, “O método que falta: Excluindo das árvores
rubro-negras de Okasaki” ( http://matt.might.net/articles/red-black-delete/ ).