Existe alguma maneira prática de uma estrutura de nó vinculado ser imutável?

26

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 addoperaçõ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 nextcampo por um novo nó contendo a 5. O problema agora 3é referenciar o antigo 4; aquele sem o anexo 5. Agora eu preciso copiar 3e substituir seu nextcampo para fazer referência à 4cópia, mas agora 2está 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.

Carcinigenicado
fonte
Eu diria que sim - as raras coleções imutáveis ​​em Java são aquelas com métodos de mutação substituídos ou as CopyOnWritexxxclasses incrivelmente caras usadas para multiencadeamento. Ninguém espera coleções para ser imutável realmente (embora não criar algumas peculiaridades)
Ordous
9
(Comente a citação.) Isso, uma ótima citação, geralmente é muito mal compreendido. A imutabilidade deve ser aplicada ao ValueObject por causa de seus benefícios , mas se você estiver desenvolvendo em Java, é impraticável usar a imutabilidade em locais onde a mutabilidade é antecipada. Na maioria das vezes, uma classe tem certos aspectos que devem ser imutáveis ​​e certos que precisam ser mutáveis ​​com base nos requisitos.
Rwong 12/08/2015
2
relacionado (possivelmente uma duplicata): O objeto de coleção é melhor imutável? "Sem custos indiretos de desempenho, esta coleção (classe LinkedList) pode ser introduzida como coleção imutável?"
Gnat
Por que você faria uma lista vinculada imutável? O maior benefício de uma lista vinculada é a mutabilidade, você não se sairia melhor com uma matriz?
Pieter B
3
Você poderia me ajudar a entender suas necessidades? Você quer ter uma lista imutável que deseja alterar? Isso não é uma contradição? O que você quer dizer com "os internos" da lista? Você quer dizer que os nós adicionados anteriormente não devem ser modificados posteriormente, mas apenas permitem anexar ao último nó da lista?
null

Respostas:

46

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:

2 -> 3 -> 4

O prefixo a 1fornece:

1 -> 2 -> 3 -> 4

Mas observe que não importa se alguém ainda está mantendo uma referência 2como o chefe de sua lista, porque a lista é imutável e os links seguem apenas um caminho. Não há como saber 1se existe apenas uma referência 2. Agora, se você anexou um 5a uma das listas, teria que fazer uma cópia da lista inteira, porque, caso contrário, ela também aparecerá na outra lista.

Karl Bielefeldt
fonte
Ahh, bom argumento sobre por que o anexo não requer uma cópia; Eu não pensei nisso.
Carcigenicate
17

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 nextponteiro do (agora) penúltimo nó, que em uma configuração imutável cria um novo nó e, em seguida, precisamos definir o nextponteiro do penúltimo nó, e assim por diante.

Penso que o principal problema aqui não é a imutabilidade, nem a appendoperaçã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):

list.append(x);
list.prepend(y);

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.Liste 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 mapou fold(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
4

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

catraca arrepiante
fonte
1
Eu usei Haskell. Além disso, também contorna parte do problema usando a preguiça. Estou fazendo anexos porque achei que era o que a Listinterface 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.
Carcigenicate
Criar uma classe de acordo com uma interface que não corresponda à estrutura / algoritmo de dados subjacente é um convite a problemas e ineficiências.
catraca aberração
Os JavaDocs usam explicitamente a palavra "anexar". Você está dizendo que é melhor ignorar isso por uma questão de implementação?
Carcigenicate
2
@Carcigenicate não estou dizendo que é um erro para tentar encaixar uma lista vinculada isoladamente com nós imutáveis no molde dejava.util.List
aberração catraca
1
Com preguiça, não haveria mais uma lista vinculada; não há lista; portanto, não há mutação (anexar). Em vez disso, há algum código que gera o próximo item, mediante solicitação. Mas não pode ser usado em qualquer lugar em que uma lista é usada. Ou seja, se você escrever um método em Java que espera alterar uma lista que é passada como argumento, essa lista deverá ser mutável em primeiro lugar. A abordagem do gerador requer uma reestruturação (reorganização) completa do código para fazê-lo funcionar - e para possibilitar a eliminação física da lista.
Rwong 12/08/2015
4

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 consoperaçõ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 StringBuilderpara construir uma string e, no final, obtém o resultado como String(imutável!) Chamando toString. Uma diferença é que a StringBuilderé 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 DListclasse imutável que forneça uma interface semelhante à de Haskell Data.DList.

Giorgio
fonte
4

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:insira a descrição da imagem aqui

feydaykyn
fonte
2

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.

Sandro Gržičić
fonte
1
Há uma pergunta sobre as portas Java de PersistentVector em stackoverflow.com/questions/15997996/...
James_pic
1

Uma abordagem que às vezes pode ser útil seria ter duas classes de objetos de lista - um objeto de lista direta com uma finalreferência ao primeiro nó em uma lista vinculada a frente e uma não finalreferê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 uma finalreferê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 finalreferência e a criação de um novo objeto de lista do mesmo tipo que o original, com uma finalreferê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 uma finalreferê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 volatilevariá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.

supercat
fonte