12/3/12 Mengen n Binäre Suchbäume n Mengen, Funktionalität, Binäre Suchbäume, Heaps, Treaps n n n Mengen: Anwendungen (I) n n n n Einfügen: Neue Telefonanschlüsse Löschen: Aufgegebene Telefonanschlüsse Suchen: Telefonnummer einer Person Anforderung: effizient auch bei grossen Mengen! Nutzerverwaltung (Menge von Nutzern mit Passwörtern und/oder weiteren Informationen) n n n n Mengen: Lösung mit Listen n n Einfügen: zum Beispiel vorne anfügen (effizient) 2 1 6 5 8 7 Effizienz: Keine Wartezeit beim Einloggen, auch bei Millionen von Nutzern. Menge wird als Liste gespeichert. n 0 9 Einfügen: Neue Nutzer Löschen: Ex-Nutzer Suchen: Einloggen Mengen: Lösung mit Listen Menge wird als Liste gespeichert. n Einfügen eines Elements Löschen eines Elements Suchen eines Elements Mengen: Anwendungen (II) Telefonbuch (Menge von Namen mit zugehörigen Telefonnummern) n Ziel: Aufrechterhalten einer Menge (hier: ganzer Zahlen) unter folgenden Operationen: 3 Löschen: Durchlaufen der Liste bis zum Element (oder bis zum Ende, falls nicht vorhanden), Zeiger umhängen. 2 1 6 5 8 0 9 7 3 1 12/3/12 Mengen: Lösung mit Listen n Mengen: Lösung mit Listen Menge wird als Liste gespeichert. n n Suchen: Durchlaufen der Liste bis zum Element (oder bis zum Ende, falls nicht vorhanden). 2 1 6 5 8 0 9 7 3 Mengen: Lösung mit Listen n Beispiel: Suche nach den letzten 100 Elementen in einer Liste von 10,000,000 Elementen Facebook hat 1,000,000,000 List l; Nutzer, eine for (int i=0; i<10000000; ++i) Suche würde l.push_front(i); 6.5 Sekunden dauern! // ...and search for the 100 first ones for (int i=0; i<100; ++i) 6.5 Sekunden l.find(i); Mengen: Lösung mit Binären Suchbäumen. n n n Falls die Menge n Elemente enthält, so ist der Aufwand fürs Suchen eines Elements im schlimmsten Fall proportional zu log2 n. Facebook: Zeit zum Einloggen ist proportional zum Logarithmus der Anzahl der Nutzer. Falls die Menge n Elemente enthält, so ist der Aufwand fürs Suchen eines Elements im schlimmsten Fall proportional zu n. Facebook: Zeit zum Einloggen wäre proportional zur Anzahl der Nutzer. Mengen: Lösung mit Listen n n Beispiel: Suche nach den letzten 100 Elementen in einer Liste von 10,000,000 Elementen Finden eines Elements: const Node* List::find (int key) { for (const Node* p = head_; p != 0; p = p>get_next()) if (p->get_key() == key) return p; return 0; } Mengen: Lösung mit Binären Suchbäumen. n n Falls die Menge n Elemente enthält, so ist der Aufwand fürs Suchen eines Elements im schlimmsten Fall proportional zu log2 n. Auch Einfügen und Löschen geht in Zeit proportional to log2 n. log2(1,000,000,000) ≈ 30. 2 12/3/12 Binärbäume, Definition und Terminologie n n Binäre Suchbäume Ein binärer Baum ist entweder leer,... oder er besteht aus einem Knoten (Kreis) n n n Sowohl der linke als auch der rechte Teilbaum sind binäre Suchbäume. n Der Schlüssel der Wurzel ist mit einem Schlüssel (hier: ganze Zahl),... und einem linken und rechten Teilbaum, die jeweils Binärbäume sind. 3 Ein Binärbaum heisst binärer Suchbaum, wenn er leer ist, oder wenn folgende Bedingungen gelten: n n Wurzel des Baums n Linker Teilbaum > alle Schlüssel im linken Teilbaum, und ≦ alle Schlüssel im rechten Teilbaum. Rechter Teilbaum Binäre Suchbäume: Beispiel Binäre Suchbäume: Suchen nach einem Schlüssel 4 3 8 1 6 5 2 3 9 4 =5? 3 2 6 5 9 7 Binäre Suchbäume: Suchen nach einem Schlüssel nein 4 >5? 3 9 7 5 2 8 6 8 1 Blätter des Baums 7 Binäre Suchbäume: Suchen nach einem Schlüssel 1 5? 4 ja 1 8 6 2 5 Nein, also muss die 5 rechts sein. 9 7 3 12/3/12 Binäre Suchbäume: Suchen nach einem Schlüssel Binäre Suchbäume: Suchen nach einem Schlüssel 4 4 3 8 =5? nein 3 8 Ja, also muss die 5 links sein. >5? nein 1 6 5 2 9 1 7 6 5 2 Binäre Suchbäume: Suchen nach einem Schlüssel 4 3 8 6 5 2 =5? nein 3 9 1 7 8 9 7 Binäre Suchbäume: Suchen nach einem Schlüssel 0? 4 3 8 6 5 >5? nein 5 4 2 6 Ja, also muss die 5 links sein. 2 Binäre Suchbäume: Suchen nach einem Schlüssel 1 7 Binäre Suchbäume: Suchen nach einem Schlüssel 4 1 9 =5? 3 9 7 Ja, Schlüssel gefunden! 8 1 0 müsste links von der 1 sein, Teilbaum ist aber leer ⇒ 0 nicht da. 6 2 5 9 7 4 12/3/12 Mengen: Lösung mit binären Suchbäumen n n Mengen: Lösung mit binären Suchbäumen Speichere die Elemente der Menge als Schlüssel in einem binären Suchbaum! Suchzeit ist proportional zur Höhe des Baums (Länge des längsten Pfades von der Wurzel bis zu einem Blatt). 4 3 1 6 5 2 Mengen: Lösung mit binären Suchbäumen n n n n Binäre Suchbäume: Einfügen 3 0 müsste links von der 1 sein, Teilbaum ist aber leer ⇒ neues Blatt 2 5 0 einfügen 4 3 9 7 Höhe 3 Binäre Suchbäume: Einfügen 8 6 7 Neues Element suchen und als Blatt an der passenden Stelle anhängen! 0 einfügen 4 9 Binäre Suchbäume: Einfügen Speichere die Elemente der Menge als Schlüssel in einem binären Suchbaum! Suchzeit ist proportional zur Höhe des Baums (Länge des längsten Pfades von der Wurzel bis zu einem Blatt). Einfügen und Löschen von Elementen? 1 8 8 1 0 6 2 5 9 7 5 12/3/12 Binäre Suchbäume: Einfügen n n Binäre Suchbäume: Löschen Neues Element suchen und als Blatt an der passenden Stelle anhängen! Auch die Einfügezeit ist proportional zur Höhe des Baums. n n Element suchen, seinen Knoten durch Rotationen zu einem Blatt machen, Blatt löschen! Rotation: lokale Operation, welche die Positionen von Knoten verändert, nicht jedoch die Suchbaumeigenschaften. Binäre Suchbäume: Löschen Rotation eines Teilbaums Rechtsrotation s 8 löschen 4 t Linksrotation 3 t 8 s C A A 1 B B A<t≦B<s≦C = C 6 7 A<t≦B<s≦C Binäre Suchbäume: Löschen Binäre Suchbäume: Löschen 8 löschen 4 3 6 8 löschen 4 8 1 5 2 9 3 C 9 1 6 5 B und C sind leere Bäume 8 A 2 5 A 7 B Rechtsrotation (Linksrotation wäre auch möglich) 2 Linksrotation (Rechtsrotation wäre auch möglich) 7 9 B C 6 12/3/12 Binäre Suchbäume: Löschen Binäre Suchbäume: Löschen 8 löschen 4 3 6 1 3 A, B, C sind leere Bäume 5 9 8 löschen 4 1 6 5 9 8 7 C 2 Rechtsrotation (Linksrotation wäre nicht möglich) A n n delete B Binäre Suchbäume: Löschen n 2 7 8 Die Höhe eines binären Suchbaums... Element suchen, seinen Knoten durch Rotationen zu einem Blatt machen, Blatt löschen! Rotation: lokale Operation, welche die Positionen von Knoten verändert, nicht jedoch die Suchbaumeigenschaften. Auch die Löschzeit ist proportional zur Höhe des Baums (nicht mehr ganz so offensichtlich) Die Höhe eines binären Einfügen in Suchbaums... Beispiel: sortierter Reihenfolge 1 n n n n n n 2 3 Höhe = n-1, Baum sieht aus und verhält sich wie eine Liste. Suchens, Einfügens, Löschens. kann aber bereits bei einer ungünstigen Einfügereihenfolge sehr gross werden! Laufzeit im schlimmsten Fall nicht besser als bei einer Liste! Abhilfe: Balancierung n n bestimmt die Laufzeit des n Beim Einfügen und Löschen stets darauf achten, dass der Baum „balanciert“ ist, (d.h. keine grossen Höhenunterschiede zwischen linken und rechten Teilbäumen) Verschiedene Möglichkeiten (mehr oder weniger kompliziert): n n AVL-Bäume, Rot-Schwarz-Bäume,... Hier: Treaps (randomisierte Suchbäume) 7 12/3/12 Heap: Schlüssel heissen Prioritäten Treap = Tree + Heap n Ein Binärbaum heisst Heap, wenn er leer ist, oder wenn folgende Bedingungen gelten: n Sowohl der linke als auch der rechte Teilbaum sind Heaps. n Der Schlüssel der Wurzel ist Sowohl der linke als auch der rechte Teilbaum sind Heaps. n Die Priorität der Wurzel ist n n n das Maximum aller Schlüssel. n Heap: Beispiel n 7 8 4 6 3 das Maximum aller Prioritäten. Treaps 9 2 n 1 Schlüssel Prioritäten 4, 9 Treaps: Eindeutigkeit n 3, 7 8, 8 n 1, 4 6, 6 5, 2 Ein Binärbaum heisst Treap, wenn jeder Knoten einen Schlüssel und eine Priorität hat, so dass folgende Eigenschaften gelten: Der Baum ist ein binärer Suchbaum bzgl. der Schlüssel. n Der Baum ist ein Heap bzgl. der Prioritäten. 5 Treap: Beispiel 2, 3 Ein Binärbaum heisst Heap, wenn er leer ist, oder wenn folgende Bedingungen gelten: 9, 5 7, 1 Satz: Für n Knoten mit verschiedenen Schlüsseln und Prioritäten gibt es genau einen Treap mit diesen Knoten. Beweis: Die Wurzel ist der Knoten mit der höchsten Priorität; im linken Teilbaum sind alle Knoten mit kleineren und im rechten Teilbaum alle Knoten mit grösseren Schlüsseln. Mit Induktion sind die Teilbäume auch eindeutig. 8 12/3/12 Treaps mit zufälligen Prioritäten Treaps: Einfügen Wir benutzen einen Treap als binären Suchbaum. Beim Einfügen eines neuen Schlüssels wird seine Priorität zufällig gewählt. Wie bei normalen binären Suchbäumen: neuer Knoten wird neues Blatt. Zusätzlich muss die Treap-Eigenschaft wieder hergestellt werden! n n n n 4, 9 0 einfügen, mit zufälliger Priorität 7.5 3, 7 8, 8 1, 4 0, 7.5 6, 6 2, 3 5, 2 9, 5 7, 1 Neues Blatt Treaps: Einfügen 4, 9 Treaps: Einfügen 0 einfügen, mit zufälliger Priorität 7.5 3, 7 4, 9 8, 8 1, 4 6, 6 0 einfügen, mit zufälliger Priorität 7.5 3, 7 9, 5 8, 8 1, 4 6, 6 9, 5 Heap-Eigenschaft verletzt: 4 < 7.5 0, 7.5 2, 3 5, 2 7, 1 0, 7.5 2, 3 5, 2 7, 1 Rechtsrotation! Treaps: Einfügen 4, 9 0 einfügen, mit zufälliger Priorität 7.5 3, 7 4, 9 8, 8 0, 7.5 6, 6 0 einfügen, mit zufälliger Priorität 7.5 3, 7 9, 5 1, 4 2, 3 Treaps: Einfügen 0, 7.5 8, 8 Heap-Eigenschaft verletzt: 7 < 7.5 6, 6 9, 5 1, 4 5, 2 7, 1 2, 3 5, 2 7, 1 9 12/3/12 Treaps: Einfügen 4, 9 Treaps: Einfügen 0 einfügen, mit zufälliger Priorität 7.5 3, 7 4, 9 8, 8 0, 7.5 6, 6 0 einfügen, mit zufälliger Priorität 7.5 0, 7.5 9, 5 1, 4 8, 8 3, 7 6, 6 9, 5 1, 4 5, 2 2, 3 7, 1 2, 3 5, 2 7, 1 Rechtsrotation! Treaps: Einfügen 4, 9 0, 7.5 Treaps: Löschen 0 einfügen, mit zufälliger Priorität 7.5 Heap-Eigenschaft ok, wir haben wieder einen Treap! n 8, 8 n 3, 7 6, 6 9, 5 1, 4 2, 3 5, 2 7, 1 Treaps: Laufzeit n n Satz (Seidel, Aragon 1996): In einem Treap über n Knoten mit zufälligen Prioritäten ist der zeitliche Aufwand für eine Einfüge-, Lösch- oder Suchoperation im Erwartungswert proportional zu log2n. Mehr dazu in der nächsten Stunde. n Element suchen, seinen Knoten durch Rotationen zu einem Blatt machen, Blatt löschen! Stets so rotieren, dass der Knoten mit seinem Nachfolger höherer Priorität vertauscht wird. Das erhält die HeapEigenschaft. Suchbaum-Eigenschaft gilt wieder nach Löschen des Blatts. Mengen: Lösung mit Treaps n Zur Erinnerung: Suche nach den letzten 100 Elementen in einer Liste von 10,000,000 Elementen List l; for (int i=0; i<10000000; ++i) l.push_front(i); // ...and search for the 100 first ones for (int i=0; i<100; ++i) 6.5 Sekunden l.find(i); 10 12/3/12 Mengen: Lösung mit Treaps n Neu: Suche nach den letzten 100 Elementen in einem Treap von 10,000,000 Elementen Treap t; for (int i=0; i<10000000; ++i) t.insert(i); Sortieren mit Treaps n n Folgerung: Durch Einfügen einer Folge von n Zahlen in einen Treap mit zufälligen Prioritäten kann die Folge in Zeit proportional zu n log2n sortiert werden. Gleicher optimaler Proportionalitätsfaktor wie Mergesort Das Treap-basierte Verfahren ist im wesentlichen äquivalent zu Quicksort. // ...and search for the 100 first ones for (int i=0; i<100; ++i) 0.017 Sekunden t.find(i); n Treaps in C++ Die Klasse TreapNode n Aehnlich wie bei Listen: Klasse TreapNode für die Knoten Jeder TreapNode speichert ausser Schlüssel und Priorität zwei Zeiger auf TreapNodes (linkes und rechtes Kind = Wurzeln des linken und rechten Teilbaums) n Klasse Treap; jeder Treap ist durch einen Zeiger auf den Wurzelknoten repräsentiert. n n class TreapNode ! {! public:! // constructor! TreapNode( int key, int priority, TreapNode* left = 0, TreapNode* right = 0);! ! // Getters! int get_key() const;! int get_priority() const;! const TreapNode* get_left() const;! const TreapNode* get_right() const;! !! private:! int key_;! double priority_;! Wie bei Facebook: Freunde dürfen TreapNode* left_;! auf private Daten und Methoden TreapNode* right_; !! (Mitgliedsfunktionen) zugreifen. friend class Treap;! }; ! Die Klasse Treap Die „Viererbande“ + Wurzel Die Klasse Treap Einfügen class Treap { public: // default constructor: // POST: *this is an empty tree Treap(); void Treap::insert(const int key)! {! insert (root_, key, std::rand());! }! Konstruktor // copy constructor // POST *this is a copy of other Treap(const Treap& other); CopyKonstruktor // assignment operator // Post: other was assigned to *this Treap& operator= (const Treap& other); Zuweisungsoperator // Destructor ~Treap(); private: TreapNode* root_; … private Hilfsfunktion Zufällige Priorität Destruktor Zeiger auf die Wurzel (0: leerer Treap) 11 12/3/12 Die Klasse Treap Einfügen Die Klasse Treap Einfügen // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); eine leere Blattposition. else if (key < current->key_) { void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); eine leere Blattposition. else if (key < current->key_) { 9, insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 10 8, 8 9, insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); current } else { insert (current->right_, key, priority); if (priority > current->get_priority()) rotate_left (current); } 8, 8 current } else { insert (current->right_, key, priority); if (priority > current->get_priority()) rotate_left (current); Damit das klappt, } } 10 9, 10 muss current ein Alias des entsprechenden Zeigers im Baum sein, keine Kopie! } Die Klasse Treap Einfügen Die Klasse Treap Einfügen // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); eine leere Blattposition. else if (key < current->key_) { void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); einen Knoten. else if (key < current->key_) { 9, insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 10 8, 8 9, 9 } else { insert (current->right_, key, priority); } current 8, 8 } else { insert (current->right_, key, priority); 9, 10 if (priority > current->get_priority()) rotate_left (current); } 8 insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); current Heap-Eigenschaft ist noch verletzt; das wird „weiter oben“ in der Rekursion repariert. 10 if (priority > current->get_priority()) rotate_left (current); } } Die Klasse Treap Einfügen Die Klasse Treap Einfügen // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); einen Knoten. else if (key < current->key_) { void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); einen Knoten. else if (key < current->key_) { 9, current insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 9, A 8 if (priority > current->get_priority()) rotate_left (current); 9, 10 } } 8, 8 } else { insert (current->right_, key, priority); 10 10 current insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 8, 8 } else { insert (current->right_, key, priority); if (priority > current->get_priority()) rotate_left (current); 10 A 9, 10 } } 12 12/3/12 Die Klasse Treap Einfügen Die Klasse Treap Einfügen // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); einen Knoten. else if (key < current->key_) { void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); einen Knoten. else if (key < current->key_) { 9, 10 current insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 9, } else { insert (current->right_, key, priority); if (priority > current->get_priority()) rotate_left (current); } 9, 10 } current insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 8, 8 } else { insert (current->right_, key, priority); if (priority > current->get_priority()) rotate_left (current); } } 10 9, 10 8, 8 A Die Klasse Treap Einfügen // PRE: current is a pointer in *this // POST: a new TreapNode with key and priority is inserted into the // subtree hanging off current 9, 10 void Treap::insert (TreapNode*& current, const int key, const int priority) { current verweist auf if (current == 0) current = new TreapNode (key, priority); einen Knoten. else if (key < current->key_) { current insert (current->left_, key, priority); if (priority > current->get_priority()) rotate_right (current); 9, 10 } else { insert (current->right_, key, priority); if (priority > current->get_priority()) rotate_left (current); } } A 8, 8 Fertig! 13