2016-08-02 14 views
1

sah ich dieses Beispiel in „Programming in Scala“ Kapitel 24 „Sammlungen in der Tiefe“. Dieses Beispiel zeigt zwei alternative Möglichkeiten, um einen Baum zu implementieren:Verkettung von Iteratoren

  1. von Traversable[Int] erstreckt - hier die Komplexität der def foreach[U](f: Int => U): Unit wäre O(N).

  2. durch Erweitern Iterable[Int] - hier wäre die Komplexität def iterator: Iterator[Int] wäre O(N log(N)). Diese

ist es zu zeigen, warum es hilfreich wäre, zwei separate Züge zu haben, Traversable und Iterable.

sealed abstract class Tree 
    case class Branch(left: Tree, right: Tree) extends Tree 
    case class Node(elem: Int) extends Tree 

    sealed abstract class Tree extends Traversable[Int] { 
    def foreach[U](f: Int => U) = this match { 
    case Node(elem) => f(elem) 
    case Branch(l, r) => l foreach f; r foreach f 
    } 
    } 

    sealed abstract class Tree extends Iterable[Int] { 
    def iterator: Iterator[Int] = this match { 
     case Node(elem) => Iterator.single(elem) 
     case Branch(l, r) => l.iterator ++ r.iterator 
    } 
    } 

die Umsetzung foreach Bezug sie sagen:

ein ausgeglichener Baum nimmt im Baum proportional zur Anzahl der Elemente Zeit durchquert. Um dies zu sehen, der Ansicht, dass für einen ausgeglichenen Baum mit N Blätter werden Sie N - 1 inneren Knoten der Klasse Niederlassung haben. So die Gesamtzahl der Schritte, um den Baum zu durchlaufen ist N + N - 1.

, die Sinn macht. :)

jedoch erwähnen sie, dass die Verknüpfung der beiden Iteratoren im iterator Methode Zeitkomplexität von log(N) hat, so dass die Gesamtkomplexität des Verfahrens wäre N log(N):

Jedes Mal, wenn ein Element erzeugt wird, durch einen verketteten Iterator, wie zum Beispiel l.iterator ++ r.iterator, muss die Berechnung einer Indirektion folgen, um den richtigen Iterator zu erhalten (entweder l.iterator oder r.iterator). Alles in allem macht das log(N) Indirektionen zu einem Blatt eines ausgewogenen Baumes mit N Blättern. So sind die Kosten alle Elemente eines Baumes Besuch ging von etwa bis 2N für die foreach Traversal-Methode N log(N) für die Traversal mit iterator.

????

Warum wird die Berechnung des verketteten Iterator Bedarf an einem Blatt der linken oder rechten Iterator zu bekommen?

+0

Nicht sicher, dass ich Ihre Frage verstehe. Der verkettete Iterator muss ** jedes ** Element (Blatt) darunter erreichen. Jedes einzelne Blatt wird entweder vom "L-Zeichen" oder vom "R-Zeichen" geliefert. Die Anzahl der Umwege, die durchlaufen werden, um zu einem Blatt zu gelangen, ist genau die Tiefe des Baumes, wenn der Baum ausgeglichen ist. Also für N Blätter, Log (N) Traversalen, um zu irgendeinem bestimmten Blatt zu kommen. – jwvh

+0

@jwvh meine Frage aktualisiert. Wie Sie sehen können, besucht die 'foreach' Methode auch jedes Blatt. Aber die Gesamtkomplexität ist nur O (N). Während die Komplexität von "Iterator" ist "O (N log (N))". Offensichtlich trägt die Anwendung von "++" auf "Iteratoren" zur Komplexität bei. – rapt

Antwort

0

In dieser Implementierung weiß der oberste Zweig NICHT, wie viele Elemente es in seinen Unterzweigen left und right gibt.

Daher ist der Iterator rekursiv mit der Kluft gebaut und Ansatz erobern, die eindeutig in der iterator Methode dargestellt wird - erhalten Sie zu jedem Knoten (case Branch), erzeugen Sie den Iterator des einzelnen Knotens case Node => ... und dann Sie beitreten Sie.

Ohne in jeden einzelnen Knoten zu gelangen, würde er nicht wissen, welche Elemente dort sind und wie der Baum strukturiert ist (ungerade Zweige erlaubt, nicht erlaubt usw.).

EDIT: Werfen wir einen Blick in die ++ Methode auf Iterator.

def ++[B >: A](that: => GenTraversableOnce[B]): Iterator[B] = new Iterator.JoinIterator(self, that) 

und dann bei Iterator.JoinIterator

private[scala] final class JoinIterator[+A](lhs: Iterator[A], that: => GenTraversableOnce[A]) extends Iterator[A] { 
    private[this] var state = 0 // 0: lhs not checked, 1: lhs has next, 2: switched to rhs 
    private[this] lazy val rhs: Iterator[A] = that.toIterator 
    def hasNext = state match { 
     case 0 => 
     if (lhs.hasNext) { 
      state = 1 
      true 
     } else { 
      state = 2 
      rhs.hasNext 
     } 
     case 1 => true 
     case _ => rhs.hasNext 
    } 
    def next() = state match { 
     case 0 => 
     if (lhs.hasNext) lhs.next() 
     else { 
      state = 2 
      rhs.next() 
     } 
     case 1 => 
     state = 0 
     lhs.next() 
     case _ => 
     rhs.next() 
    } 

    override def ++[B >: A](that: => GenTraversableOnce[B]) = 
     new ConcatIterator(this, Vector(() => that.toIterator)) 
    } 

Von dass wir das Verbinden Iteratoren nur eine rekursive Struktur auf dem Gebiet rhs schafft sehen können. Konzentrieren wir uns darüber hinaus ein bisschen mehr. Betrachten wir eine noch Baum mit Struktur level1 [A]; level2 [B][C]; level 3[D][E][F][F]

Wenn Sie JoinIterator auf dem Iterator rufen Sie die vorhandene lhs Iterator bewahren. Sie haben jedoch immer .toIterator auf rhs. Das bedeutet, dass für jede nachfolgende Ebene der rhs Teil rekonstruiert wird. Also für B ++ C bekommst du das wie A.lhs (stands for B) und A.rhs (stands for C.toIterator) wo C.toIterator für C.lhs and C.rhs etc. steht. Also die zusätzliche Komplexität.

Ich hoffe, dies beantwortet Ihre Frage.

+0

Ich denke nicht, dass dies die Frage beantwortet - natürlich sollte jeder Knoten mindestens einmal erreicht werden - aber das würde Zeitkomplexität von O (N) ergeben, wie sie in der alternativen "Traversable [Int]" - Implementierung erklärt haben. Sobald jedoch die beiden Unterzweige berechnet sind, sagen sie, dass das Anwenden von "++" auf sie "log (N)" -Operationen erfordern würde (und ich verstehe nicht, warum sie das sagen). Die gesamte Komplexität der "Iterator" -Methode wäre also N log (N).Hier ist N die Anzahl der Blätter (Knoten), daher sind die gesamten Elemente im Baum ≈ 2N. – rapt

+0

Daher ist die Frage nicht richtig strukturiert, da Sie wissen, warum Sie auf jedes Element zugreifen müssen. Sie fragen stattdessen nach der zeitlichen Komplexität. Sie sollten die ursprüngliche Frage aktualisieren, damit sie die Leute nicht mehr verwirrt. – sebszyller

+0

Ich entschuldige mich für das Missverständnis. Ich glaube, ich fragte, warum die Verkettung zweier Iteratoren O (log (N)) kostet, nicht warum sie produziert werden. Wie auch immer, ich habe meine Frage aktualisiert. – rapt

2

Das Wortspiel auf "Sammlungen in Tiefe" ist apt. Die Tiefe der Datenstruktur ist wichtig.

Wenn Sie top.iterator.next(), die jeweils innere Branch Delegierten der Iterator der Branch oder Node darunter, eine Verbindungskette aufrufen, die log(N) ist.

Diese Anrufkette entsteht bei jedem next().

Mit foreach, besuchen Sie nur einmal Branch oder Node.

Edit: Nicht sicher, ob dies hilft, aber hier ist ein Beispiel für eifrig die Blätter zu finden, aber faul produzieren die Werte. In älteren Versionen von Scala würde Stackoverflow auftreten oder langsamer sein, aber die Implementierung von Chained ++ wurde verbessert. Jetzt ist es eine flache Kette, die kürzer wird, wenn sie verbraucht wird.

sealed abstract class Tree extends Iterable[Int] { 
    def iterator: Iterator[Int] = { 
    def leafIterator(t: Tree): List[Iterator[Int]] = t match { 
     case Node(_) => t.iterator :: Nil 
     case Branch(left, right) => leafIterator(left) ::: leafIterator(right) 
    } 
    this match { 
     case n @ Node(_) => Iterator.fill(1)(n.value) 
     case Branch(left @ Node(_), right @ Node(_)) => left.iterator ++ right.iterator 
     case b @ Branch(_, _) => 
     leafIterator(b).foldLeft(Iterator[Int]())((all, it) => all ++ it) 
    } 
    } 
} 

case class Branch(left: Tree, right: Tree) extends Tree { 
    override def toString = s"Branch($left, $right)" 
} 
case class Node(elem: Int) extends Tree { 
    def value = { 
    Console println "An expensive leaf calculation" 
    elem 
    } 
    override def toString = s"Node($elem)" 
} 

object Test extends App { 
    // many leaves 
    val n = 1024 * 1024 
    val ns: List[Tree] = (1 to n).map(Node(_)).toList 
    var b = ns 
    while (b.size > 1) { 
    b = b.grouped(2).map { case left :: right :: Nil => Branch(left, right) }.toList 
    } 
    Console println s"Head: ${b.head.iterator.take(3).toList}" 
} 
+0

In dem Code, den ich zitiert habe, wo siehst du einen Aufruf von 'iterator.next()'? – rapt

+0

"Jedes Mal, wenn ein Element von einem verketteten Iterator erzeugt wird" ist eine andere Art, "next" zu sagen. Vielleicht bedeutet dein Satz "die Berechnung des verketteten Iterators", dass du nur an den Iterator denkst (der nur linksbündig geht, da '++' einen by-name-Parameter annimmt. Ich denke, "alle Elemente zu besuchen" bedeutet auch "Erzeuge alle Werte", indem ich sie überspringe. –

+0

Willst du damit sagen, dass Iterator.next() aufgerufen wird, wenn der Iterator für den Baum erzeugt wird (dh wenn Iterator. ++ (...) aufgerufen wird)? nicht so aus dem Code (Sebszyller's Antwort). Es sieht aus wie Tree.foreach (...) und Tree.iterator() haben die gleiche Komplexität, O (2N) = O (N), stimmen Sie damit überein? – rapt