http://www.mpi-sb.mpg.de/~hannah/info5-ws00 IS UN R S WS 2000/2001 E R SIT S Bast, Schömer SA IV A Grundlagen zu Datenstrukturen und Algorithmen A VIE N Lösungsvorschläge für das 7. Übungsblatt Letzte Änderung am 13. Dezember 2000 Aufgabe 1 /* Die beiden sortierten Listen a und b werden sortiert in die Liste c eingetragen. Dabei muss sichergestellt sein, dass in c nicht bereits irgendwas steht. */ void slist_merge (slist& a, slist& b, slist& c) { handle d = c.end(); // handle to last sorted element while (!a.empty() && !b.empty()) // while sorting still needed { if ((a.begin()->inf) <= (b.begin()->inf)) // compare data of list heads { c.insert_after(d,a.begin()->inf); // insert at end of sorted list d = d->next; // new last sorted element a.pop_front(); // delete smallest element } else { c.insert_after(d,b.begin()->inf); // insert at end of sorted list d = d->next; // new last sorted element b.pop_front(); // delete smallest element } } if (a.empty()) { while (!b.empty()) { c.insert_after(d,b.begin()->inf); d = d ->next; b.pop_front(); } } else // determine not empty list // append c with rest of b // s.a. { while (!a.empty()) { c.insert_after(d,a.begin()->inf); d = d->next; a.pop_front(); } } delete a; delete b; // append c with rest of a // s.a. // clear memory } /* Die Liste a wird sortiert. */ void slist_mergesort(slist& a) { slist b; slist c; // first half of a // second half of a if (a.empty() || a.begin()->next == a.end()) // a empty or only one element { return (a); // => no sorting needed } handle mitte = a.begin(); // handle vor_ende = a.begin(); handle ende = a.begin()->next; // while(ende != a.end() && ende->next { mitte = mitte->next; // vor_ende = ende->next; ende = ende->next->next; // } // mitte startet auf dem 1. Element ende startet auf dem 2. Element != a.end()) mitte laeuft immer um 1 weiter, ende um 2 dann sollte mitte in der Mitte stehen. b.head = a.head; // b ist die erste Haelfte c.head = mitte->next; // c ist die zweite Haelfte mitte->next = b.head; if(ende == a.end()) vor_ende->next = c.head; else ende->next = c.head; a.clear(); slist_mergesort(b); slist_mergesort(c); slist_merge(b,c,a); // sort first half // sort second half // merge sorted halfs together delete b; delete c; return(d); // clear memory // return handle to result } Aufgabe 2 handle find(const T& x) { handle v = head; handle w = v->next; if(w->inf == x) return w; //wenn Element vorne steht, //ist keine Transposition notwendig while (w->next != head) //untersuche jeweils w->next auf Wert x { if (w->next->inf == x) { handle u = w->next; w->next = u->next; u->next = v->next; v->next = u; return u; } w = w->next; v = v->next; } return 0; } handle find(const T& x) { handle v = head; handle w = v->next; while (w != head) //untersuche jeweils w auf Wert x { if (w->inf == x) { if(w != head->next) //nur wenn Element nicht schon vorne steht, { //mache move-to-front v->next = w->next; w->next = head->next; head->next = w; } return w; } w = w->next; v = v->next; } return 0; } Aufgabe 3 1. Seien a, b, c Elemente aus den jeweiligen Teilbäumen A, B bzw. C, dann gilt wegen (I) a ≤ v ≤ b ≤ u ≤ c vor der Rotation. Die Rotation ändert nichts an der Ordnung der Elemente untereinander, also ist noch immer a ≤ v ≤ b ≤ u ≤ c, und (I) ist wirklich invariant unter diesen Operationen. 2. Ein Knoten des Suchbaums besteht aus dem zu speichernden Element selbst sowie Zeigern auf den linken und rechten Teilbaum und einem Zeiger auf den Elternknoten: template<class T> class TreeNode { public: T inf; TreeNode *left, *right, *parent; TreeNode (T info, TreeNode *p) { inf = info; left = 0; right = 0; parent = p; } ~TreeNode() {} }; Ein Suchbaum besteht aus einem Verweis auf den Wurzelknoten sowie (mindestens) den folgenden Methoden: template<class T> class SearchTree { TreeNode<T> *root; public: SearchTree() { root = 0; } ~SearchTree() {} void insert (T n) { TreeNode<T> *p = 0; TreeNode<T> *t = root; while (t) { p = t; if (n < t->inf) t = t->left; else t = t->right; } t = new TreeNode<T> (n,p); // find leaf position if (!p) root = t; else if (n < p->inf) p->left = t; else p->right = t; // tree was empty } TreeNode<T> *find (T n) { ... } void rotateLeft (TreeNode<T> *v) { ... } void rotateRight (TreeNode<T> *v) { ... } }; Die Methoden zum links- bzw. rechtsrotieren sind symmetrisch, die Variablen sind benannt wie im Bild zur Aufgabenstellung. void rotateLeft (TreeNode<T> *v) { TreeNode<T> *u = v->right; // assumes right child of v exists v->right = u->left; // turn B into v’s right subtree if (u->left) u->left->parent = v; u->parent = v->parent; // link v’s parent to u if (! v->parent) root = u; else if (v == v->parent->left) v->parent->left = u; else v->parent->right = u; u->left = v; // finally make v left child of u v->parent = u; } void rotateRight (TreeNode<T> *u) { TreeNode<T> *v = u->left; // assumes left child of u exists u->left = v->right; // turn B into u’s left subtree if (v->right) v->right->parent = u; v->parent = u->parent; // link u’s parent to v if (! u->parent) root = v; else if (u == u->parent->left) u->parent->left = v; else u->parent->right = v; v->right = u; u->parent = v; // finally make u right child of v } Die (rekursive) Version der find (T, TreeNode<T>*)-Funktion (bezüglich eines Teilbaums) liefert einen Zeiger auf das Element im Suchbaum zurück, bzw. einen nil-Zeiger, falls das Element nicht vorhanden ist. Zum implementieren der transposition-Heuristik kann dieser nun benutzt werden, um ein rotate durchzuführen. Abhängig davon, ob der gefundene Knoten die Wurzel des linken oder rechten Teilbaums seines Elternknotens ist, wird dies ein rotateRight oder rotateLeft sein. TreeNode<T> *find (T n, TreeNode<T> *t) { if (!t || t->inf == n) return t; if (t->inf > n) return (find (n, t->left)); return (find (n, t->right)); } TreeNode<T> *find (T n) { TreeNode<T> *t = find (n, root); if (t && t->parent) { if (t->parent->left == t) rotateRight(t->parent); else rotateLeft(t->parent); } return (t); } // t exists and is not the root // t left child // t right child Bei der move to root-Heuristik werden Rotationen entlang des gesamten Suchpfades durchgeführt. In jedem Schritt vermindert sich die Tiefe von t im Baum um 1, damit ist die Terminierung gewährleistet: TreeNode<T> *find (T n) { TreeNode<T> *t = find (n,root); while (t && t->parent) { if (t->parent->left == t) rotateRight(t->parent); else rotateLeft(t->parent); } return (t); } // have not reached the root // t left child // t right child 3. Bei der transposition-Heuristik ist einfach einzusehen, dass sich die Tiefe des Baums nach dem Aufruf der find-Funktion um maximal 1 erhöhen kann. Da nur ein einziges rotate durchgeführt wird, dieses die Tiefe von B erhält, die Tiefe von C (im rotateRight-Fall) um 1 erhöht, und die von A sogar um 1 erniedrigt wird, kann die Tiefe des gesamten Suchbaums sich um höchstens 1 erhöhen. Überraschenderweise gilt diese Abschätzung auch für die move to root-Heuristik. Genauer kann man zeigen: gibt es eine Folge von n rotate-Operationen, die einen Knoten u zur Wurzel bringen, und die erste Operation erhöht die Tiefe, dann wird sie durch nachfolgende Operationen nicht mehr weiter erhöht. Die wesentliche Beobachtung ist die folgende: Die Tiefe des gesamten Baumes erhöht sich durch ein rotateRight (und analog ein rotateLeft) nur, falls der Teilbaum C zur Tiefe des gesamten Baumes S beiträgt, d.h. dS (u) + dS (C) = dS (S). Die Tiefe von Knoten im Teilbaum mit Wurzel v wird dabei höchstens kleiner, die Tiefe aller anderen Knoten (i.e. nicht in A,B,C oder D enthalten) bleibt gleich. S S’ w w u v v u A D A B C D B C Bei einem nachfolgenden rotate (um die Kante (w, v)) wird nun entsprechend die Tiefe des Teilbaums mit Wurzel v höchstens kleiner, die von D dagegen erhöht sich maximal um 1 und somit gilt dS (S)+1 ≥ dS 0 (S 0 ) ≥ dS 0 (u)+dS 0 (C) ≥ dS 00 (S 00 ). Ein formaler Beweis geht mittels Induktion über die Anzahl der durchgeführten rotate-Operationen, indem man zeigt: Sei v1 , . . . , vn ein Pfad von v0 (dem gefundenen Element) zur Wurzel im Suchbaum S, und sei rotate rotate rotate S = S0 −→ S1 −→ . . . −→ Sn die Folge der durch die n rotate-Operationen entstehenden Suchbäume. Dann gilt für alle 1 ≤ i ≤ n: Für alle Blätter b von Si im Teilbaum mit Wurzel vi ist d(b) ≤ d(S) + 1. Für alle Blätter b von Si , die nicht im Teilbaum mit Wurzel vi sind, ist d(b) ≤ d(S). Den Induktionsanfang haben wir oben schon gesehen: Bei Durchführung von einem rotate wird die Tiefe des unter dem Knoten v1 liegenden Unterbaumes maximal um 1 erhöht, alle anderen Blätter bleiben entweder gleich oder erhalten eine um 1 niedrigere Tiefe. Für den Induktionsschritt nehmen wir an, daß wir bereits k Rotationen durchgeführt haben und daß die Induktionsbehauptung für vk gilt. Jetzt wird vk mit vk+1 rotiert, dabei liegt vk+1 über vk . Wenn man sich die beiden Rotationen ansieht, sieht man, daß die beiden Unterbäume von vk ihre Tiefe behalten oder die Tiefe um 1 erniedrigt wird. Es gilt also nach der Rotation immer noch für alle Blätter b im Teilbaum mit Wurzel vk : d(b) ≤ d(S)+1. Die Blätter aus dem zweiten Teilbaum von vk+1 können evtl. ihre Tiefe um 1 erhöhen. Da sie nach der Induktionsvoraussetzung Tiefe ≤ d(S) hatten, haben sie nach der Rotation Tiefe ≤ d(S) + 1. Also haben wir für alle Blätter b im Teilbaum mit Wurzel vk+1 gezeigt, daß ihre Tiefe ≤ d(S) + 1 ist. Alle anderen Blätter hatten vorher schon (nach Induktionsvoraussetzung) Tiefe ≤ d(S). Bei der Rotation werden sie nicht verändert, so daß sie ihre Tiefe erhalten. Es ist nicht sinnvoll, das zuletzt gefundene Element direkt als neue Wurzel zu setzen, da nicht klar ist, wie in diesem Fall die Suchbaumeigenschaft aufrecht erhalten werden kann: Um ein Element als neue Wurzel zu setzen (ohne weitere Umstrukturierung des restlichen Baumes) müsste es entweder ein minimales oder ein maximales Element des gesamten Suchbaumes sein, was i.a. nicht gegeben ist.