Qual árvore binária de auto balanceamento você recomendaria?

18

Estou aprendendo Haskell e, como exercício, estou criando árvores binárias. Tendo feito uma árvore binária regular, quero adaptá-la para se auto balancear. Então:

  • Qual é o mais eficiente?
  • Qual é a mais fácil de implementar?
  • Qual é o mais usado?

Mas crucialmente, o que você recomenda?

Suponho que isso pertence aqui porque está aberto ao debate.

dan_waterworth
fonte
Em termos de eficiência e facilidade de implementação, as eficiências gerais estão bem definidas, mas para sua implementação, acho que o melhor seria implementar o maior número possível e, em seguida, deixe-nos saber o que funciona melhor ...
glenatron

Respostas:

15

Eu recomendaria que você iniciasse com uma árvore vermelho-preta ou uma árvore AVL .

A árvore vermelho-preta é mais rápida para inserir, mas a árvore AVL tem uma leve margem para pesquisas. A árvore AVL é provavelmente um pouco mais fácil de implementar, mas não muito com base na minha própria experiência.

A árvore AVL garante que a árvore seja equilibrada após cada inserção ou exclusão (nenhuma subárvore possui um fator de equilíbrio maior que 1 / -1, enquanto a árvore Vermelho-preta garante que a árvore esteja razoavelmente equilibrada a qualquer momento.

Quick Joe Smith
fonte
1
Pessoalmente, acho a inserção vermelho-preta mais fácil do que a inserção AVL. A razão é através da analogia (imperfeita) às árvores-B. As inserções são complicadas, mas as exclusões são más (muitos casos a serem considerados). Na verdade, eu não tenho mais uma implementação de exclusão em preto-e-branco do C ++ - excluí-a quando percebi (1) que nunca a estava usando - toda vez que queria excluir, estava excluindo vários itens, então converti de árvore para lista, exclua da lista e depois converta novamente em uma árvore e (2) foi quebrado de qualquer maneira.
precisa saber é o seguinte
2
@ Steve314, as árvores vermelho-preto são mais fáceis, mas você não conseguiu fazer uma implementação que funcione? Como são as árvores AVL?
dan_waterworth
@dan_waterworth - ainda não fiz uma implementação com um método de inserção que funcione - faça anotações, entenda o princípio básico, mas nunca recebi a combinação certa de motivação, tempo e confiança. Se eu apenas queria versões que funcionassem, isso é apenas copiar-pseudocódigo-de-livro-texto-e-traduzir (e não se esqueça de que o C ++ possui contêineres de biblioteca padrão), mas qual é a graça nisso?
precisa saber é o seguinte
BTW - Acredito (mas não posso fornecer a referência) que um livro bastante popular inclui uma implementação de buggy de um dos algoritmos de árvore binária balanceada - não tenho certeza, mas pode ser uma exclusão em preto e vermelho. Portanto, não é apenas me ;-)
Steve314
1
@ Steve314, eu sei, as árvores podem ser extremamente complicadas em linguagem imperativa, mas, surpreendentemente, implementá-las em Haskell tem sido uma brisa. Eu escrevi uma árvore AVL regular e também uma variante espacial 1D no fim de semana e ambas são apenas cerca de 60 linhas.
dan_waterworth
10

Eu consideraria uma alternativa se você estiver bem com estruturas de dados aleatórios : Skip Lists .

Do ponto de vista de alto nível, é uma estrutura em árvore, exceto que não é implementada como uma árvore, mas como uma lista com várias camadas de links.

Você receberá inserções / pesquisas / exclusões O (log N) e não precisará lidar com todos esses casos complicados de reequilíbrio.

Porém, nunca pensei em implementá-los em uma linguagem funcional, e a página da Wikipédia não mostra nenhuma, portanto pode não ser fácil (devido à imutabilidade)

Matthieu M.
fonte
Eu realmente gosto de pular listas e já as implementei antes, embora não em uma linguagem funcional. Acho que vou tentar depois disso, mas agora estou em árvores que se equilibram.
precisa saber é o seguinte
Além disso, as pessoas costumam usar skiplists para estruturas de dados simultâneas. Pode ser melhor, em vez de forçar a imutabilidade, usar as primitivas de simultaneidade do haskell (como MVar ou TVar). Embora isso não me ensine muito a escrever código funcional.
dan_waterworth
2
@ Fanatic23, uma Skip List não é uma ADT. O ADT é um conjunto ou uma matriz associativa.
dan_waterworth
@dan_waterworth meu mal, você está correto.
precisa saber é o seguinte
5

Se você deseja iniciar uma estrutura relativamente fácil (tanto as árvores AVL quanto as vermelhas e pretas são complicadas), uma opção é um treap - nomeado como uma combinação de "tree" e "heap".

Cada nó obtém um valor de "prioridade", geralmente atribuído aleatoriamente à medida que o nó é criado. Os nós são posicionados na árvore para que a ordem das chaves seja respeitada e para que a ordem de pilha de valores de prioridade seja respeitada. Ordenação de pilha significa que ambos os filhos de um pai têm prioridades mais baixas que o pai.

EDIT excluído "dentro dos valores-chave" acima - a prioridade e a ordem das chaves se aplicam juntas, portanto a prioridade é significativa mesmo para chaves exclusivas.

É uma combinação interessante. Se as chaves são únicas e as prioridades são únicas, existe uma estrutura em árvore exclusiva para qualquer conjunto de nós. Mesmo assim, inserções e exclusões são eficientes. Estritamente falando, a árvore pode ser desequilibrada ao ponto de efetivamente ser uma lista vinculada, mas isso é extremamente improvável (como nas árvores binárias padrão), inclusive em casos normais, como chaves inseridas em ordem (ao contrário das árvores binárias padrão).

Steve314
fonte
1
+1. Treaps é minha escolha pessoal, até escrevi um post sobre como eles são implementados.
precisa saber é o seguinte
5

Qual é o mais eficiente?

Vagas e difíceis de responder. As complexidades computacionais são todas bem definidas. Se é isso que você quer dizer com eficiência, não há debate real. De fato, todos os bons algoritmos vêm com provas e fatores de complexidade.

Se você quer dizer "tempo de execução" ou "uso de memória", precisará comparar as implementações reais. Então, idioma, tempo de execução, sistema operacional e outros fatores entram em jogo, dificultando a resposta da pergunta.

Qual é a mais fácil de implementar?

Vagas e difíceis de responder. Alguns algoritmos podem parecer complexos para você, mas triviais para mim.

Qual é o mais usado?

Vagas e difíceis de responder. Primeiro, há o "por quem?" parte disso? Apenas Haskell? E o C ou C ++? Segundo, há o problema de software proprietário em que não temos acesso à fonte para fazer uma pesquisa.

Mas crucialmente, o que você recomenda?

Suponho que isso pertence aqui porque está aberto ao debate.

Corrigir. Como seus outros critérios não são muito úteis, é isso que você obterá.

Você pode obter fonte para um grande número de algoritmos em árvore. Se você quiser aprender alguma coisa, pode simplesmente implementar todas as que encontrar. Em vez de pedir uma "recomendação", basta coletar todos os algoritmos que encontrar.

Aqui está a lista:

http://en.wikipedia.org/wiki/Self-balancing_binary_search_tree

Existem seis populares definidos. Comece com aqueles.

S.Lott
fonte
3

Se você está interessado em árvores Splay, há uma versão mais simples daquelas que, acredito, foram descritas pela primeira vez em um artigo de Allen e Munroe. Ele não possui as mesmas garantias de desempenho, mas evita complicações ao lidar com o reequilíbrio "zig-zig" vs. "zig-zag".

Basicamente, ao pesquisar (incluindo pesquisas de um ponto ou nó de inserção a ser excluído), o nó encontrado é girado diretamente em direção à raiz, de baixo para cima (por exemplo, quando uma função de pesquisa recursiva sai). Em cada etapa, você seleciona uma única rotação para a esquerda ou direita, dependendo de o filho que você deseja dar outro passo em direção à raiz ser o filho certo ou o filho esquerdo (se me lembro corretamente das instruções de rotação, respectivamente).

Assim como as árvores Splay, a idéia é que os itens acessados ​​recentemente estejam sempre perto da raiz da árvore, para acessar rapidamente novamente. Sendo mais simples, essas árvores Allen-Munroe de rotação para raiz (o que eu as chamo - não sabem o nome oficial) podem ser mais rápidas, mas elas não têm a mesma garantia de desempenho amortizada.

Uma coisa: como essa estrutura de dados, por definição, sofre mutação, mesmo para operações de busca, provavelmente precisará ser implementada monadicamente. IOW talvez não seja uma boa opção para programação funcional.

Steve314
fonte
Os splays são um pouco irritantes, pois eles modificam a árvore mesmo quando são encontrados. Isso seria bastante doloroso em ambientes multithread, o que é uma das grandes motivações para o uso de uma linguagem funcional como Haskell em primeiro lugar. Por outro lado, nunca usei linguagens funcionais antes, então talvez isso não seja um fator.
Quick Joe Smith
@ Quick - depende de como você pretende usar a árvore. Se você o estivesse usando no verdadeiro código de estilo funcional, você eliminaria a mutação em cada descoberta (tornando uma árvore Splay um pouco boba) ou acabaria duplicando uma parte substancial da árvore binária em cada pesquisa, e acompanhe com qual estado da árvore você está trabalhando à medida que o trabalho avança (o motivo para provavelmente usar um estilo monádico). Essa cópia pode ser otimizada pelo compilador se você não fizer mais referência ao estado antigo da árvore após a criação do novo (suposições semelhantes são comuns na programação funcional), mas talvez não.
precisa saber é o seguinte
Nenhuma abordagem parece valer o esforço. Por outro lado, nem a maioria das linguagens puramente funcionais.
Quick Joe Smith
1
@Quick - Duplicar a árvore é o que você fará para qualquer estrutura de dados de árvore em uma linguagem funcional pura para alterar algoritmos como inserções. Em termos de origem, o código não será tão diferente do código imperativo que faz atualizações no local. As diferenças já foram tratadas, presumivelmente, para árvores binárias desequilibradas. Desde que você não tente adicionar links pai aos nós, as duplicatas compartilharão subárvores comuns no mínimo, e a otimização profunda em Haskell é bastante grave, se não perfeita. Sou anti-Haskell em princípio, mas isso não é necessariamente um problema.
Steve314
2

Uma árvore equilibrada muito simples é uma árvore AA . Sua invariante é mais simples e, portanto, mais fácil de implementar. Devido à sua simplicidade, seu desempenho ainda é bom.

Como exercício avançado, você pode tentar usar GADTs para implementar uma das variantes de árvores balanceadas cuja invariante é imposta pelo tipo de sistema de tipos.

Petr Pudlák
fonte