4.4 Range-Trees
Motivation (Folien 22–23)
Für großes und kleines ist die Anfragezeit des kd-Baums noch relativ groß. Range-Trees schaffen , zahlen dafür aber mit mehr Speicher.
| Prinzip | Vergleiche von und … |
|---|---|
| kd-Baum | … abwechselnd (pro Ebene eine andere Koordinate) |
| Range-Tree | … nacheinander (erst alle , dann alle ) |
Idee:
- Erst : Ein balancierter Suchbaum über den -Koordinaten. Eine 1d-Bereichsanfrage auf liefert alle Kandidaten.
- Dann : Unter den Kandidaten per 1d-Bereichsanfrage die mit heraussuchen.
Damit das schnell geht, dürfen die Kandidaten nicht einzeln aufgezählt werden (das wären evtl. ). Stattdessen kommen sie als wenige vorbereitete Teilmengen, für die es schon -Suchbäume gibt.
4.4.1 1d-Bereichssuche (Folien 24–28)
Ausgangspunkt: ein balancierter, blattorientierter binärer Suchbaum. Die Punkte stehen in den Blättern, jeder innere Knoten speichert einen Schlüssel = größter Wert seines linken Teilbaums. Suche: bei nach links, sonst nach rechts.
Idee: Nach und gleichzeitig suchen.
- Den Knoten finden, an dem sich die Suchpfade zu und trennen.
- Von dem Pfad zu folgen. Wo der Pfad nach links abbiegt, liegt der ganze rechte Teilbaum im Bereich → komplett ausgeben.
- Von dem Pfad zu folgen. Wo der Pfad nach rechts abbiegt, liegt der ganze linke Teilbaum im Bereich → komplett ausgeben.
- Die beiden Blätter am Ende der Pfade einzeln prüfen.
Algorithmus 3: FindSplitNode(T, xl, xr)
1: v = Root(T);
2: while (v ist kein Blatt && (xl > xv || xr ≤ xv)) {
3: if (xr ≤ xv) v = LeftChild(v); // beide Pfade gehen nach links
4: else v = RightChild(v); // beide Pfade gehen nach rechts
5: }
6: return v;
Algorithmus 4: 1dRangeSearch(T, xl, xr)
1: vsplit = FindSplitNode(T, xl, xr);
2: if (vsplit ist Blatt) gib den Punkt aus, falls er in [xl, xr] liegt;
3: else {
4: v = LeftChild(vsplit); // Pfad zu xl
5: while (v ist kein Blatt) {
6: if (xl ≤ xv) {
7: ReportSubtree(RightChild(v));
8: v = LeftChild(v);
9: } else v = RightChild(v);
10: }
11: gib den Punkt in v aus, falls nötig;
12: // analog mit RightChild(vsplit) und xr (gespiegelt)
13: }
Beispiel (eigenes, kleiner als auf Folie 24)
Blätter 3, 10, 19, 23, 30, 37, 49, 59. Innere Knoten tragen den größten Wert ihres linken Teilbaums:
23
┌────────┴────────┐
10 37
┌───┴───┐ ┌───┴───┐
3 19 30 49
/ \ / \ / \ / \
3 10 19 23 30 37 49 59
Anfrage :
- Split-Knoten: Wurzel 23, denn .
- Pfad zu 9: Knoten 10: → rechten Teilbaum ausgeben, nach links. Knoten 3: → nach rechts, Blatt 10 ∈ Bereich → ausgeben.
- Pfad zu 37: Knoten 37: → linken Teilbaum ausgeben, nach rechts. Knoten 49: → nach links, Blatt 49 ∉ Bereich.
- Ergebnis: 10, 19, 23, 30, 37 ✔
Satz 1 (Folie 28)
1d-Bereichssuche geht mit Speicher, Vorverarbeitung und pro Anfrage.
Beweis:
- Vorverarbeitung: Punkte in einem balancierten Suchbaum (z.B. AVL) speichern: Speicher , Aufbau .
- Anfrage: finden und den zwei Pfaden folgen kostet , weil der Baum balanciert ist und pro Knoten anfällt.
ReportSubtreeist linear in der Zahl der ausgegebenen Punkte, zusammen (ein Teilbaum mit Blättern hat innere Knoten). Gesamt . □
Wichtige Nebenbeobachtung: Die Antwort setzt sich aus disjunkten Teilbäumen zusammen (höchstens einer pro Knoten auf den zwei Pfaden). Genau das nutzt der 2d-Range-Tree.
2d-Range-Tree (Folien 29–31)
Kanonische Teilmengen
Im -Baum repräsentiert jeder Knoten die Punkte in den Blättern seines Teilbaums, die kanonische Teilmenge .
Eine 1d-Anfrage auf liefert das Ergebnis als Vereinigung von kanonischen Teilmengen. Von diesen Punkten braucht man nur die mit . Also speichert man für jedes schon einen -Suchbaum.
Aufbau
- Hauptstruktur (first level): balancierter binärer Suchbaum über den -Koordinaten.
- In jedem inneren Knoten : ein balancierter binärer Suchbaum über den -Koordinaten der Punkte in , die assoziierte Struktur (second level). Ihre Blätter enthalten die Punkte.
Satz 2: Speicher und Aufbau
Ein Range-Tree für Punkte in 2d braucht Speicher und kann in aufgebaut werden.
Beweis:
- Speicher: Auf jeder Ebene von liegt jeder Punkt in genau einer assoziierten Struktur (die kanonischen Teilmengen einer Ebene sind disjunkt). Jede assoziierte Struktur braucht linearen Speicher, pro Ebene also . hat Ebenen ⇒ .
- Aufbau: Die Punkte einmal nach und einmal nach vorsortieren. Die Bäume dann von unten nach oben bauen: Die nach sortierte Liste eines Knotens entsteht durch Mischen (Merge) der Listen seiner Kinder in linearer Zeit. Aus einer sortierten Liste baut man einen balancierten Baum in . Pro Ebene , also insgesamt . (Würde man jede assoziierte Struktur neu sortieren, käme man auf .) □
Anfrage
Algorithmus 5: 2dRangeSearch(T, [xl, xr] × [yl, yr])
1: vsplit = FindSplitNode(T, xl, xr);
2: if (vsplit ist Blatt) gib den Punkt aus, falls er im Bereich liegt;
3: else {
4: v = LeftChild(vsplit);
5: while (v ist kein Blatt) {
6: if (xl ≤ xv) {
7: 1dRangeSearch(Tassoc(RightChild(v)), yl, yr); // statt ReportSubtree
8: v = LeftChild(v);
9: } else v = RightChild(v);
10: }
11: prüfe den Punkt in v;
12: // analog mit RightChild(vsplit) und xr
13: }
Der einzige Unterschied zur 1d-Suche: Statt einen Teilbaum komplett auszugeben, fragt man dessen assoziierten -Baum nach ab.
Satz 3: Anfragezeit (Folie 33)
Eine Anfrage in einem Range-Tree mit Punkten in 2d kostet .
Beweis:
- Jeder Aufruf von
1dRangeSearchin einem Knoten kostet , mit = Anzahl der Punkte in . Weil die kanonischen Teilmengen disjunkt sind, gilt . - Aufgerufen wird nur an Knoten auf den beiden Suchpfaden, das sind Stück.
- Also . □
Höhere Dimensionen (Folie 34)
Rekursiv: Die assoziierte Struktur eines -dimensionalen Range-Trees ist ein -dimensionaler Range-Tree über den restlichen Koordinaten.
| d-Range-Tree | |
|---|---|
| Aufbau | |
| Anfrage | |
| Speicher |
Jede zusätzliche Dimension kostet einen Faktor bei allen drei Größen.
Vergleich (Folie 35)
| kd-Baum 2d | kd-Baum d | Range-Tree 2d | Range-Tree d | |
|---|---|---|---|---|
| Speicher | ||||
| Aufbau | ||||
| Anfrage |
Faustregel:
- Wenig Speicher, einfache Implementierung, auch für Nächste-Nachbarn → kd-Baum.
- Sehr viele Anfragen, kleines , Speicher egal → Range-Tree.
Fractional Cascading (Ergänzung, nicht auf den Folien)
Die assoziierten Suchen fragen alle dasselbe Intervall ab. Ersetzt man die assoziierten Bäume durch sortierte Arrays und speichert in jedem Eintrag einen Zeiger auf die passende Position im Array des Kindes, muss man nur einmal binär suchen und kann danach den Zeigern folgen. Das senkt die Anfragezeit auf in 2d (allgemein ).