Warum hat das Anhängen an eine Liste in Scala eine O (n) -Zeitkomplexität?

13

Ich habe gerade gelesen, dass die Ausführungszeit der Anfügungsoperation für ein List(: +) linear mit der Größe des wächst List.

Das Anhängen an a Listscheint eine ziemlich häufige Operation zu sein. Warum sollte die idiomatische Vorgehensweise darin bestehen, die Komponenten voranzustellen und dann die Liste umzukehren? Es kann auch kein Designfehler sein, da die Implementierung jederzeit geändert werden kann.

Aus meiner Sicht sollte sowohl das Voranstellen als auch das Anhängen O (1) sein.

Gibt es einen berechtigten Grund dafür?

DPM
quelle
2
Hängt von Ihrer Definition von "legitim" ab. Scala befasst sich intensiv mit unveränderlichen Datenstrukturen, allgegenwärtigen anonymen Listen, funktionaler Zusammensetzung usw. Die Standard-Listenimplementierung (ohne einen zusätzlichen veränderlichen Zeiger auf das Listenende) eignet sich gut für diesen Stil. Wenn Sie eine leistungsfähigere Liste benötigen, ist es zumindest sehr einfach, einen eigenen Container zu schreiben, der sich kaum von den Standardcontainern unterscheidet.
Kilian Foth
1
Standortübergreifend - Anhängen eines Elements an das Ende einer Liste in Scala - Es gibt einige Details zur Art der Liste . Es scheint, dass eine Liste in Scala unveränderlich ist, daher müssen Sie sie kopieren. Dies ist O (N).
Sie können eine der vielen verfügbaren veränderlichen Datenstrukturen oder unveränderliche Datenstrukturen mit der von Scala bereitgestellten O (1) -Anfügungszeit (Vector) verwenden. List[T]Es wird davon ausgegangen, dass Sie es so verwenden, wie Sie es in einer reinen funktionalen Sprache tun würden. In der Regel wird vom Kopf aus mit Dekonstruktion und Präfixen gearbeitet.
KChaloux
3
Voranstellen setzt den nächsten Knotenzeiger des neuen Kopfes auf die vorhandene unveränderliche Liste - was sich nicht ändern kann. Das ist O (1).
1
Für die wegweisende Arbeit und Analyse zum allgemeinen Thema Datenstrukturkomplexitätsmessungen in Pure FP lesen Sie die These von Okasaki, die später als Buch veröffentlicht wurde. Es ist eine hoch angesehene und sehr gute Lektüre für jeden, der FP lernt, um zu verstehen, wie man über die Organisation von Daten in FP nachdenkt. Es ist auch gut geschrieben und einfach zu lesen und zu befolgen.
Jimmy Hoffa

Antworten:

24

Ich werde meinen Kommentar etwas erweitern. Die List[T]Datenstruktur von scala.collection.immutableist so optimiert, dass sie so funktioniert, wie eine unveränderliche Liste in einer rein funktionalen Programmiersprache. Es hat sehr schnelle Vorlaufzeiten und es wird davon ausgegangen, dass Sie für fast alle Ihre Zugriffe am Kopf arbeiten werden.

Unveränderliche Listen haben sehr schnelle Vorlaufzeiten, da sie ihre verknüpften Listen als eine Reihe von "Cons-Zellen" modellieren. Die Zelle definiert einen einzelnen Wert und einen Zeiger auf die nächste Zelle (klassischer einfach verknüpfter Listenstil):

Cell [Value| -> Nil]

Wenn Sie einer Liste voranstellen, erstellen Sie eigentlich nur eine einzelne neue Zelle, auf die der Rest der vorhandenen Liste verweist:

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

Da die Liste unveränderlich ist, können Sie dies ohne Kopieren tun . Es besteht keine Gefahr, dass sich die alte Liste ändert und alle Werte in Ihrer neuen Liste ungültig werden. Als Kompromiss verlieren Sie jedoch die Möglichkeit, einen veränderlichen Zeiger auf das Ende Ihrer Liste zu setzen.

Dies eignet sich sehr gut für die rekursive Bearbeitung von Listen. Angenommen, Sie haben Ihre eigene Version von 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)
  }
}

Dies ist eine rekursive Funktion, die ausschließlich vom Anfang der Liste aus funktioniert und den Pattern Matching über den :: Extractor nutzt. In Sprachen wie Haskell sieht man so viel.

Wenn Sie wirklich schnelle Anhänge wünschen, bietet Scala eine Vielzahl von veränderlichen und unveränderlichen Datenstrukturen zur Auswahl. Auf der veränderlichen Seite könnten Sie nachsehen ListBuffer. Alternativ hat Vectorab scala.collection.immutableeine schnelle Append-Zeit.

KChaloux
quelle
jetzt verstehe ich! Es macht vollkommen Sinn.
DPM
Ich kenne keine Scala, aber ist das nicht elseeine Endlosschleife? Ich denke, es sollte so etwas sein x::deleteIf(xs)(f).
Svick
@svick Äh ... ja. Ja ist es. Ich schrieb es schnell und verifizierte meinen Code nicht, weil ich eine Besprechung hatte, zu der ich gehen konnte: p (Sollte jetzt behoben werden!)
KChaloux
@Jubbat Da headund tailZugang mit dieser Art von Liste ist sehr schnell - schneller als jede Hash-basierte Karte oder Array - es ist eine ausgezeichnete Art für rekursive Funktionen. Dies ist einer der Gründe, warum Listen in den meisten funktionalen Sprachen (z. B. Haskell oder Scheme) ein zentraler Typ sind
11.
Hervorragende Antwort. Ich würde vielleicht einen TL; DR hinzufügen, der einfach sagt "weil Sie voranstellen sollten, nicht anhängen" (könnte helfen, die Grundannahmen zu klären, die die meisten Entwickler über Lists und das Anhängen / Voranstellen haben).
Daniel B