Decidi escrever uma lista vinculada individualmente e tive o plano para tornar a estrutura interna do nó vinculado imutável.
Eu encontrei um obstáculo embora. Digamos que eu tenha os seguintes nós vinculados (de add
operações anteriores ):
1 -> 2 -> 3 -> 4
e diga que eu quero acrescentar a 5
.
Para fazer isso, como o nó 4
é imutável, preciso criar uma nova cópia de 4
, mas substituir seu next
campo por um novo nó contendo a 5
. O problema agora 3
é referenciar o antigo 4
; aquele sem o anexo 5
. Agora eu preciso copiar 3
e substituir seu next
campo para fazer referência à 4
cópia, mas agora 2
está fazendo referência ao antigo 3
...
Ou, em outras palavras, para anexar, parece que a lista inteira precisa ser copiada.
Minhas perguntas:
Meu pensamento está correto? Existe alguma maneira de fazer um acréscimo sem copiar toda a estrutura?
Aparentemente, "Java eficaz" contém a recomendação:
As aulas devem ser imutáveis, a menos que haja uma boa razão para torná-las mutáveis ...
Esse é um bom caso para mutabilidade?
Não acho que seja uma duplicata da resposta sugerida, pois não estou falando da lista em si; isso obviamente tem que ser mutável para se adaptar à interface (sem fazer algo como manter a nova lista internamente e recuperá-la por meio de um getter. Pensando bem, mesmo isso exigiria alguma mutação; seria apenas mantido no mínimo). Estou falando sobre se os internos da lista devem ou não ser imutáveis.
fonte
CopyOnWritexxx
classes incrivelmente caras usadas para multiencadeamento. Ninguém espera coleções para ser imutável realmente (embora não criar algumas peculiaridades)Respostas:
Com listas em linguagens funcionais, você quase sempre trabalha com uma cabeça e um rabo, o primeiro elemento e o restante da lista. A adição antecipada é muito mais comum porque, como você supôs, a anexação requer a cópia da lista inteira (ou outras estruturas de dados preguiçosas que não se assemelham exatamente a uma lista vinculada).
Em linguagens imperativas, anexar é muito mais comum, porque tende a parecer mais natural semanticamente, e você não se preocupa em invalidar referências a versões anteriores da lista.
Como um exemplo de por que a anexação não requer cópia da lista inteira, considere que você tem:
O prefixo a
1
fornece:Mas observe que não importa se alguém ainda está mantendo uma referência
2
como o chefe de sua lista, porque a lista é imutável e os links seguem apenas um caminho. Não há como saber1
se existe apenas uma referência2
. Agora, se você anexou um5
a uma das listas, teria que fazer uma cópia da lista inteira, porque, caso contrário, ela também aparecerá na outra lista.fonte
Você está correto, o anexo requer a cópia de toda a lista, se você não desejar alterar nenhum nó no local. Pois precisamos definir o
next
ponteiro do (agora) penúltimo nó, que em uma configuração imutável cria um novo nó e, em seguida, precisamos definir onext
ponteiro do penúltimo nó, e assim por diante.Penso que o principal problema aqui não é a imutabilidade, nem a
append
operação é desaconselhada. Ambos estão perfeitamente bem em seus respectivos domínios. Misturá- los é ruim: a interface natural (eficiente) para uma lista imutável enfatizava a manipulação na frente da lista, mas para listas mutáveis muitas vezes é mais natural criar a lista anexando sucessivamente os itens do primeiro ao último.Portanto, sugiro que você decida: deseja uma interface efêmera ou persistente? A maioria das operações produz uma nova lista e deixa acessível uma versão não modificada (persistente) ou você escreve um código como este (efêmero):
Ambas as opções são boas, mas a implementação deve refletir a interface: uma estrutura de dados persistente se beneficia de nós imutáveis, enquanto uma efêmera precisa de mutabilidade interna para realmente cumprir as promessas de desempenho implícitas.
java.util.List
e outras interfaces são efêmeras: implementá-las em uma lista imutável é inapropriado e, de fato, um risco para o desempenho. Bons algoritmos em estruturas de dados mutáveis geralmente são bem diferentes de bons algoritmos em estruturas de dados imutáveis, portanto, vestir uma estrutura de dados imutáveis como uma estrutura mutável (ou vice-versa) convida maus algoritmos.Embora uma lista persistente tenha algumas desvantagens (sem anexos eficientes), isso não precisa ser um problema sério ao programar funcionalmente: muitos algoritmos podem ser formulados com eficiência mudando a mentalidade e usando funções de ordem superior como
map
oufold
(para citar duas relativamente primitivas ) ou anexando repetidamente. Além disso, ninguém está forçando você a usar apenas essa estrutura de dados: quando outros (efêmeros ou persistentes, mas mais sofisticados) forem mais apropriados, use-os. Devo também observar que as listas persistentes têm algumas vantagens para outras cargas de trabalho: elas compartilham suas caudas, o que pode economizar memória.fonte
Se você tem uma lista com link único, você estará trabalhando com a frente se mais do que com a parte de trás.
Linguagens funcionais como prolog e haskel fornecem maneiras fáceis de obter o elemento frontal e o restante da matriz. Anexando à parte traseira está uma operação O (n) com cópias de cada nó.
fonte
List
interface esperava (talvez eu esteja errado lá). Eu não acho que isso realmente responda à pergunta também. A lista inteira ainda precisaria ser copiada; apenas tornaria o acesso ao último elemento adicionado mais rápido, pois não seria necessário um percurso completo.java.util.List
Como outros já apontaram, você está certo de que uma lista vinculada imutável de link único requer a cópia de toda a lista ao executar operações de acréscimo.
Geralmente, você pode usar a solução alternativa para implementar seu algoritmo em termos de
cons
operações (pré-anexadas) e, em seguida, reverter a lista final uma vez. Isso ainda precisa copiar a lista uma vez, mas a sobrecarga da complexidade é linear no comprimento da lista, enquanto, usando repetidamente o append, você pode facilmente obter uma complexidade quadrática.As listas de diferenças (veja, por exemplo, aqui ) são uma alternativa interessante. Uma lista de diferenças agrupa uma lista e fornece uma operação de acréscimo em tempo constante. Basicamente, você trabalha com o invólucro pelo tempo necessário para anexar e depois converte novamente em uma lista quando terminar. De alguma forma, isso é semelhante ao que você faz quando usa a
StringBuilder
para construir uma string e, no final, obtém o resultado comoString
(imutável!) ChamandotoString
. Uma diferença é que aStringBuilder
é mutável, mas uma lista de diferenças é imutável. Além disso, quando você converte uma lista de diferenças em uma lista, você ainda precisa construir toda a nova lista, mas, novamente, você só precisa fazer isso uma vez.Deve ser bastante fácil implementar uma
DList
classe imutável que forneça uma interface semelhante à de HaskellData.DList
.fonte
Você deve assistir a este ótimo vídeo de 2015 React conf do criador do Immutable.js , Lee Byron. Ele fornecerá ponteiros e estruturas para entender como implementar uma lista imutável eficiente que não duplique o conteúdo. As idéias básicas são: - desde que duas listas usem nós idênticos (mesmo valor, mesmo próximo nó), o mesmo nó seja usado - quando as listas começam a diferir, uma estrutura é criada no nó de divergência, que contém ponteiros para o próximo nó específico de cada lista
Esta imagem de um tutorial de reação pode ser mais clara que o meu inglês quebrado:
fonte
Não é estritamente Java, mas sugiro que você leia este artigo em uma estrutura de dados persistente imutável, mas com desempenho indexada, escrita em Scala:
http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala
Por ser uma estrutura de dados Scala, também pode ser usada em Java (com um pouco mais de detalhamento). É baseado em uma estrutura de dados disponível no Clojure, e tenho certeza de que também existe uma biblioteca Java "nativa".
Além disso, uma observação sobre a construção de estruturas de dados imutáveis: o que você geralmente deseja é algum tipo de "Construtor", que permite "modificar" sua estrutura de dados "em construção", acrescentando elementos a ela (dentro de um único encadeamento); depois de terminar de anexar, você chama um método no objeto "em construção", como
.build()
or.result()
, que "constrói" o objeto, fornecendo uma estrutura de dados imutável que você pode compartilhar com segurança.fonte
Uma abordagem que às vezes pode ser útil seria ter duas classes de objetos de lista - um objeto de lista direta com uma
final
referência ao primeiro nó em uma lista vinculada a frente e uma nãofinal
referência inicialmente nula que (quando não -null) identifica um objeto de lista reversa que contém os mesmos itens na ordem oposta e um objeto de lista reversa com umafinal
referência ao último item de uma lista vinculada a ela e uma referência não final inicialmente nula que (quando não -null) identifica um objeto de lista direta contendo os mesmos itens na ordem oposta.Anexar um item a uma lista direta ou anexar um item a uma lista reversa exigiria apenas a criação de um novo nó vinculado ao nó identificado pela
final
referência e a criação de um novo objeto de lista do mesmo tipo que o original, com umafinal
referência para esse novo nó.Anexar um item a uma lista direta ou anexar a uma lista reversa exigiria uma lista do tipo oposto; a primeira vez que é feita com uma lista específica, ele deve criar um novo objeto do tipo oposto e armazenar uma referência; repetir a ação deve reutilizar a lista.
Observe que o estado externo de um objeto de lista será considerado o mesmo, independentemente de sua referência a uma lista do tipo oposto ser nula ou identificar uma lista de ordem oposta. Não haveria necessidade de fazer nenhuma variável
final
, mesmo ao usar código multiencadeado, pois todo objeto de lista teria umafinal
referência a uma cópia completa de seu conteúdo.Se o código em um encadeamento criar e armazenar em cache uma cópia invertida de uma lista e o campo em que essa cópia for armazenada em cache não for volátil, seria possível que o código em outro encadeamento não veja a lista em cache, mas o único efeito adverso disso seria que o outro thread precisaria fazer um trabalho extra construindo outra cópia invertida da lista. Como essa ação, na pior das hipóteses, prejudicaria a eficiência, mas não afetaria a correção, e como as
volatile
variáveis criam seus próprios prejuízos de eficiência, muitas vezes será melhor que a variável seja não volátil e aceite a possibilidade de operações redundantes ocasionais.fonte