Почему добавление к списку в Scala имеет O (n) сложность по времени?

13

Я только что прочитал, что время выполнения операции добавления для List(: +) растет линейно с размером List.

Присоединение к a Listкажется довольно обычной операцией. Почему идиоматический способ сделать это состоит в том, чтобы предварительно добавить компоненты, а затем перевернуть список? Это также не может быть ошибкой проекта, поскольку реализация может быть изменена в любой момент.

С моей точки зрения, и добавление, и добавление должны быть O (1).

Есть ли законная причина для этого?

ДПМ
источник
2
Зависит от вашего определения «законный». Scala в значительной степени состоит из неизменяемых структур данных, вездесущих анонимных списков, функциональной композиции и т. Д. Реализация списка по умолчанию (без дополнительного изменяемого указателя на хвост списка) хорошо работает для этого стиля. Если вам нужен более мощный список, по крайней мере, очень легко написать свой собственный контейнер, который почти неотличим от стандартных.
Килиан Фот
1
Связанный с сайтом - добавление элемента в конец списка в Scala - там есть кое-что о его природе. Это кажется , что список в Скале неизменен, поэтому вам нужно скопировать его, что O (N).
Вы можете использовать одну из многих доступных изменяемых структур данных или неизменных структур данных с O (1) временем добавления (Vector), которые предоставляет Scala. List[T]Предполагается, что вы используете его так, как если бы вы работали на чистом функциональном языке - как правило, работая с головы с деконструкцией и приставками.
KChaloux
3
Предшествующий помещает указатель следующего узла новой головы в существующий неизменный список - который не может измениться. То О (1).
1
Для оригинальной работы и анализа общей темы измерения сложности структуры данных в чистом FP ​​прочитайте тезис Окасаки, который позже был опубликован в виде книги. Это высоко ценится и очень хорошее чтение для любого, кто изучает некоторый FP, чтобы понять, как думать об организации данных в FP. Это также хорошо написано и легко читается, и в целом следует качественному тексту.
Джимми Хоффа

Ответы:

24

Я немного расширю свой комментарий. Структура List[T]данных от scala.collection.immutableоптимизирована для работы так, как работает неизменный список в более чисто функциональном языке программирования. У него очень быстрое время предварительной подготовки , и предполагается, что вы будете работать над головой почти весь свой доступ.

Неизменяемые списки получают очень быстрое время предопределения из-за того, что они моделируют свои связанные списки как серию «cons-ячеек». Ячейка определяет одно значение и указатель на следующую ячейку (классический стиль односвязного списка):

Cell [Value| -> Nil]

Когда вы переходите к списку, вы на самом деле просто делаете одну новую ячейку, а остальная часть существующего списка указывается на:

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

Поскольку список неизменен, вы можете сделать это без фактического копирования . Нет опасности того, что старый список изменится и все значения в вашем новом списке станут недействительными. Однако вы теряете возможность иметь изменяемый указатель на конец вашего списка как компромисс.

Это очень хорошо подходит для рекурсивной работы со списками. Допустим, вы определили свою собственную версию 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)
  }
}

Это рекурсивная функция, которая работает исключительно с заголовка списка и использует преимущества сопоставления с образцом через :: extractor. Это то, что вы часто видите в таких языках, как Haskell.

Если вы действительно хотите быстрое добавление, Scala предоставляет множество изменяемых и неизменных структур данных на выбор. На изменчивой стороне, вы можете посмотреть ListBuffer. Кроме того , Vectorиз scala.collection.immutableимеет быстрое время добавления.

KChaloux
источник
теперь я понимаю! Это имеет полный смысл.
DPM
Я не знаю Scala, но разве это elseне бесконечный цикл? Я думаю, что это должно быть что-то вроде x::deleteIf(xs)(f).
svick
@ Свик Мм ... да. Да, это так. Я написал это быстро и не подтвердил свой код, потому что у меня было собрание, чтобы перейти к: p (Должно быть исправлено сейчас!)
KChaloux
@Jubbat Потому что headи tailдоступ к этому виду списка очень быстрый - быстрее, чем использование любой карты или массива на основе хеша - это отличный тип для рекурсивных функций. Это одна из причин, почему списки являются базовым типом в большинстве функциональных языков (например, Haskell или Scheme)
itsbruce
Отличный ответ. Возможно, я бы добавил TL; DR, который просто говорит: «потому что вы должны добавлять, а не добавлять» (может помочь прояснить базовые предположения, которые большинство разработчиков имеют относительно Lists и добавления / добавления).
Даниэль Б