Por que anexar a uma lista em Scala tem complexidade de tempo O (n)?


13

Acabei de ler que o tempo de execução da operação de acréscimo para a List(: +) cresce linearmente com o tamanho da List.

Anexar a um Listparece ser uma operação bastante comum. Por que a maneira idiomática de fazer isso é preceder os componentes e depois reverter a lista? Também não pode ser uma falha de design, pois a implementação pode ser alterada a qualquer momento.

Do meu ponto de vista, tanto o anexo quanto o anexo devem ser O (1).

Existe alguma razão legítima para isso?


2
Depende da sua definição de "legítimo". O Scala é fortemente estruturado em estruturas de dados imutáveis, listas anônimas onipresentes, composição funcional etc. A implementação da lista padrão (sem um ponteiro mutável extra para a cauda da lista) funciona bem para esse estilo. Se você precisa de uma lista mais poderosa, pelo menos é muito fácil escrever seu próprio contêiner, que é quase indistinguível dos padrões.
precisa

1
Relacionado a vários sites - Anexando um elemento ao final de uma lista no Scala - há um pouco sobre a natureza dele. Ele parece que uma lista em scala é imutável, portanto, você precisa copiá-lo, o que é O (N).

Você pode usar uma das muitas estruturas de dados mutáveis ​​disponíveis ou estruturas de dados imutáveis ​​com O (1) append time (Vector) que o Scala fornece. List[T]pressupõe que você o esteja usando da maneira que usaria em uma linguagem funcional pura - geralmente trabalhando na cabeça com desconstrução e prepends.
KChaloux #

3
A adição antecipada colocará o próximo ponteiro do nó do novo cabeçalho na lista imutável existente - que não pode ser alterada. Isso é O (1).

1
Para o trabalho seminal e a análise do tópico geral das medidas de complexidade da estrutura de dados em FP puro, leia a tese de Okasaki, que mais tarde foi publicada como livro. É uma leitura muito respeitada e muito boa para quem está aprendendo algum FP para entender como pensar sobre a organização de dados no FP. Também é bem escrito e fácil de ler e seguir, de modo que um texto de qualidade.
Jimmy Hoffa

Respostas:


24

Vou expandir um pouco meu comentário. A List[T]estrutura de dados, from scala.collection.immutableé otimizada para funcionar da mesma maneira que uma lista imutável em uma linguagem de programação mais puramente funcional. Tem tempos de pré-inserção muito rápidos , e supõe-se que você estará trabalhando na cabeça por quase todo o seu acesso.

As listas imutáveis ​​passam a ter tempos de pré-inserção muito rápidos, devido ao fato de modelarem suas listas vinculadas como uma série de "células contras". A célula define um valor único e um ponteiro para a próxima célula (estilo clássico de lista vinculada individual):

Cell [Value| -> Nil]

Quando você anexa uma lista, na verdade você está apenas criando uma única célula nova, com o restante da lista existente sendo apontado:

Cell [NewValue| -> [Cell[Value| -> Nil]]

Como a lista é imutável, você pode fazer isso sem nenhuma cópia real . Não há perigo de a lista antiga mudar e fazer com que todos os valores em sua nova lista se tornem inválidos. No entanto, você perde a capacidade de ter um ponteiro mutável para o final da sua lista como um compromisso.

Isso se presta muito bem ao trabalho recursivo em listas. Digamos que você definiu sua própria versão de filter:

def deleteIf[T](list : List[T])(f : T => Boolean): List[T] = list match {
  case Nil => Nil
  case (x::xs) => f(x) match {
    case true => deleteIf(xs)(f)
    case false => x :: deleteIf(xs)(f)
  }
}

Essa é uma função recursiva que trabalha exclusivamente a partir da cabeça da lista e aproveita a correspondência de padrões por meio do extrator ::. Isso é algo que você vê muito em idiomas como Haskell.

Se você realmente deseja anexos rápidos, o Scala fornece várias estruturas de dados mutáveis ​​e imutáveis ​​para você escolher. No lado mutável, você pode investigar ListBuffer. Como alternativa, Vectorfrom scala.collection.immutabletem um tempo de acréscimo rápido.


agora eu entendo! Faz todo sentido.
DPM

Não conheço nenhum Scala, mas não é elseum loop infinito? Eu acho que deveria ser algo assim x::deleteIf(xs)(f).
svick

@svick Uh ... sim. Sim, ele é. Escrevi-o rapidamente e não verificar o meu código, porque eu tinha uma reunião para ir: p (Deve ser fixado agora!)
KChaloux

@Jubbat Porque heade tailacesso a este tipo de lista é muito rápido - mais rápido do que usar qualquer mapa com base em hash ou matriz - é um excelente tipo para funções recursivas. Esta é uma razão pela qual as listas são um tipo de núcleo na maioria das linguagens funcionais (por exemplo, Haskell ou Scheme)
itsbruce

Excelente resposta. Talvez eu adicione um TL; DR que simplesmente diga "porque você deve anexar, não anexar" (pode ajudar a esclarecer as suposições básicas que a maioria dos desenvolvedores tem sobre se Listanexar / anexar).
Daniel B
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.