Você está na página 1de 47

11Resolvendo problemas com árvores

avançadas

Nissocapítulo

Evitando o estouro de pilha com árvores de auto-balanceamento


Implementando a árvore rubro-negra
Implementando mapas
Implementando filas de prioridade

No capítulo anterior, você aprendeu sobre a estrutura da árvore binária e as ope-


rações básicas da árvore. Mas você viu que, para se beneficiar totalmente das ár-
vores, você deve ter casos de uso específicos, como lidar com dados ordenados
aleatoriamente ou um conjunto de dados limitado para evitar qualquer risco de
estouro de pilha. Tornar as árvores empilháveis ​é muito mais difícil do que era
para as listas porque cada etapa de computação envolve duas chamadas recursi-
vas. Isso torna impossível criar versões recursivas de cauda. Neste capítulo, você
aprenderá sobre duas árvores específicas:

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

oO algoritmo de balanceamento Day-Stout-Warren usado no capítulo anterior não


é ideal para balancear árvores imutáveis ​porque foi projetado para modificações
no local. Para escrever programas mais seguros, as modificações no local devem
ser evitadas sempre que possível.

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

Girar diretamente a árvore original (eliminando o processo de lista/árvore


desbalanceada)
Aceite uma certa quantidade de desequilíbrio

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.

Se você estiver interessado em estruturas de dados imutáveis, recomendo forte-


mente que compre e leia o livro de Okasaki. Você também pode ler sua tese de
1996 com o mesmo título. É muito menos completo que seu livro, mas está dispo-
nível para download gratuito ( http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf ).
11.1.1 Compreendendo a estrutura básica da árvore rubro-negra

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.

Listagem 11.1 A estrutura base da árvore rubro-negra

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

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

tamanho do val abstrato: Int

altura do val abstrato: Int ②

val abstrato interno cor: Cor

valor abstrato interno isTB: Booleano ③

valor abstrato interno isTR: Booleano


resumo interno val direito: Tree<A>

val resumo interno esquerdo: Tree<A>

valor de valor abstrato interno: A

resumo interno
class Vazio<saída A: Comparável<@UnsafeVariance A>>:
Árvore<A>() { ④

substituir val isTB: Boolean = false

substituir val isTR: Boolean = false

substituir val à direita: Tree<Nothing> por lazy { ⑤


throw IllegalStateException("direito chamado na árvore vazia")
}

substituir val à esquerda: Tree<Nothing> por lazy {


throw IllegalStateException("esquerda chamada na árvore vazia")
}

substituir valor val: Nothing por preguiçoso {


throw IllegalStateException("valor chamado na árvore vazia")
}

substituir val color: Color = B ⑥

substituir o tamanho do val: Int = 0

substituir val altura: Int = -1

substituir fun toString(): String = "E"


}

objeto interno E: Vazio<Nada>() ⑦


interno
classe T<saída A: Comparável<@UnsafeVariance A>>(
substituir val color: Cor, ⑧
substituir val à esquerda: Tree<A>,
substituir valor val: A,
substituir val à direita: Tree<A>): Tree<A>() {
substituir val isTB: Boolean = color == B

substituir val isTR: Boolean = color == R

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

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

override fun toString(): String = "(T $cor $esquerda $valor $direita)"


}

objeto complementar {

operador
fun <A: Comparable<A>> invoke(): Tree<A> = E ⑨

classe selada Cor {

// Vermelho
objeto interno R: Color() { ⑩
substituir fun toString(): String = "R"
}

// Preto
objeto interno B: Color() {
substituir fun toString(): String = "B"
}
}
}

① As cores são importadas para simplificar o código.

② As propriedades abstratas são definidas na classe pai.

③ 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 árvore vazia é preta.

⑦ A árvore vazia é representada pelo singleton E.

⑧ Árvores não vazias podem ser pretas ou vermelhas.

⑨ Esta função retorna uma árvore vazia.

⑩ Cores são objetos singleton.

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

ocaracterística principal de uma árvore rubro-negra são as invariantes que de-


vem ser verificadas. Ao modificar a árvore, ela será testada para verificar se esses
invariantes estão sendo quebrados e restaurá-los através de rotações e mudanças
de cores, se necessário. Esses invariantes são os seguintes:

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.

Adicionar um elemento a uma árvore rubro-negra é um processo um tanto com-


plexo que inclui a verificação dos invariantes após a inserção (e o rebalancea-
mento, se necessário). Aqui está o algoritmo correspondente:

Uma árvore vazia é sempre preta.


A inserção propriamente dita é feita exatamente como em uma árvore co-
mum, mas é seguida pelo balanceamento.
Inserir um elemento em uma árvore vazia produz uma árvore vermelha.
Após o balanceamento, a raiz é escurecida.

As Figuras 11.1 a 11.7 ilustram a inserção de inteiros de 1 a 7 em uma árvore inici-


almente vazia. Este é o pior caso possível, onde os elementos são inseridos em or-
dem. Se estiver usando uma árvore binária comum, isso resultaria em uma ár-
vore totalmente desbalanceada. A Figura 11.1 mostra a inserção do elemento 1 na
árvore vazia. Como você está inserindo em uma árvore vazia, a cor inicial é ver-
melha. Uma vez que o elemento é inserido, a raiz é escurecida.
Figura 11.1 Passo 1: inserção do inteiro 1 em uma árvore inicialmente vazia

A Figura 11.2 mostra a inserção do elemento 2. O elemento inserido é vermelho, a


raiz já é preta e ainda não há necessidade de balanceamento.

Figura 11.2 Passo 2: inserção do inteiro 2 em uma árvore inicialmente vazia

A Figura 11.3 ilustra a inserção do elemento 3. O elemento inserido é vermelho. A


árvore está sendo balanceada porque tem dois elementos vermelhos sucessivos.
Como o elemento vermelho agora tem dois filhos, eles se tornam pretos. (Filhos de
um elemento vermelho sempre devem ser pretos.) Eventualmente, a raiz fica
enegrecida.
Figura 11.3 Passo 3: inserção do inteiro 3 em uma árvore inicialmente vazia

A Figura 11.4 mostra a inserção do elemento 4. Nenhuma outra manipulação é


necessária.
Figura 11.4 Passo 4: inserção do inteiro 4 em uma árvore inicialmente vazia

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.

Figura 11.5 Passo 5: inserção do inteiro 5 em uma árvore inicialmente vazia


A Figura 11.6 mostra a inserção do elemento 6. Nenhuma outra manipulação é
necessária.

Figura 11.6 Passo 6: inserção do inteiro 6 em uma árvore inicialmente vazia

Na figura 11.7 , o elemento 7 é adicionado à árvore. Como os elementos 6 e 7 são


dois elementos vermelhos sucessivos, a árvore deve ser balanceada. O primeiro
passo é tornar o elemento 5 o filho esquerdo de 6 e o ​6 o filho direito de 4, o que
novamente deixa dois elementos vermelhos sucessivos: 4 e 6. A árvore é mais
uma vez equilibrada, tornando o elemento 4 a raiz, 2 o filho esquerdo de 4, e 3 o
filho direito de 2. O elemento 6 é escurecido porque todo caminho para uma su-
bárvore vazia deve ter o mesmo número de negros. A última operação consiste
em escurecer a raiz.
Figura 11.7 Passo 7: inserção do inteiro 7 em uma árvore inicialmente vazia

a balance funçãorecebe os mesmos argumentos do construtor de árvore: co-


lor , left , value e right . Esses quatro parâmetros são testados para vários
padrões e o resultado é construído de acordo. A balance função substitui o cons-
trutor de árvore. Você precisa modificar qualquer processo usando o construtor
para usar essa função. A lista a seguir mostra como cada padrão de argumentos é
transformado por esta função:

(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 os plus , balance e blacken funções para adicionar um elemento à ár-


vore rubro-negra. Aqui estão as assinaturas correspondentes:

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


fun balance(cor: Color, esquerda: Tree<A>, valor: A, direita: Tree<A>): Tree<A>
divertido <A: Comparável<A>> blacken(): Árvore<A>
Dica

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:

equilíbrio divertido protegido (cor: cor,


esquerda: Tree<@UnsafeVariance A>,
valor: @UnsafeVariance A,
direita: Árvore<@UnsafeVariance A>): Árvore<A> = quando {

// saldo B (TR (TR axb) yc) zd = TR (TB axb) y (TB czd)


color == B && left.isTR && left.left.isTR ->
T(R, esquerda.esquerda.blacken(), esquerda.valor, T(B, esquerda.direita, valor, direita)

// saldo B (TR ax (TR byc)) zd = TR (TB axb) y (TB czd)


color == B && left.isTR && left.right.isTR ->
T(R, T(B, esquerda.esquerda, esquerda.valor, esquerda.direita.esquerda), esquerda.direit
T(B, esquerda.direita.direita, valor, direita))

// saldo B ax (TR (TR byc) zd) = TR (TB axb) y (TB czd)


cor == B && direita.isTR && direita.esquerda.isTR ->
T(R, T(B, esquerda, valor, direita.esquerda.esquerda), direita.esquerda.valor,
T(B, direita.esquerda.direita, direita.valor, direita.direita))

// saldo B ax (TR by (TR czd)) = TR (TB axb) y (TB czd)


cor == B && direita.isTR && direita.direita.isTR ->
T(R, T(B, esquerda, valor, direita.esquerda), direita.valor, direita.direita.blacken())

// balancear cor axb = T cor axb


else -> T(cor, esquerda, valor, direita)
}

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:

override fun add(newVal: @UnsafeVariance A): Tree<A> = T(R, E, newVal, E)

E aqui está a T implementação:

override fun add(newVal: @UnsafeVariance A): Árvore<A> = quando {


newVal < value -> balance(color, left.add(newVal), value, right)
newVal > value -> balance(color, left, value, right.add(newVal))
senão -> quando (cor) {
B -> T(B, esquerda, newVal, direita)
R -> T(R, esquerda, newVal, direita)
}
}

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.

a blacken funçãotambém é implementado na Tree classecomo uma função abs-


trata com duas implementações. Aqui está a Empty implementação:

override fun blacken(): Tree<A> = E

Aqui está a T implementação:

override fun blacken(): Tree<A> = T(B, esquerda, valor, direita)

Finalmente, a plus funçãoé definido na Tree classecom a palavra- opera-


tor chave. Isso permite que você use o + operador para chamá-lo. Ele retorna o
resultado escurecido de add :

operador fun plus(valor: @UnsafeVariance A): Tree<A> = add(value).blacken()

11.1.3 Removendo elementos da árvore rubro-negra

Removendoum elemento de uma árvore rubro-negra é discutido pelos já conheci-


dos autores Germane e Might. 4   A implementação em Kotlin é muito longa para
ser incluída neste livro, mas está incluída no código que a acompanha (
http://github.com/pysaumont/fpinKotlin ). Ele será utilizado noPróximoexercício.

11.2 Um caso de uso para a árvore rubro-negra: Mapas

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

11.2.1 Mapa de Implementação

Funcionalárvores como a árvore rubro-negra que você desenvolveu têm a vanta-


gem da imutabilidade, o que permite usá-las em ambientes multiencadeados sem
se preocupar com bloqueios e sincronização. Isso também oferece benefícios de
bom desempenho porque a maioria dos dados é compartilhada entre a árvore an-
tiga e a nova árvore quando um elemento é adicionado ou removido. A Listagem
11.3 mostra a interface de a Map que pode ser implementada usando a árvore ru-
bro-negra.

Listagem 11.3 Um mapa funcional

class Map<out K: Comparable<@UnsafeVariance K>, V> {

operador fun plus(entrada: Pair<@UnsafeVariance K, V>): Map<K, V> = TODO()

operador fun menos(chave: @UnsafeVariance K): Map<K, V> = TODO()

fun contém (chave: @UnsafeVariance K): Boolean = TODO ()

fun get(chave: @UnsafeVariance K): Resultado<MapEntry<@UnsafeVariance K, V>>


= TODO()

fun isEmpty(): Boolean = TODO()

tamanho divertido(): Int = TODO("tamanho")


objeto complementar {
operador fun invoke(): Map<Nothing, Nothing> = Map()
}
}

Exercício 11.2

Complete a Map aulaimplementando todas as funções.

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:

fun operator get(elemento: @UnsafeVariance A): Resultado<A>

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

O MapEntry componente é semelhante a um Pair com esta importante dife-


rença: deve ser comparável e a comparação deve ser baseada no key . o equal-
s e hashCode as funções também serão baseadas em códigos de hash e igualdade
de chave. Aqui está uma implementação possível:

classe MapEntry<K: Comparable<@UnsafeVariance K>, V>

construtor privado (chave val privada: K, valor val: Resultado<V>):


Comparável<MapEntry<K, V>> {

override fun compareTo(other: MapEntry<K, V>): Int =


this.key.compareTo(other.key)

override fun toString(): String = "MapEntry($key, $value)"

substituir diversão é igual a (outro: Qualquer?): Boolean =


este === outro || quando (outro) {
é MapEntry<*, *> -> chave == outra.chave
senão -> falso
}

substituir fun hashCode(): Int = key.hashCode()

objeto complementar {

fun <K: Comparable<K>, V> of(chave: K, valor: V): MapEntry<K, V> =


MapEntry(chave, Resultado(valor))

operador
fun <K: Comparable<K>, V> invoke(pair: Pair<K, V>): MapEntry<K, V> =
MapEntry(pair.first, Result(pair.second))

operador divertido <K: Comparável<K>, V> invocar(chave: K): MapEntry<K, V> =


MapEntry(chave, Resultado())
}
}
Implementar o Map componente é agora uma questão de delegar todas as opera-
ções paraa Tree<MapEntry<Key, Value>> . Aqui está uma implementação
possível:

class Map<out K: Comparable<@UnsafeVariance K>, V>(


delegado de val privado: Tree<MapEntry<@UnsafeVariance K, V>> =
Árvore()) {

operador divertido plus(entrada: Pair<@UnsafeVariance K, V>): Map<K, V> =


Map(delegado + MapEntry(entrada))

operador fun menos(chave: @UnsafeVariance K): Map<K, V> =


Mapa(delegado - MapEntry(chave))

fun contém (chave: @UnsafeVariance K): Boolean =


delegado.contains(MapEntry(chave))

operador divertido get(chave: @UnsafeVariance K):


Result<MapEntry<@UnsafeVariance K, V>> =
delegado[MapEntry(chave)]

fun isEmpty(): Boolean = delegado.isEmpty

tamanho divertido() = delegado.tamanho

substitui diversão toString() = delegado.toString()

objeto complementar {

operador fun <K: Comparable<@UnsafeVariance K>, V> invoke():


Mapa<K, V> = Mapa()
}
}
11.2.2 Estendendo mapas

Nãotodas as operações de árvore foram delegadas porque algumas operações não


fazem muito sentido nas condições atuais. Você pode precisar de operações adici-
onais em alguns casos de uso especiais. A implementação dessas operações é fácil:
estenda a Map classee adicionar funções de delegação.

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:

fun <B> foldLeft(identidade: B, f: (B) ->


(MapEntry<@UnsafeVariance K, V>) -> B, g: (B) -> (B) -> B): B =
delegado.foldLeft(identidade, { b ->
{ eu: MapEntry<K, V> ->
f(b)(eu)
}
}, g)

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

Existem várias implementações possíveis da values função. Seria possível dele-


gar para a foldInOrder função, mas essa função itera sobre os valores da árvore
em ordem crescente. Usar esta função para construir uma lista resultaria em uma
lista em ordem decrescente.

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

Tudo o que você precisa fazer é inverter a ordem:

substituir fun <B> foldInReverseOrder(identidade: B,


f: (B) -> (A) -> (B) -> B): B =
f(direita.foldInReverseOrder(identidade, f))(valor)(esquerda
.foldInReverseOrder(identidade, f))

Como de costume, a Empty implementação retorna identity . Agora você pode


delegar a esta função de dentro do Map classe:

valores divertidos(): List<V> =


sequence(delegate.foldInReverseOrder(List<Result<V>>()) { lst1 ->
{ eu ->
{ lst2 ->
lst2.concat(lst1.cons(me.value))
}
}
}).getOrElse(List())
11.2.3Usando Mapa com chaves não comparáveis

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

A primeira coisa a fazer é modificar a MapEntry classeremovendo a necessidade


de a chave ser comparável:

class MapEntry<K: Any, V> private constructor(val key: K,


valor val: Resultado<V>):
Comparável<MapEntry<K, V>> {

Note que a MapEntry classeainda é comparável, embora o K tipo não seja.

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 :

class MapEntry<K: Any, V> private constructor(val key: K,


valor val: Resultado<V>):
Comparável<MapEntry<K, V>> {

override fun compareTo(other: MapEntry<K, V>): Int =


hashCode().compareTo(other.hashCode())

...

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:

private val delegado: Tree<MapEntry<Int, List<Pair<K, V>>>> = Tree()

Em seguida, você precisará de uma função para recuperar a lista de tuplas de


chave/valor correspondentes ao mesmo código hash de chave:

private fun getAll(key: @UnsafeVariance K): Result<List<Pair<K, V>>> =


delegado[MapEntry(key.hashCode())]
.flatMap { x ->
x.value.map { lt ->
lt.map { t -> t }
}
}
Você pode então definir as funções plus , contains , minus e get em termos da
getAll função. Aqui está a add função:

operador divertido plus(entrada: Pair<@UnsafeVariance K, V>): Map<K, V> {


val lista = getAll(entry.first).map { lt ->
lt.foldLeft(List(entry)) { lst ->
{ par ->
if (pair.first == entry.first) lst else lst.cons(pair)
}
}
}.getOrElse { Lista(entrada) }
return Map(delegate + MapEntry.of(entry.first.hashCode(), list))
}

Aqui está a minus função:

operador fun menos(chave: @UnsafeVariance K): Map<K, V> {


val lista = getAll(key).map { lt ->
lt.foldLeft(List()) { lst: List<Pair<K, V>> ->
{ par ->
if (pair.first == key) lst else lst.cons(pair)
}
}
}.getOrElse { Lista() }
retornar quando {
list.isEmpty() -> Map(delegate - MapEntry(key.hashCode()))
else -> Map(delegate + MapEntry.of(key.hashCode(), list))
}
}

Aqui está a contains função:

fun contém (chave: @UnsafeVariance K): Boolean =


getAll(chave).map { lista ->
lista.existe { par ->
par.primeiro == chave
}
}.getOrElse( falso)

E aqui está a get função:

fun get(chave: @UnsafeVariance K): Resultado<Par<K, V>> =


getAll(chave).flatMap { lista ->
list.filter { par ->
par.primeiro == chave
}.headSafe()
}

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.

Uma coisa a observar sobre essa implementação é que a minus funçãotesta se a


lista de pares resultante está vazia. Se for, ele chama a minus função no delegado.
Caso contrário, chama a plus funçãopara reinserir a nova lista da qual a entrada
correspondente foi excluída. Lembre-se do exercício 10.1 do capítulo 10. Isso só é
possível porque você optou por implementar plus de forma que um elemento
encontrado igual a um elemento presente no mapa fosse inserido no lugar do ori-
ginal. Se você não tivesse feito isso, primeiro teria que remover o elemento e de-
pois inserir o novo com omodificadoLista.

11.3 Implementando uma fila de prioridade funcional

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 .

11.3.1 Observando o protocolo de acesso prioritário à fila

valorespodem ser inseridos em uma fila de prioridade em qualquer ordem, mas


só podem ser recuperados em uma ordem específica. Todos os valores têm um ní-
vel de prioridade e apenas o elemento com a prioridade mais alta está disponível.
A prioridade é representada por uma ordenação dos elementos, o que implica que
os elementos devem ser comparáveis ​de alguma forma.

A prioridade corresponde à posição dos elementos em uma fila de espera teórica.


A prioridade mais alta pertence ao elemento com a posição mais baixa (o pri-
meiro elemento). Por convenção, a prioridade mais alta é representada pelo valor
mais baixo.

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

11.3.2 Explorando casos de uso de fila de prioridade

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.

Outro caso de uso comum é reordenar elementos após o processamento paralelo


assíncrono. Imagine que você tenha várias páginas de dados para processar. Para
acelerar o processamento, você pode distribuir os dados entre vários threads que
trabalham em paralelo. Mas não há garantia de que os threads retornarão seus
trabalhos na mesma ordem em que os receberam. Para ressincronizar as páginas,
você pode colocá-las em uma fila de prioridade. O processo que deveria consumir
as páginas pesquisa a fila para verificar se o elemento disponível (o início da fila)
é o esperado. Por exemplo, se as páginas 1, 2, 3, 4, 5, 6, 7 e 8 forem fornecidas a
oito threads para serem processadas em paralelo, o consumidor sondará a fila
para ver se a página 1 está disponível. Se for, ele o consome. Se não, ele espera.

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.

Como eu disse anteriormente, a escolha de uma implementação geralmente é


uma questão de negociar espaço contra tempo ou tempo contra tempo. Aqui, a es-
colha que você precisa fazer é entre os tempos de inserção e recuperação. No caso
de uso geral, o tempo de recuperação deve ser otimizado em relação ao tempo de
inserção porque a proporção entre os números de operações de inserção e recu-
peração geralmente será amplamente favorável à recuperação. (A cabeça será fre-
quentemente lida sem serremovido.)

11.3.3 Olhando para os requisitos de implementação

Vocêpoderia implementar uma fila de prioridade baseada na árvore rubro-negra


porque encontrar o valor mínimo é rápido. Mas a recuperação não significa remo-
ção. Se você procurar o valor mínimo e descobrir que não é o que você deseja,
terá que voltar mais tarde e pesquisar novamente. Uma solução para isso poderia
ser memorizar o valor mais baixo na inserção. A outra alteração que você pode
querer fazer é em relação à remoção. A remoção de um elemento é relativamente
rápida, mas como você sempre removerá o elemento mais baixo, pode ser possí-
vel otimizar a estrutura de dados para esta operação.

Outra consideração importante seria em relação às duplicatas. Embora a árvore


rubro-negra não permita duplicatas, a fila de prioridade deve sim, pois é perfeita-
mente possível ter vários elementos com a mesma prioridade. A solução pode ser
a mesma dos mapas—armazenar listas de elementos (em vez de elementos úni-
cos) com a mesma prioridade. Mas isso provavelmente não será ideal paraatua-
ção.

11.3.4 A estrutura de dados do heap esquerdo

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.

Como você pode ver, é possível recuperar o elemento de maior prioridade em


tempo constante porque sempre será a raiz da árvore. Este elemento é chamado
de cabeça da estrutura. Remover um elemento, por analogia com uma lista, con-
siste em retornar o resto da árvore uma vez que a raiz foi removida. Esse valor re-
tornado é chamado de cauda doestrutura.
11.3.5 Implementando o heap esquerdo

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.

Listagem 11.4 As estruturas heap da esquerda

classe selada Heap<out A: Comparable<@UnsafeVariance A>> {

internal abstract val left: Result<Heap<A>> ①


internal abstract val right: Result<Heap<A>> ①
internal abstract val head: Result<A> ①
classificação de valor abstrato protegido: Int
tamanho do val abstrato: Int ②
val abstrato isEmpty: Booleano

classe abstrata Vazio<saída A: Comparável<@UnsafeVariance A>>:


Heap<A>() { ③

substituir val isEmpty: Boolean = true

substituir val esquerda: Result<Heap<A>> = Result(E)

substituir val à direita: Result<Heap<A>> = Result(E)

substituir val head: Resultado<A> =


Result.failure("head() chamado em heap vazio")

substituir classificação val: Int = 0


substituir o tamanho do val: Int = 0
}

objeto interno E: Vazio<Nada>() ④

classe interna H<saída A: Comparável<@UnsafeVariance A>>(


substituir classificação val: Int, ⑤
valor privado lft: Heap<A>,
valor privado hd: A,
private val rght: Heap<A>): Heap<A>() {

substituir val isEmpty: Boolean = false

substituir val esquerda: Result<Heap<A>> = Result(lft)

substituir val à direita: Result<Heap<A>> = Result(rght)

substituir val head: Result<A> = Result(hd)

substituir val size: Int = lft.size + rght.size + 1


}

objeto complementar {

operador divertido <A: Comparável<A>> invocar(): Heap<A> = E ⑥


}
}

① As funções left, right e head retornam um Result.

② O tamanho da árvore é o número de elementos que ela contém.

③ A classe Empty é abstrata.

④ O objeto E singleton representa todas as árvores vazias.


⑤ A propriedade rank é computada fora da subclasse H e passada para o construtor.

⑥ Esta função retorna uma árvore vazia.

Exercício 11.5

A primeira funcionalidade que você deseja adicionar à sua Heap implementação


é a capacidade de adicionar um elemento. Definir uma plus funçãopor esta.
Torná-lo uma função de operador de instância na Heap classecom a seguinte
assinatura:

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

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

Defina uma função no objeto complementar para criar um a Heap partir de um


elemento e outra para criar um heap mesclando dois heaps com as seguintes
assinaturas:

operador divertido <A: Comparável<A>> invocar(elemento: A): Heap<A>

fun <A: Comparable<A>> merge(primeiro: Heap<A>, segundo: Heap<A>): Heap<A>

Em seguida, defina a plus funçãoem relação a esses dois.


Solução

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:

operador divertido <A: Comparável<A>> invocar(elemento: A): Heap<A> =


H(1, E, elemento, E)

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

Esse código primeiro verifica se a classificação do primeiro heap é maior ou igual


à segunda. Se for, a nova classificação é definida como a classificação do segundo
heap + 1 e os dois heaps são usados ​em primeira e segunda ordem. Caso contrário,
a nova classificação é definida como a primeira classificação de heap + 1 e as duas
heaps são usadas na ordem inversa (segundo, primeiro). Agora a função para
mesclar dois heaps pode ser escrita da seguinte forma:

fun <A: Comparable<A>> merge(primeiro: Heap<A>, segundo: Heap<A>): Heap<A> =


first.head.flatMap { fh ->
second.head.flatMap { sh ->
quando {
fh <= sh -> first.left.flatMap { fl ->
first.right.map { fr ->
merge(fh, fl, merge(fr, segundo))
}
}
else -> second.left.flatMap { sl ->
second.right.map { sr ->
merge(sh, sl, merge(first, sr))
}
}
}
}
}.getOrElse(quando (primeiro) {
E -> segundo
senão -> primeiro
})

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:

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


merge(this, Heap(elemento))

11.3.6 Implementando a interface do tipo fila

Apesaré implementado como uma árvore, o heap do ponto de vista do usuário é


como uma fila de prioridade, o que significa uma espécie de lista encadeada onde
a cabeça é sempre o menor elemento. Por analogia, o elemento raiz da árvore é
chamado de cabeça e o que resta após a remoção da cabeça é chamado de cauda.
Exercício 11.6

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:

abstract fun tail(): Result<Heap<A>>

Solução

A Empty implementação é óbvia e retorna um Failure :

override fun tail(): Result<Heap<A>> =


Result.failure(IllegalStateException("tail() chamado na pilha vazia"))

A H implementação não é mais complexa, dadas as funções que você definiu no


exercício anterior. Ele retorna o resultado da fusão dos ramos esquerdo e direito:

override fun tail(): Result<Heap<A>> = Result(merge(lft,rght))

Exercício 11.7

Implementar uma get funçãoque leva um int parâmetroe retorna o enésimo


elemento por ordem de prioridade. Esta função retornará um Result para lidar
com o caso em que nenhum elemento é encontrado. Aqui está sua assinatura na
Heap classe pai:

fun get(index: Int): Resultado<A>


Solução

A Empty implementação é óbvia e retorna uma falha:

override fun get(index: Int): Resultado<A> =


Result.failure(NoSuchElementException("Índice fora dos limites"))

A H implementação é igualmente simples. Ele começa testando o index. Se for 0,


retorna Success do head valor. Caso contrário, ele procura recursivamente pelo
elemento do índice index - 1 na cauda. Como a cauda não existe, mas é apenas o
valor retornado pela tail função (que é um Result ), esse resultado é mapeado
de forma plana com uma chamada recursiva para get :

override fun get(index: Int): Result<A> = when (index) {


0 -> Resultado(hd)
else -> tail().flatMap { it.get(index - 1) }
}

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.

11.4 Elementos e listas ordenadas

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

Aqui está a pop funçãoassinatura:

fun pop(): Option<Pair<A, Heap<A>>>

Solução

Aqui está uma pop implementação possível. Primeiro defina uma função abstrata
na Heap classe:

abstract fun pop(): Option<Pair<A, Heap<A>>>

A implementação na Empty classe retorna uma opção vazia:

override fun pop(): Option<Pair<A, Heap<A>>> = Option()

Na H classe, a pop funçãoretorna um Option de um par contendo a cabeça e a


cauda do heap:

override fun pop(): Option<Pair<A, Heap<A>>> =


Option(Pair(hd, merge(lft, rght)))
O tipo de retorno da pop funçãotorna-o adequado para a unfold funçãovocê de-
finiu para o List tipo. Veja como você pode implementar uma toList função na
Heap classe pai:

fun toList(): List<A> = desdobrar(este) { it.pop() }

Exercício 11.9

No exercício anterior, você usou a unfold funçãoproduzindo um List<A> . Mas


há espaço para abstração aqui. Esta função poderia ser generalizada para produ-
zir qualquer B tipo se List<A> fosse uma realização possível de B .

Escreva uma unfold funçãoem Heap produzir a B em vez de a List<A> e rees-


crever a toList função em termos deste. Em seguida, escreva uma fol-
dLeft funçãocom a mesma assinatura da List.foldLeft versão.

Dica

Comece copiando a implementação da List unfold funçãona Heap classee faça


as alterações necessárias para substituir o List<A> tipo por um tipo mais gené-
rico B . Aqui está a implementação da unfold função produzindo um List :

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:

fun <A, S, B> desdobrar(z: S,


getPróximo: (S) -> Opção<Par<A, S>>,
identidade: B,
f: (B) -> (A) -> B): B {
tailrec divertido desdobrar (acc: B, z: S): B {
val proximo = getPróximo(z)
retornar quando (próximo) {
é Option.None -> acc
é Option.Some ->
desdobrar(f(acc)(próximo.valor.primeiro), próximo.valor.segundo)
}
}
return desdobrar(identidade, z)
}

uma foldLeft funçãoagora pode ser reescrita como

fun <B> foldLeft(identity: B, f: (B) -> (A) -> B): B =


desdobrar(este, { it.pop() }, identidade, f)
E você poderia então reescrever toList Como

fun toList(): List<A> =


foldLeft(List<A>()) { list -> { a -> list.cons(a) } }.reverse()

11.5 Uma fila de prioridade para elementos não comparáveis

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

Primeiro, você deve alterar a declaração da Heap classesubstituindo

classe selada Heap<out A: Comparable<@UnsafeVariance A>>

com

classe selada Heap<out A>

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:

comparador de valor abstrato interno: Result<Comparator<@UnsafeVariance A>>

O E objeto singleton é removido. a Empty classenão é mais abstrato e é cons-


truído com a Result<Comparator> with Empty como valor padrão:

classe interna Vazio<saída A>(


substituir val comparator: Result<Comparator<@UnsafeVariance A>> =
Result.Vazio): Heap<A>() {

Você fará o mesmo na H classe, exceto que usará o construtor existente (se hou-
ver) para o valor padrão:

classe interna H<out A>(substituir val rank: Int,


valor interno lft: Heap<A>,
valor interno hd: A,
valor interno direito: Heap<A>,
substituir val comparator: Result<Comparator<@UnsafeVariance A>> =
lft.comparator.orElse { rght.comparator }): Heap<A>() {

As funções do objeto companheiro para criar um vazio Heap existem em várias


versões: uma sem comparador e outra com Result<Comparator> . E para sim-
plificar o uso, você adicionará uma versão com extensão Comparator . Você tam-
bém precisa de uma versão específica da função que cria um a Heap partir de um
único elemento:

operador divertido <A: Comparável<A>> invocar(): Heap<A> = Vazio()

operador fun <A> invocar(comparator: Comparator<A>): Heap<A> =


Vazio(Resultado(comparador))
operador fun <A> invoke(comparator: Result<Comparator<A>>): Heap<A> =
Vazio(comparador)

operador divertido <A> invocar(elemento: A, comparador: Resultado<Comparador<A>>):


Heap<A> = H(1, Vazio(comparador), elemento,
Vazio(comparador), comparador)

Observe que a invoke() funçãosem um argumento só pode ser chamado para


um Comparable tipo. Como consequência, não é possível criar um heap vazio
para um tipo não comparável sem fornecer um comparador. Isso é verificado
pelo compilador. A função que toma um elemento como parâmetro será alterada
para criar um comparador ou receber um como parâmetro adicional:

operador divertido <A: Comparável<A>> invocar(elemento: A): Heap<A> =


invoca(elemento, Comparador { o1: A, o2: A -> o1.compareTo(o2) })

operador fun <A> invoca(elemento: A, comparador: Comparator<A>): Heap<A> =


H(1, Vazio(Resultado(comparador)), elemento,
Vazio(Resultado(comparador)), Resultado(comparador))

a merge função, pegando um elemento e dois heaps, precisa ser modificado de


acordo, mas desta vez, você extrairá o comparador dos argumentos do heap:

diversão protegida <A> merge(head: A, primeiro: Heap<A>, segundo: Heap<A>): Heap<A> =


first.comparator.orElse { second.comparator }.let {
quando {
primeiro.rank >= segundo.rank -> H(segundo.rank + 1,
primeiro, cabeça, segundo, ele)
else -> H(first.rank + 1, second, head, first, it)
}
}
para a merge funçãotomando dois Heap s como argumentos, você pode usar o
Comparator de qualquer uma das duas árvores a serem mescladas. Se nenhum
tiver um Comparator , você pode usar um Result.Empty . Para não extrair o
comparador dos argumentos em cada chamada recursiva, você pode dividir a
função em duas:

fun <A> merge(primeiro: Heap<A>, segundo: Heap<A>,


comparador: Resultado<Comparador<A>> =
first.comparator.orElse { second.comparator }): Heap<A> =
first.head.flatMap { fh ->
second.head.flatMap { sh ->
quando {
compare(fh, sh, comparador) <= 0 ->
first.left.flatMap { fl ->
first.right.map { fr ->
merge(fh, fl, merge(fr, segundo, comparador))
}
}
else -> second.left.flatMap { sl ->
second.right.map { sr ->
merge(sh, sl, merge(primeiro, sr, comparador))
}
}
}
}
}.getOrElse(quando (primeiro) {
está vazio -> segundo
senão -> primeiro
})

A segunda função usa uma helper função chamada compare :

private fun <A> compare(primeiro: A, segundo: A,


comparador: Resultado<Comparador<A>>): Int =
comparator.map { comp ->
comp.compare(primeiro, segundo)
}.getOrElse { (primeiro como Comparable<A>).compareTo(second) }

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:

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


merge(this, Heap(elemento, comparador))

Finalmente, as funções left e na classe right Empty deve ser mudadoComose-


gue:

override val left: Result<Heap<A>> = Result(this)


substituir val à direita: Result<Heap<A>> = Result(this)

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

5  Chris Okasaki, Estruturas de Dados Puramente Funcionais (Cambridge University


Press, 1999).

Você também pode gostar