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? */