Quais estruturas de dados você pode usar para obter a remoção e substituição de O (1)? Ou como você pode evitar situações em que precisa dessas estruturas?
ST
mônada em Haskell faz isso de forma excelente.
Quais estruturas de dados você pode usar para obter a remoção e substituição de O (1)? Ou como você pode evitar situações em que precisa dessas estruturas?
ST
mônada em Haskell faz isso de forma excelente.
Respostas:
Existe uma vasta gama de estruturas de dados que exploram a preguiça e outros truques para obter tempo constante amortizado ou até (em alguns casos limitados, como filas ) atualizações de tempo constante para muitos tipos de problemas. A tese de doutorado de Chris Okasaki "Estruturas de dados puramente funcionais" e livro com o mesmo nome é um excelente exemplo (talvez o primeiro grande), mas o campo avançou desde então . Essas estruturas de dados normalmente não são apenas puramente funcionais na interface, mas também podem ser implementadas em puro Haskell e linguagens semelhantes e são totalmente persistentes.
Mesmo sem nenhuma dessas ferramentas avançadas, as árvores de pesquisa binária balanceada simples fornecem atualizações em tempo logarítmico, de modo que a memória mutável pode ser simulada com, no pior caso, uma lentidão logarítmica.
Existem outras opções, que podem ser consideradas trapaceiras, mas são muito eficazes em relação ao esforço de implementação e ao desempenho do mundo real. Por exemplo, tipos lineares ou tipos de exclusividade permitem a atualização no local como estratégia de implementação de uma linguagem conceitualmente pura, impedindo que o programa mantenha o valor anterior (a memória que seria modificada). Isso é menos geral do que as estruturas de dados persistentes: por exemplo, você não pode criar facilmente um log de desfazer armazenando todas as versões anteriores do estado. Ainda é uma ferramenta poderosa, embora o AFAIK ainda não esteja disponível nas principais linguagens funcionais.
Outra opção para introduzir com segurança o estado mutável em uma configuração funcional é a ST
mônada em Haskell. Ele pode ser implementado sem mutação e, com as unsafe*
funções de restrição , se comporta como se fosse apenas um invólucro sofisticado em torno da passagem implícita de uma estrutura de dados persistente (cf. State
). Porém, devido a algum tipo de truque do sistema que impõe a ordem da avaliação e evita a fuga, ele pode ser implementado com segurança com mutação no local, com todos os benefícios de desempenho.
Uma estrutura mutável barata é a pilha de argumentos.
Dê uma olhada no cálculo fatorial típico do estilo SICP:
(defn fac (n accum)
(if (= n 1)
accum
(fac (- n 1) (* accum n)))
(defn factorial (n) (fac n 1))
Como você pode ver, o segundo argumento para fac
é usado como um acumulador mutável para conter o produto que muda rapidamente n * (n-1) * (n-2) * ...
. Porém, não existe uma variável mutável à vista e não há como alterar inadvertidamente o acumulador, por exemplo, de outro encadeamento.
Este é, obviamente, um exemplo limitado.
Você pode obter listas vinculadas imutáveis com a substituição barata do nó principal (e, por extensão, qualquer parte que comece a partir do cabeçalho): basta fazer o novo cabeçalho apontar para o mesmo nó seguinte do cabeçote antigo. Isso funciona bem com muitos algoritmos de processamento de lista ( fold
com base em qualquer coisa ).
Você pode obter um desempenho bastante bom de matrizes associativas baseadas, por exemplo, em HAMTs . Logicamente, você recebe uma nova matriz associativa com alguns pares de valores-chave alterados. A implementação pode compartilhar a maioria dos dados comuns entre os objetos antigos e os recém-criados. Este não é O (1); normalmente você obtém algo logarítmico, pelo menos na pior das hipóteses. Árvores imutáveis, por outro lado, geralmente não sofrem nenhuma penalidade de desempenho em comparação com árvores mutáveis. Obviamente, isso requer alguma sobrecarga de memória, geralmente longe de proibitiva.
Outra abordagem é baseada na idéia de que, se uma árvore cai em uma floresta e ninguém a ouve, ela não precisa produzir som. Ou seja, se você puder provar que um pouco de estado mutado nunca deixa algum escopo local, poderá alterar os dados nele com segurança.
O Clojure possui transientes que são 'sombras' mutáveis de estruturas de dados imutáveis que não vazam para fora do escopo local. O Clean usa o Uniques para obter algo semelhante (se bem me lembro). A ferrugem ajuda a fazer coisas semelhantes com ponteiros únicos verificados estaticamente.
ref
e limitá-los dentro de um certo escopo. Veja IORef
ou STRef
. E depois, claro, há TVar
s e MVar
s que são semelhantes, mas com semântica simultâneos sãs (STM para TVar
s e mutex base para MVar
s)
O que você está pedindo é um pouco amplo demais. O (1) remoção e substituição de qual posição? A cabeça de uma sequência? A calda? Uma posição arbitrária? A estrutura de dados a ser usada depende desses detalhes. Dito isto, 2-3 Finger Trees parecem uma das estruturas de dados persistentes mais versáteis existentes:
Apresentamos 2 a 3 árvores de dedos, uma representação funcional de seqüências persistentes que suportam o acesso às extremidades em tempo constante amortizado e concatenação e divisão logarítmica no tempo no tamanho da peça menor.
(...)
Além disso, definindo a operação de divisão de uma forma geral, obtemos uma estrutura de dados de uso geral que pode servir como uma sequência, fila de prioridade, árvore de pesquisa, fila de pesquisa prioritária e muito mais.
As estruturas de dados geralmente persistentes têm desempenho logarítmico ao alterar posições arbitrárias. Isso pode ou não ser um problema, pois a constante em um algoritmo O (1) pode ser alta e a desaceleração logarítmica pode ser "absorvida" em um algoritmo geral mais lento.
Mais importante, as estruturas de dados persistentes facilitam o raciocínio sobre o seu programa, e esse deve sempre ser o seu modo padrão de operação. Você deve favorecer estruturas de dados persistentes sempre que possível e usar apenas uma estrutura de dados mutável depois de criar um perfil e determinar que a estrutura de dados persistentes é um gargalo de desempenho. Qualquer outra coisa é otimização prematura.