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.