Zum Hauptinhalt springen

4.4 Range-Trees

Motivation (Folien 22–23)​

Für großes nn und kleines RR ist die Anfragezeit O(n+R)O(\sqrt n+R) des kd-Baums noch relativ groß. Range-Trees schaffen O(log⁡2n+R)O(\log^2 n + R), zahlen dafür aber mit mehr Speicher.

PrinzipVergleiche von xx und yy …
kd-Baum… abwechselnd (pro Ebene eine andere Koordinate)
Range-Tree… nacheinander (erst alle xx, dann alle yy)

Idee:

  1. Erst xx: Ein balancierter Suchbaum über den xx-Koordinaten. Eine 1d-Bereichsanfrage auf [xl,xr][x_l,x_r] liefert alle Kandidaten.
  2. Dann yy: Unter den Kandidaten per 1d-Bereichsanfrage die mit y∈[yl,yr]y\in[y_l,y_r] heraussuchen.

Damit das schnell geht, dürfen die Kandidaten nicht einzeln aufgezählt werden (das wären evtl. Θ(n)\Theta(n)). Stattdessen kommen sie als wenige vorbereitete Teilmengen, für die es schon yy-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 vv speichert einen Schlüssel xvx_v = größter Wert seines linken Teilbaums. Suche: bei x≤xvx\le x_v nach links, sonst nach rechts.

Idee: Nach xlx_l und xrx_r gleichzeitig suchen.

  1. Den Knoten vsplitv_\text{split} finden, an dem sich die Suchpfade zu xlx_l und xrx_r trennen.
  2. Von vsplitv_\text{split} dem Pfad zu xlx_l folgen. Wo der Pfad nach links abbiegt, liegt der ganze rechte Teilbaum im Bereich → komplett ausgeben.
  3. Von vsplitv_\text{split} dem Pfad zu xrx_r folgen. Wo der Pfad nach rechts abbiegt, liegt der ganze linke Teilbaum im Bereich → komplett ausgeben.
  4. 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 [9,37][9, 37]:

  • Split-Knoten: Wurzel 23, denn 9≤23<379\le23<37.
  • Pfad zu 9: Knoten 10: 9≤109\le10 → rechten Teilbaum {19,23}\{19,23\} ausgeben, nach links. Knoten 3: 9>39>3 → nach rechts, Blatt 10 ∈ Bereich → ausgeben.
  • Pfad zu 37: Knoten 37: 37≥3737\ge37 → linken Teilbaum {30,37}\{30,37\} ausgeben, nach rechts. Knoten 49: 37<4937<49 → nach links, Blatt 49 ∉ Bereich.
  • Ergebnis: 10, 19, 23, 30, 37 ✔

Satz 1 (Folie 28)​

Satz 1

1d-Bereichssuche geht mit O(n)O(n) Speicher, O(nlog⁡n)O(n\log n) Vorverarbeitung und O(R+log⁡n)O(R+\log n) pro Anfrage.

Beweis:

  • Vorverarbeitung: Punkte in einem balancierten Suchbaum (z.B. AVL) speichern: Speicher O(n)O(n), Aufbau O(nlog⁡n)O(n\log n).
  • Anfrage: vsplitv_\text{split} finden und den zwei Pfaden folgen kostet O(log⁡n)O(\log n), weil der Baum balanciert ist und pro Knoten O(1)O(1) anfällt. ReportSubtree ist linear in der Zahl der ausgegebenen Punkte, zusammen O(R)O(R) (ein Teilbaum mit mm Blättern hat m−1m-1 innere Knoten). Gesamt O(R+log⁡n)O(R+\log n). □

Wichtige Nebenbeobachtung: Die Antwort setzt sich aus O(log⁡n)O(\log n) 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 xx-Baum TT repräsentiert jeder Knoten vv die Punkte in den Blättern seines Teilbaums, die kanonische Teilmenge P(v)P(v).

Eine 1d-Anfrage auf [xl,xr][x_l,x_r] liefert das Ergebnis als Vereinigung von O(log⁡n)O(\log n) kanonischen Teilmengen. Von diesen Punkten braucht man nur die mit y∈[yl,yr]y\in[y_l,y_r]. Also speichert man für jedes P(v)P(v) schon einen yy-Suchbaum.

Aufbau​

  • Hauptstruktur (first level): balancierter binärer Suchbaum TT über den xx-Koordinaten.
  • In jedem inneren Knoten vv: ein balancierter binärer Suchbaum über den yy-Koordinaten der Punkte in P(v)P(v), die assoziierte Struktur Tassoc(v)T_\text{assoc}(v) (second level). Ihre Blätter enthalten die Punkte.

Satz 2: Speicher und Aufbau​

Satz 2

Ein Range-Tree für nn Punkte in 2d braucht O(nlog⁡n)O(n\log n) Speicher und kann in O(nlog⁡n)O(n\log n) aufgebaut werden.

Beweis:

  • Speicher: Auf jeder Ebene von TT liegt jeder Punkt in genau einer assoziierten Struktur (die kanonischen Teilmengen einer Ebene sind disjunkt). Jede assoziierte Struktur braucht linearen Speicher, pro Ebene also O(n)O(n). TT hat O(log⁡n)O(\log n) Ebenen ⇒ O(nlog⁡n)O(n\log n).
  • Aufbau: Die Punkte einmal nach xx und einmal nach yy vorsortieren. Die Bäume dann von unten nach oben bauen: Die nach yy 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 O(m)O(m). Pro Ebene O(n)O(n), also insgesamt O(nlog⁡n)O(n\log n). (Würde man jede assoziierte Struktur neu sortieren, käme man auf O(nlog⁡2n)O(n\log^2 n).) □

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 yy-Baum nach [yl,yr][y_l,y_r] ab.

Satz 3: Anfragezeit (Folie 33)​

Satz 3

Eine Anfrage in einem Range-Tree mit nn Punkten in 2d kostet O(R+log⁡2n)O(R+\log^2 n).

Beweis:

  • Jeder Aufruf von 1dRangeSearch in einem Knoten vv kostet O(Rv+log⁡n)O(R_v+\log n), mit RvR_v = Anzahl der Punkte in D∩P(v)D\cap P(v). Weil die kanonischen Teilmengen disjunkt sind, gilt ∑Rv=R\sum R_v = R.
  • Aufgerufen wird nur an Knoten auf den beiden Suchpfaden, das sind O(log⁡n)O(\log n) Stück.
  • Also ∑vO(Rv+log⁡n)=O(R+log⁡n⋅log⁡n)=O(R+log⁡2n)\sum_v O(R_v+\log n) = O(R + \log n\cdot\log n) = O(R+\log^2 n). □

Höhere Dimensionen (Folie 34)​

Rekursiv: Die assoziierte Struktur eines kk-dimensionalen Range-Trees ist ein (k−1)(k-1)-dimensionaler Range-Tree über den restlichen Koordinaten.

kkd-Range-Tree
AufbauO(nlog⁡k−1n)O(n\log^{k-1} n)
AnfrageO(R+log⁡kn)O(R+\log^k n)
SpeicherO(nlog⁡k−1n)O(n\log^{k-1} n)

Jede zusätzliche Dimension kostet einen Faktor log⁡n\log n bei allen drei Größen.

Vergleich (Folie 35)​

kd-Baum 2dkd-Baum kkdRange-Tree 2dRange-Tree kkd
SpeicherO(n)O(n)O(n)O(n)O(nlog⁡n)O(n\log n)O(nlog⁡k−1n)O(n\log^{k-1}n)
AufbauO(nlog⁡n)O(n\log n)O(k nlog⁡n)O(k\,n\log n)O(nlog⁡n)O(n\log n)O(nlog⁡k−1n)O(n\log^{k-1}n)
AnfrageO(R+n)O(R+\sqrt n)O(R+k n1−1/k)O(R+k\,n^{1-1/k})O(R+log⁡2n)O(R+\log^2 n)O(R+log⁡kn)O(R+\log^k n)

Faustregel:

  • Wenig Speicher, einfache Implementierung, auch für Nächste-Nachbarn → kd-Baum.
  • Sehr viele Anfragen, kleines RR, Speicher egal → Range-Tree.

Fractional Cascading (Ergänzung, nicht auf den Folien)​

Die O(log⁡n)O(\log n) assoziierten Suchen fragen alle dasselbe Intervall [yl,yr][y_l,y_r] 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 O(R+log⁡n)O(R+\log n) in 2d (allgemein O(R+log⁡k−1n)O(R+\log^{k-1}n)).