¢¡¤£¦¥¤§© ¤

Werbung
http://www.mpi-sb.mpg.de/~hannah/info5-ws00
IS
UN
R
S
WS 2000/2001
E R SIT
S
Schömer, Bast
SA
IV
A
Grundlagen zu
Datenstrukturen und Algorithmen
A VIE N
Lösungsvorschläge für das 8. Übungsblatt
Letzte Änderung am 18. Dezember 2000
Aufgabe 1
(a) Das Minimum eines Suchbaumes befindet sich im linkesten Blatt, das Maximum im rechtesten. Um nicht jedesmal den ganzen Pfad von der Wurzel zu diesen Elementen laufen zu
müssen, legt man in der Klasse Tree zwei Membervariablen vom Typ handle an - min und
max -, die bei jedem insert geupdatet werden. insert könnte zum Beispiel so modifiziert
werden: Wenn der Weg nach unten irgendwo nach rechts abzweigt, kann das einzufügende
Element nicht das Minimum sein, wenn er nach links abzweigt, kann es nicht das Maximum
sein.
handle insert(const T& x)
{
handle u = new Tree_node<T>;
...
bool min_ = true;
bool max_ = true;
while(v!=0)
{
p=v;
if(x<v->inf)
{
v=v->left;
max_=false; // x kann nicht das Maximum sein
}
else
{
v=v->right;
min_=false; // x kann nicht das Minimum sein
}
}
...
if(max_) max = u;
if(min_) min = u;
}
Beim Runterlaufen von der Wurzel zu der Einfügestelle wird also gecheckt, ob das einzufügende Element das Miniumum oder Maximum sein kann. Dabei wird die Laufzeit of-
fensichtlich nicht verändert.
Die Funktionen minimum und maximum dagegen sind nun in konstanter Zeit ausführbar:
handle minimum()
{return min;}
handle maximum()
{return max;}
Bei einem erase muss man auch überprüfen, ob man nicht gerade das Minimum oder das
Maximum löscht:
void erase(handle v)
{
if(v==min)
if(succ(v)!=0) min=succ(v);
else min=0;
if(v==max)
if(pred(v)!=0) max=pred(v);
else max=0;
...
}
(b) Man führt für jeden Knoten v zwei int-Variablen ein (in der Klasse Tree_node): l für die
Anzahl der Knoten, die sich im linken Unterbaum von v befinden und r für die Anzahl der
Knoten im rechten Unterbaum.
Man sucht das k-t kleinste Element nun so:
Beginne bei der Wurzel.
Ist l = k − 1, so hat man das k-t kleinste Element gefunden, denn es sind gerade k − 1
Elemente kleiner als der aktuelle Knoten.
Ist l > k − 1, suche im linken Unterbaum nach dem k-t kleinsten Element.
Ist l < k − 1, suche im rechten Unterbaum nach dem k − (l + 1) kleinsten Element.
Diese Suche lässt sich offensichtlich in O(Tiefe(B))= O(log n) realisieren.
Die insert-Funktion muss angepasst werden: Für jeden Knoten auf dem Pfad von der
Wurzel zur Einfügstelle müssen l und r geupdatet werden. Das verändert die Laufzeit von
insert nicht, da dieser Weg sowieso abgegangen werden muss.
handle insert(const T& x)
{
...
while (v != 0)
{
p = v;
if (x < v->inf)
{
v = v->left;
p->l++; // Der linke Unterbaum erh"alt ein Element mehr
}
else
{
v = v->right;
p->r++; // Der rechte Unterbaum erh"alt ein Element mehr
}
}
}
Auch erase(handle v) muss verändert werden. Für alle Knoten p auf dem Pfad von u
(ist gleich v bzw. succ(v), falls v zwei Nachfolger hat) nach oben zur Wurzel muss l bzw. r
angepasst werden:
void erase(handle v)
{
...
handle p=u;
while(p!=0)
{
handle q=p->parent;
if(q)
{
if(p==q->left)
q->l--;
else
q->r--;
p=q;
}
}
delete u;
}
(c) Suche mit einer leicht veränderten find-Funktion den kleinsten Knoten, der größer oder
gleich a ist sowie den größten Knoten, der kleiner oder gleich b ist. Das geht in 2 · O(log n)
= O(log n) Zeit. Diese beiden Knoten, nennen wir sie u und v, liegen in einem kleinsten
Unterbaum von B. Auf dem Pfad von u zur Wurzel dieses Unterbaums muss man nun die
r-Werte aller Knoten ≥ a aufaddieren (mit Ausnahme der Wurzel). Auf dem Pfad von v
zur Wurzel des Unterbaumes muss man die l-Werte aller Knoten ≤ b aufaddieren (mit Ausnahme der Wurzel). Zählt man noch die Knoten, die auf diesen Pfaden liegen und ≥ a bzw.
≤ b sind, dazu (die Wurzel nur einmal), so erhält man die Anzahl der Knoten, die zwischen
a und b liegen. Das geht in 2 · O(Länge des längeren Pfades) = O(log n). Also insgesamt
O(log n).
/* Hier sollte noch hin, wie man die Wurzel findet (analog zu find() in 2c)?) */
Um jetzt alle diese Elemente auch zurückzugeben, muss man rekursiv die rechten Unterbäume der Knoten ≥ a auf dem linken Pfad sowie die linken Unterbäume der Knoten
≤ b auf dem rechten Pfad durchlaufen. Das braucht natürlich soviel Zeit, wieviel Elemente
dort gespeichert sind. Also benötigt diese Funktion T (find) +O(Anzahl Elemente zwischen
a und b) = O(k+log n) (mit k =Anz. El. zw. a und b).
Aufgabe 2
a) Sei p = (x, y) und v0 , . . . , vk der Pfad von der Wurzel v0 zu dem Knoten vk mit vk → inf = x.
Die x-Koordinate x von p kommt genau in den Unterbäumen mit Wurzel v0 , . . . , vk vor.
Also steht y genau in den sekundären Datenstrukturen von v0 , . . . , vk . Es ist k =Tiefe(x) =
O(log n).
Jedes y kommt also O(log n) mal vor. Damit hat man einen Platzbedarf von O(n log n).
b) Annahme: Alle Zahlen sind verschieden, d.h. es treten keine zwei gleiche x-Koordinaten und
keine zwei gleich y-Koordinaten auf.
Man definiert eine Klasse ext_Tree_node, die zusätzlich zu dem Inhalt (die x-Koordinate)
noch die y-Koordinate und einen Zeiger auf eine Baum (die sekundäre Datenstruktur)
enthält:
template <typename T>
class ext_Tree_node
{
public:
typedef ext_Tree_node<T>* ext_handle;
T inf;
T sec_inf;
Tree<T>* sec_tree;
ext_handle left, right, parent;
};
Weiter definiert man die Klasse ext_Tree, die als Knoten statt handle eben ext_handle
hat. Diese erweiterte Klasse stellt dann die primäre Datenstruktur dar.
Damit der (primäre) Baum beim Einfügen und Löschen balanciert bleibt, muß man die
Methoden rotate_right und rotate_left zur Verfügung stellen. Beim Rotieren muß man
aufpassen, da man die sekundären Datenstrukturen verändern muß. Diese beiden Methoden
waren aber nicht verlangt und werden hier deshalb nicht gezeigt.
insert: p = (x, y) soll eingefügt werden. Man geht von der Wurzel aus und sucht das Blatt,
bei dem x eingefügt werden soll. Bei jedem Knoten, den man erreicht, fügt man y in die
sekundäre Datenstruktur ein. Das ist korrekt, da x in jedem Unterbaum auf dem Pfad nach
unten enthalten ist.
ext_handle insert(const float& x, const float& y)
{
// erzeuge den neuen ext_handle
ext_handle u = new ext_Tree_node<float>;
u->inf = x;
u->sec_inf = y;
u->sec_tree = new Tree<float>;
u->sec_tree->insert(y);
// suche das Blatt, an dem x angefuegt wird.
ext_handle v = root;
ext_handle p = 0;
while(v!=0)
{
p = v;
v->sec_tree->insert(y); // fuege y in sec_tree von v ein
if(x<v->inf) v = v->left;
else v = v->right;
//Annahme: alle Zahlen verschieden
}
u->parent = p;
// p ist jetzt der Vater von dem neuen u
if(p==0) root = u; // evtl. Wurzel
else
// sonst: u ist Nachfolger von p
{
if(x<p->inf) p->left = u;
else p->right = u;
}
return u;
}
Das Einfügen von y in die sekundäre Datenstruktur geht in O(log n) Zeit. Dies wird auf
einem Weg von der Wurzel zu einem Blatt getan, also O(log n) mal. Damit hat man (ohne
Rotation) O(log2 n) Zeit.
erase: Der Knoten v soll gelöscht werden. Man geht zuerst von v aus hoch zur Wurzel und
löscht aus allen sekundären Datenstrukturen das y raus. Danach wird gelöscht wie für Tree.
In einem Fall muß man allerdings aufpassen. Falls das v zwei Nachfolger hat, so wird in
erase() der Knoten v mit succ(v) vertauscht und dann succ(v) geloescht. In diesem
Fall muß man natürlich auch die ext_trees berichtigen. D.h. die y-Koordinate des alten
succ(v) muß auf dem Pfad vom alten succ(v) bis zur Wurzel aus allen ext_trees raus
und auf dem Pfad von dem alten v bis zur Wurzel in alle ext_trees rein (geht vermutlich
auch etwas effizienter?).
void erase{ext_handle v}
{
/* die zu loeschende y-Koordinate */
T y = v->sec_inf;
/* gehe von v aus nach oben und loesche ueberall y */
ext_handle p = v->parent;
handle hy;
while(p!=0)
{
hy = p->ext_tree->find(y); // finde y in ext_tree von p
p->ext_tree->erase(hy);
// und loesche es
p = p->parent;
}
/* Weiter mit erase wie in der Vorlesung fuer Tree.
*/
...
if(u!=v)
{
v->inf = u->inf;
// das steht da
/* das kommt dazu */
/* Die y-Koordinate von u->inf */
y = u->sec_inf;
/* gehe von u aus nach oben und loesche ueberall y */
p = u->parent;
while(p!=0)
{
hy = p->ext_tree->find(y); // finde y in ext_tree von p
p->ext_tree->erase(hy);
// und loesche es
p = p->parent;
}
/* gehe von v aus nach oben und fuege ueberall y ein */
p = v->parent;
while(p!=0)
{
p->ext_tree->insert(y);
// fuege y ein
p = p->parent;
}
}
delete u;
}
Das Löschen (oder Einfügen) einer y-Koordinate in jeder sekundären Datenstruktur auf
dem Pfad von einem Knoten zur Wurzel geht in O(log2 n) Zeit. Damit erhält man insgesamt
O(log2 n).
c) int find(float a, float b, float c, float d) gibt die Zahl der Punkte p = (x, y)
mit a ≤ x ≤ b und c ≤ y ≤ d zurück.
Idee: Laufe den Baum von der Wurzel aus durch und suche die Wurzel des Unterbaums,
die im Intervall [a, b] liegt. Gehe von dort aus links und rechts runter bis zu dem kleinsten
Element ≥ a bzw. zu dem größten Element ≤ b. Von Knoten auf diesem Pfad, die ≥ a (bzw.
≤ b) sind, gehen rechts (bzw. links) Teilbäume ab, deren Knoten alle in [a, b] liegen. Man
zählt dann (wie in Aufgabe 1), wieviele y-Koordinaten der sekundären Datenstrukturen
dieser Teilbäume im Intervall [c, d] liegen.
int find(float a, float b, float c, float d)
{
/* Suche die Wurzel des Teilbaums, die in [a,b] liegt. */
ext_handle wurzel = root;
while(wurzel!=0 && (wurzel->inf<a || wurzel->inf>b))
{
// die x-Koordinaten in [a,b] liegen rechts von wurzel
if(wurzel->inf < a) wurzel = wurzel->right;
// die x-Koordinaten in [a,b] liegen links von wurzel
else wurzel = wurzel->left;
}
if(wurzel==0) return(0); // keine Elemente im Intervall
/* Alle Punkte im Baum, deren x-Koordinate in [a,b] liegt, sind
im Teilbaum mit Wurzel wurzel enthalten. */
int zahl = 0 ;
/* Ist die Wurzel im Intervall? */
if(wurzel->sec_inf >= c && wurzel->sec_inf <= d) zahl++;
/* Gehe links runter */
ext_handle left_end = wurzel->left;
while(left_end!=0)
{
if(left_end->inf < a ) // gehe rechts weiter
{
left_end = left_end->right;
}
else // x-Koordinate von left_end ist drin.
{
/* Ist left_end im Intervall? */
if(left_end->sec_inf >= c && left_end->sec_inf <= d) zahl++;
/* Die x-Koordinaten des rechten Teilbaumes liegen alle in
[a,b]. Bestimme die Anzahl der y-Koordinaten, die in [c,d]
liegen. */
zahl += left_end->right->sec_tree->find(c,d);
/* Falls x=a ist, kann es keine kleinere Zahl >=a geben, da
alle Elemente verschieden sind. */
if(left_end->inf == a) break;
left_end = left_end->left; // gehe links weiter
}
}
/* Das Gleiche fuer rechts */
ext_handle right_end = wurzel->right;
while(right_end!=0)
{
if(right_end->inf > b ) // gehe links weiter
{
right_end = right_end->left;
}
else // x-Koordinate von right_end ist drin.
{
/* Ist right_end im Intervall? */
if(right_end->sec_inf >= c && right_end->sec_inf <= d) zahl++;
/* Die x-Koordinaten des linken Teilbaumes liegen alle in
[a,b]. Bestimme die Anzahl der y-Koordinaten, die in [c,d]
liegen. */
zahl += right_end->left->sec_tree->find(c,d);
/* Falls x=b ist, kann es keine groessere Zahl <=b geben, da
alle Elemente verschieden sind. */
if(right_end->inf == b) break;
right_end = right_end->right; // gehe rechts weiter
}
}
return zahl;
}
d) Speichert man an v nur die y-Koordinaten aller Punkte mit x-Koordinate v->inf, so lassen
sich insert und erase in Zeit O(log2 n) realisieren (sogar O(log n), falls man davon ausgeht,
daß keine zwei x-Koordinaten gleich sind, da es dann im Prinzip ausreicht, die x-Koordinate
zu verwalten).
Allerdings würde sich find nicht in O(log2 n) realisieren lassen. Da man die y-Koordinaten
der Knoten im Unterbaum von wurzel nicht kennt, muß man, anders als in Teil c), alle
Knoten, deren x-Koordinate im richtigen Intervall ist, testen.
Die Zahl der y-Koordinaten eines Knotens zu bestimmen, die in [c, d] liegen, geht in O(log n)
(bzw. O(1), falls alle Elemente verschieden sind).
Man muß also, ähnlich wie in Aufgabe 1c) bei der Rückgabe aller Elemente, für jedes
Element in [a, b] die Zahl der y-Koordinaten in [c, d] finden. Es seien k x-Koordinaten aus
dem Baum in [a, b]. Man braucht O(log n) Zeit um die kleinste x-Koordinate im Baum in
[a, b] zu finden. Dann benötigt man die Zeit O((k + 1) log n) (bzw. O(k + log n), falls alle
Elemente verschieden sind).
e) Gegeben sei eine Menge P von paarweise verschiedenen Punkten p1 , . . . , pn im Rd , d ≥ 1,
d.h. jeder Punkt hat die Koordinaten pi = (xi,1 , . . . , xi,d ). Die Datenstruktur zur Verwaltung
dieser Punktmenge wird rekursiv über die Dimension definiert. Für d = 1 verwendet man
einen gewöhnlichen binären Suchbaum, für d = 2 wurde die Datenstruktur in dieser Aufgabe
erklärt.
Sei d > 1. Die primäre Datenstruktur ist wieder ein binärer Suchbaum, in dem die x1 Koordinaten der Punkte verwaltet werden. (Zur Vereinfachung speichern wir in einem Knoten nicht nur die erste Koordinate, sondern alle Koordinaten. Sortiert wird aber nur nach
der ersten Koordinate.)
Zusätzlich wird in jedem Knoten v dieses Baumes ein Suchbaum zur Verwaltung von
Punkten aus Rd−1 verwaltet, in dem alle (x2 , . . . , xd )-Koordinaten aller Punkte, deren x1 Koordinaten im Unterbaum von v gespeichert sind, liegen.
Platzbedarf: O(n logd−1 n)
‘Beweis’: Jeder Punkt (x2 , . . . , xd ) kommt O(log n) mal vor: auf dem Weg von der Wurzel zu
dem Knoten, der den Punkt (x1 , . . . , xd ) enthält. In jedem dieser Knoten ist eine sekundäre
Datenstruktur, die den Punkt (x3 , . . . , xd ) auch O(log n) mal enthält. Letztendlich kommt
jedes xd O(logd−1 n) mal vor. Da das für alle Punkte gilt, folgt die Behauptung.
Laufzeiten: O(logd n)
‘Beweis’ über Induktion nach d: Für d = 1 ist das klar, für d = 2 wurde es in der Aufgabe
gezeigt.
d − 1 → d: Bei jeder der drei Funktionen insert, erase, find wird zuerst die primäre
Datenstruktur durchgegangen, dann die sekundäre, usw. Also braucht man, um die erste
Koordinate abzuarbeiten, O(log n) Zeit. Nach Induktionsvoraussetzung braucht man dann
O(logd−1 n) Zeit, um die einzelnen sekundären Datenstrukturen abzuarbeiten. Damit ergibt
sich die Behauptung.
/* Stimmen die Beweise? Formaler? */
Herunterladen