Teil I: Schlüsselbasiertes Suchen ! ! ! ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-1 Dictionaries (1) Dictionary Datenstruktur zur effizienten Verwaltung einer (dynamischen) Menge von Datensätzen. Datensätze (records) bestehen aus ! Suchschlüssel (search key) und ! Nutzdaten (value). Beispiel Telefonbuch: Beispiel Wörterbuch: Maier Hauptstr. 1 14234 sehen see; look Baier Bahnhofstr. 5 24829 sprechen speak; talk Müller Uferweg 5 53289 gehen go; walk Anton Mozartstr. 2 36478 hören hear; listen Suchschlüssel Datensatz Nutzdaten: Adresse + Tel.Nr Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-2 Dictionaries (2) Operationen: ! search(key) suche nach Datensatz mit Schlüssel key. ! insert(key, value) füge neuen Datensatz mit Schlüssel key und Daten value ein. ! remove(key) Lösche Datensatz mit Schlüssel key. Datenstrukturen mit diesen Operationen werden in der Literatur und Programmiersprachen auch als Dictionaries oder Maps bezeichnet. Eventuell zusätzliche Operationen: ! Traversieren: Gehe über alle Datensätze in bestimmter Reihenfolge: - über Schlüssel sortiert - nach Einfügezeitpunkt - irgendeine Reihenfolge (von der Implementierung bestimmt) ! Union: Vereinige zwei Dictionaries. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-3 Varianten Eindeutigkeit der Schlüssel: ! Die Zuordnung von Schlüssel zu einem Datensatz ist meistens eindeutig. ! Es können aber auch mehrere Datensätze zu einem Schlüssel existieren. Die search-Operation liefert dann mehrere Datensätze zu einem Schlüssel zurück. ! Die Forderung der Eindeutigkeit der Schlüssel hängt von der Anwendung ab. Beispiele: - Telefonbuch: Schlüssel (= Familenname) sind nicht eindeutig - Wörterbuch: Schlüssel sind eindeutig Schlüsseltypen: ! int, ! String, ! zusammengesetze Daten; z.B. Uhrzeit, Kalenderdatum, ... Dictionaries ohne Nutzdaten: ! entsprechen Mengen von Schlüssel und werden auch Sets genannt. In dieser Vorlesung: ! Schlüssel sind eindeutig. ! Im wesentlichen nur int-Werte als Schlüssel. Nutzdaten in den Darstellungen meist weggelassen. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-4 Dictionaries in Programmbibliotheken Üblicherweise als generische Klasse: ! Schlüsseltyp und Typ für Nutzdaten werden parameterisiert ! Z.B. K für Key und V für Value. Java: ! Interface Set<K> mit den Implementierungen - TreeSet<K> (ausgeglichener binärer Suchbaum) - und HashSet<K> (Hashverfahren) ! Interface Map<K, V> mit den Implementierungen TreeMap<K, V> und HashMap<K, V> ! Bei TreeSet<K> und TreeMap<K, V> muss der Schlüsseltyp K den Vergleichsoperator compareTo (siehe Interface Comparable) anbieten. ! Dictionary<K, V> ist veraltet; stattdessen Map<K, V> verwenden. STL in C++: ! Template-Klassen set<K> und map<K, V> ! Template-Klassen multiset<K> und multimap<K, V>, falls Schlüssel mehrfach vorkommen dürfen. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-5 Teil I: Schlüsselbasiertes Suchen ! Dictionaries ! Elementare Suchverfahren - Sequentielle Suche - Binäre Suche - Umsetzung in Java ! ! ! ! ! Hashverfahren Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-6 Sequentielle Suche (1) Datenstruktur ! Halte n Datensätzen in einem Feld lückenlos und unsortiert. ! Alternative: linear verkettete Liste. 7 3 a[0] [1] 9 ... 5 ... [n-1] Algorithmen (in Pseudocode) search(int k) { for (int i = 0; i < n; i++) if (a[i].key == k) breche ab, da k gefunden; nicht gefunden; } remove(int k) { if (k kommt in a vor) { lösche k und schließe Lücke; n--; } } Prof. Dr. O. Bittel, HTWG Konstanz insert(int k) { if (k kommt nicht in a vor) { if (a vollständig gefüllt) vergrößere Feld a; a[n++] = k; } } Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-7 Sequentielle Suche (2) Laufzeiten (Worst Case): Operation bei einer Menge mit n Elementen T(n) search O(n) insert O(n) *) remove O(n) Aufbau einer Menge mit n Elementen (d.h. n insert-Aufrufe) O(n2) n search-Aufrufe O(n2) *) Es muss geprüft werden, ob das Element bereits vorkommt (search-Aufruf) Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-8 Binäre Suche (1) Datenstruktur ! Halte Datensätze lückenlos in einem sortierten Feld. Idee für Such-Algorithmus Mittleres Element a[m] 1 3 5 6 8 9 11 12 15 17 19 20 22 Sortiertes Feld a k < a[m] oder k > a[m] oder k == a[m] ? Falls k == a[m], dann gefunden. Falls k < a[m], dann suche in linker Hälfte weiter. Falls k > a[m], dann suche in rechter Hälfte weiter Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-9 Binäre Suche (2) Beispiel: Suche nach k = 8: li=0 1 m=6 3 li=0 1 5 6 8 5 11 12 15 17 19 20 22 11 12 15 17 19 20 22 Suche in linker Hälfte 11 12 15 17 19 20 22 Suche in rechter Hälfte; k wird gefunden! re=5 m=2 3 9 re=12 6 8 9 li=3 m=4 re=5 1 3 5 6 8 9 Zu durchsuchender Bereich geht von a[li] bis a[re] Mittleres Element m = (li + re)/2 (ganzzahlige Division) Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-10 Binäre Suche (3) Algorithmen: search(int k) { int li = 0; int re = n-1; Laufzeit von search: T(n) = O(log n) In jedem Schleifendurchlauf wird while (re >= li) { das zu durchsuchende Teilfeld in int m = (li + re)/2; etwa halbiert. if (k == a[m].key) breche ab, da k gefunden; else if (k < a[m].key) re = m – 1; Suche weiter in linker Hälfte else li = m + 1; Suche weiter in rechter Hälfte } k wurde nicht gefunden; } insert(int k) // füge in sortiertes Feld ein Prof. Dr. O. Bittel, HTWG Konstanz remove(int k) // wie zuvor Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-11 Binäre Suche (4) Laufzeiten (Worst Case): Operation bei einer Menge mit n Elementen T(n) search O(log n) insert O(n) remove O(n) Aufbau einer Menge mit n Elementen (d.h. n insert-Aufrufe) O(n2) *) n search-Aufrufe O(n log n) *) Alternativ lassen sich auch n Elemente unsortiert einfügen und dann mit einem schnellen Sortierverfahren sortieren. Damit würde man T(n) = O(n log n) erreichen Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-12 Umsetzung in Java Konzept: ! Definiere Dictionary<K, V> als Interface ! Implementiere Klassen: - ArrayDictionary<K, V> mit sequentieller Suche - SortedArrayDictionaryK, V> mit binärer Suche Dictionary<K, V> + V search(K); + V insert(K, V); +V remove(K); ArrayDictionary<K, V> Prof. Dr. O. Bittel, HTWG Konstanz SortedArrayDictionary<K, V> Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-13 Interface Dictionary public interface Dictionary<K,V> { V insert(K key, V value); // Falls ein Datensatz mit Schlüssel key noch nicht existiert, wird das // Schlüssel-Wert-Paar key, value eingefügt und null zurückgeliefert. // Ansonsten werden beim Datensatz mit Schlüssel key die alten // Nutzdaten durch value ausgetauscht und zurückgeliefert. V search(K key); // Liefert die Daten zu dem Schlüssel key zurück oder // null falls ein Datensatz mit Schlüssel key nicht vorhanden ist. V remove(K key); // Löscht den Datensatz mit Schlüssel key (falls vorhanden) und liefert die alten // Daten zurück. Falls der Datensatz nicht existiert, wird null zurückgeliefert. } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-14 ArrayDictionary (1) public class ArrayDictionary<K, V> implements Dictionary<K, V> { private static class Entry<K,V> { K key; V value; Entry(K k, V v) {key = k; value = v;} }; private static final int DEF_CAPACITY = 16; private int size; private Entry<K,V>[] data; Feld data mit @SuppressWarnings("unchecked") public ArrayDictionary() { size = 0; data = new Entry[DEF_CAPACITY]; } Schlüssel-Wert-Paaren @SuppressWarnings("unchecked") private void ensureCapacity(int newCapacity) { if (newCapacity < size) return; Feld data vergrößern. Entry[] old = data; data = new Entry[newCapacity]; System.arraycopy(old, 0, data, 0, size); } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-15 ArrayDictionary (2) private int searchKey(K key) { for (int i = 0; i < size; i++) { if (data[i].key.equals(key)) { return i; } } return -1; // nicht gefunden } public V search(K key) { int i = searchKey(key); if (i >= 0) return data[i].value; else return null; } Prof. Dr. O. Bittel, HTWG Konstanz data[i].key.equals(key) statt data[i].key == key, denn sonst würde Vergleich nur auf Referenzebene stattfinden. Jedoch muss equals für den Schlüsseltyp geeignet definiert sein. Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-16 ArrayDictionary (3) public V insert(K key, V value) { int i = searchKey(key); // Vorhandener Eintrag wird überschrieben: if (i >= 0) { V r = data[i].value; data[i].value = value; return r; } // Neueintrag: if (data.length == size) { ensureCapacity(2*size); } data[size] = new Entry<K,V>(key,value); size++; return null; } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-17 ArrayDictionary (4) public V remove(K key) { int i = searchKey(key); if (i == -1) return null; // Datensatz loeschen und Lücke schließen V r = data[i].value; for (int j = i; j < size-1; j++) data[j] = data[j+1]; data[--size] = null; return r; } } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-18 SortedArrayDictionary mit Typbeschränkung (1) public class SortedArrayDictionary<K extends Comparable<? super K>, V> implements Dictionary<K, V> { // ... ! Typbeschränkung: Schlüssel von Typ K müssen vergleichbar sein. } public interface Comparable<T> { int compareTo(T o); } ! Dazu genügt es zu fordern, dass die Elemente von irgendeinem Obertyp von K vergleichbar sind. x.compareTo(y) liefert eine negative Zahl, 0, bzw. eine positive Zahl zurück, falls x kleiner, gleich bzw. größer als y ist. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-19 SortedArrayDictionary mit Typbeschränkung (2) public class SortedArrayDictionary<K extends Comparable<? super K>, V> implements Dictionary<K, V> { Daten, Konstruktor, ensureCapacity, search und remove wie bei ArrayDictionary // ... private int searchKey(K key) { int li = 0; int re = size - 1; Binäre Suche while (re >= li) { int m = (li + re)/2; if (key.compareTo(data[m].key) < 0) re = m – 1; else if (key.compareTo(data[m].key) > 0) li= m + 1; else return m; // key gefunden } return -1; // key nicht gefunden } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-20 SortedArrayDictionary mit Typbeschränkung (3) public V insert(K key, V value) { int i = searchKey(key); // Vorhandener Eintrag wird überschrieben: if (i != -1) { V r = data[i].value; data[i].value = value; return r; } // Neueintrag: if (data.length == size) { ensureCapacity(2*size); } int j = size-1; while (j >= 0 && key.compareTo(data[j].key) < 0) { data[j+1] = data[j]; Einfuegen in sortiertes Feld. j--; } data[j+1] = new Entry<K,V>(key,value); size++; return null; } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-21 Anwendung public class WoerterbuchAnwendung { public static void main(String[] args) { Dictionary<String, String> wb; Scanner in = new Scanner(System.in); int i; i = in.nextInt(); if (i == 1) wb = new ArrayDictionary<String,String>(); else wb = new SortedArrayDictionary<String, String>(); wb.insert("lesen", "read"); wb.insert("schreiben", "write"); System.out.println(wb.search("lesen")); } ! Beachte, dass für den Schlüsseltyp String sowohl equals als auch compareTo geeignet definiert ist. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-22 Beispiel: Datum als Schlüssel (1) ! Wie muss die Klasse Datum erweitert werden, so dass Datum als Schlüsseltyp sowohl für ArrayDictionary als auch SortedArrayDictionary eingesetzt werden kann? public class Datum { private int tag = 1; private int mon = 1; private int jahr = 1970; public Datum() {} public Datum(int t, int m, int j) { tag = t; mon = m; jahr = j; } } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-23 Beispiel: Datum als Schlüssel (2) public class Datum implements Comparable<Datum> { private int tag = 1; private int mon = 1; private int jahr = 1970; // ... public boolean equals(Object o) { if (this == o) return true; if (! (o instanceof Datum))) return false; Datum d = (Datum) o; return (jahr == d.jahr && mon == d.mon && tag == d.tag); } } public int compareTo(Datum d) { if (jahr < d.jahr) return -1; else if (jahr > d.jahr) return 1; else if (mon < d.mon) return -1; else if (mon > d.mon) return 1; else if (tag < d.tag) return -1; else if (tag > d.tag) return 1; else return 0; } d1.compareTo(d2) liefert Prof. Dr. O. Bittel, HTWG Konstanz -1, falls d1 chronologisch vor d2 0, falls d1 gleich d2 +1, falls d1 chronologisch nach d2 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-24 SortedArrayDictionary mit Comparable-Downcast ! SortedArrayDictionary kann auch ohne Typbeschränkung definiert werden. Eine compareTo-Methode wird dann durch ein Comparable-Downcast "erzwungen". ! Laufzeitfehler (ClassCastException), falls Schlüsseltyp K nicht Comparable ist. ! Die Java-Klassen TreeSet und TreeMap setzen dieselbe Technik ein. public class SortedArrayDictionary<K, V> implements Dictionary<K, V> { // ... private int searchKey(K key) { Comparable<? super K> comparableKey = (Comparable<? super K>) key; ... ! Es genügt zu fordern, dass die Elemente von irgendeinem Obertyp von K vergleichbar sind. ! ClassCastException, falls Cast nicht möglich. if (comparableKey.compareTo(data[m].key) < 0) ... } // ... } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-25 SortedArrayDictionary mit Comparator-Parameter (1) public class SortedArrayDictionary<K, V> implements Dictionary<K, V> { private Comparator<? super K> cmp; // ... public SortedArrayDictionary(Comparator<? super K> c) { if (c == null) cmp = ...; // Natural Order als Default-Wert (später) else cmp = c; Für den übergebenen // ... Comparator-Parameter c } private int searchKey(K key) { ... if (cmp.compare(key,data[m].key)) < 0) ... } // ... } public interface Comparator<T> { int compare(T x, T y); } Prof. Dr. O. Bittel, HTWG Konstanz genügt es zu fordern, dass c die Elemente von irgendeinem Obertyp von K vergleichen kann. compare(x, y) liefert eine negative Zahl, 0, bzw. eine positive Zahl zurück, falls x kleiner, gleich bzw. größer als y ist. Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-26 SortedArrayDictionary mit Comparator-Parameter (2) ! Comparator ist ein funktionales Interface. Daher kann einfachheitshalber ein Lambda-Ausdruck übergeben werden. ! Kalenderdaten chronologisch aufsteigend sortiert: Dictionary<Datum,String> kalender = new SortedArrayDictionary<>( (d1, d2) -> d1.compareTo(d2) ); Lambda-Ausdruck. ! Kalenderdaten chronologisch absteigend sortiert: Dictionary<Datum,String> kalender = new SortedArrayDictionary<>( (d1, d2) -> d2.compareTo(d1) ); Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-27 SortedArrayDictionary mit Comparator-Parameter mit Default-Wert public class SortedArrayDictionary<K, V> implements Dictionary<K, V> { private Comparator<? super K> cmp; // ... Konstruktor mit Comprarator-Parameter public SortedArrayDictionary(Comparator<? super K> c) { if (c == null) // Natural Order als Default-Wert: cmp = (x,y) -> ((Comparable<? super K>) x).compareTo(y); else cmp = c; // ... } Default-Konstruktor public SortedArrayDictionary() { // Natural Order als Default-Wert: cmp = (x,y) -> ((Comparable<? super K>) x).compareTo(y); } // ... } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-28 Teil I: Schlüsselbasiertes Suchen ! Dictionaries ! Elementare Suchverfahren ! Hashverfahren - Idee - - - - - ! ! ! ! Hashfunktion Hashverfahren mit linear verketteten Listen Offene Hashverfahren Dynamische Hashverfahren Hashverfahren in Java Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-29 Idee der Hashverfahren ! Mit einer Hashfunktion h wird aus einem Schlüssel k eine Hashadresse h(k) (positive ganze Zahl) berechnet. ! Die Hashadresse gibt den Index in einem Feld an, wo der Datensatz abgespeichert werden kann bzw. abgespeichert ist. ! Das Feld wird auch Hashtabelle genannt. tab[0] Schlüssel k = 17 Hashfunktion h Hashadresse h(k) = 3 8 tab[1] tab[2] 17 tab[3] 11 … 20 tab[6] Hashtabelle tab Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-30 Wichtige Anforderungen an Hashfunktionen ! Hashfunktion sollte einfach zu berechnen sein. ! Gute Streuwirkung: vorkommende Schlüssel sollten sich möglichst gut über die Tabelle verteilen. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-31 Typische Hashfunktionen ! Division-Rest-Methode h(k) = k mod m; Hierbei ist m die Tabellengröße. m sollte möglichst eine Primzahl sein. ! Multiplikative Methode h(k) = ⎣ m(kφ-1 – ⎣kφ-1⎦ ) ⎦ – Dabei ist φ-1 = des goldenen Schnitts. ≈ 0.6180339887 der Kehrwert – ⎣x⎦ rundet auf die nächst kleinere ganze Zahl ab. – m ist die Tabellengröße. – Beispielsweise bilden die Werte h(1), h(2), …, h(10) für m = 10 eine Permutation der Zahlen 0,1, …, 9 nämlich: 6, 2, 8, 4, 0, 7, 3, 9, 5, 1. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-32 Beispiel mit Division-Rest-Methode Beispiel ! Füge die Zahlen 7, 24, 5, 8 in eine Hashtabelle der Größe m = 7 ein. ! Benutze die Hashfunktion h(k) = k mod m. 7 tab[0] 8 tab[1] tab[2] 24 tab[3] tab[4] 5 tab[5] tab[6] Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-33 Tabellengröße als Primzahl bei Division-Rest-Methode (1) ! Bei der Division-Rest-Methode ist es zweckmäßig die Tabellengröße als Primzahl zu wählen. ! Die beiden folgenden Beispiele zeigen, dass eine ungeschickte Wahl der Tabellengröße eine schlecht streuende Hashfunktion verursachen kann. ! Beispiel 1: - Die Personaldaten einer Firma sollen in einer Hashtabelle abgespeichert werden. - Dazu wird die Personalnummer als Schlüssel verwendet. Die letzte Stelle der Personalnummer gibt das Geschlecht an: 0 = männlich und 1 = weiblich. - Eine Hashfunktion h = k mod m mit einer geraden Tabellengröße m würde folgendes ergeben: männliche Person: h(x...xx0) ist gerade weibliche Person: h(x...xx1) ist ungerade Ungleichmäßige Streuung bei ungleicher Geschlecherquote! Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-34 Tabellengröße als Primzahl bei Division-Rest-Methode (2) ! Beispiel 2: - Die Personalnummer wird nun 6-stellig gewählt, wobei die hintersten 3 Stellen die Abteilung codieren, in der die Person arbeitet. - Eine Hashfunktion h = k mod m mit einer Tabellengröße m = 1000 würde folgendes ergeben: h(xxxyyy) = yyy, wobei yyy die Abteilungsnummer ist. Ungleichmäßige Streuung bei ungleicher Verteilung der Mitarbeiter auf die einzelnen Abteilungen. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-35 Einschub: Verteilung der Primzahlen Primzahlfunktion π(n) = Anzahl der Primzahlen ≤ n. Primzahlsatz π(n) ∼ n/ln(n) Damit gilt für die Primzahlhäufigkeit: π(n)/n ∼ 1/ln(n) Tabelle zeigt die tatsächliche und die geschätzte Primzahlhäufigkeit n π(n)/n 1/ln(n) 104 0.123 0.11 105 0.096 0.087 106 0.078 0.072 107 0.066 0.062 108 0.058 0.054 109 0.051 0.048 10100 ? 0.0043 10200 ? 0.0021 Prof. Dr. O. Bittel, HTWG Konstanz Im praktisch relevanten Bereich liegt die Primzahlhäufigkeit bei 5 bis 10%. Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-36 Hashfunktion für Strings als Schlüssel ! Interpretierte String s = z0...zn-2zn-1 (Folge von ASCII-Zeichen) als eine Zahl im Stellenwertsystem 31: h(z0...zn-2zn-1) = [z0*31n-1 + ... + zn-2*311 + zn-1*310] mod m; ! 31 ist eine Primzahl und führt zu einer guten Streuung. ! Mithilfe des Horner-Schemas lässt sich der Ausdruck effizienter berechnen: h(z0...zn-2zn-1) = [( ... (z0*31 + z1)*31 + ... + zn-2)*31 + zn-1] mod m; ! Bei einer Implementierung der Hashfunktion kann ein evtl. auftretender Überlauf ignoriert werden. Die Hashadresse kann dabei negativ werden, was abgeprüft werden muss. static int hash(String key){ int adr = 0; for (int i = 0; i < key.length(); i++) adr = 31*adr + key.charAt(i); if (adr < 0) adr = -adr; return adr % m; } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-37 Hashfunktion hashCode in Java (1) ! In Java ist in der Klasse Object die Methode hashCode definiert, die jedes Objekt auf einen ganzzahligen Wert abbildet. class Object { public int hashCode() {...} // ... } ! Für alle Wrapper-Klassen wie Integer, Long, Short, etc. und für die Klasse String ist hashCode geeignet überschrieben. ! hashCode für String ist ähnlich implementiert wie die for-Schleife auf der vorhergehenden Seite. Es ist zu berücksichtigen, dass hashCode eine negative int-Zahl zurückliefern kann . Wird der HashCode als Adresse für eine Hash-Tabelle benötigt, muss ein negativer Wert abgefangen und modolo der Tabellengröße gerechnet werden: String key = "Zimmer"; int adr = key.hashCode(); if (adr < 0) adr = -adr; adr = adr % m; Prof. Dr. O. Bittel, HTWG Konstanz liefert -1618018788 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-38 Hashfunktion hashCode in Java (2) Hashfunktion für zusammengesetze Schlüssel ! wird für einen zusammengesetzten Schlüsseltyp eine Hashfunktion benötigt, dann überschreibt man am besten für den Schlüsseltyp die hashCode-Methode. ! Dabei kann eine ähnliche Technik wir bei einem String-Schlüssel eingesetzt werden. ! Beispiel: public class Datum { private int tag; private int mon; private int jahr; // ... @Override public int hashCode() { int adr = 0; adr = 31*adr + tag; adr = 31*adr + mon; adr = 31*adr + jahr; return adr; } } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-39 Hashfunktion hashCode in Java (3) Hashfunktion für zusammengesetze Schlüssel (Fortsetzung) ! Im allgemeinen ist auch hier zu berücksichtigen, dass ein Überlauf auftreten kann und hashCode eine negative int-Zahl zurückliefert. ! Wird der HashCode als Adresse für eine Hash-Tabelle benötigt, muss ein negativer Wert abgefangen und modolo der Tabellengröße gerechnet werden. ! Beispiel: Datum key = new Datum(24,12,2011); int adr = key.hashCode(); if (adr < 0) adr = -adr; adr = adr % m; ist in diesem Fall unnötig. Bei Datum.hashCode kann es keinen Überlauf geben. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-40 Adresskollision ! Im allgemeinen ist eine Hashfunktion nicht injektiv. D.h. unterschiedliche Schlüssel können die gleiche Hashadresse haben. ! Beispiel: mit h(k) = k mod m und m = 7 gilt: h(11) = h(25) = h(74) = 4. ! Die Wahrscheinlichkeit einer Adresskollision ist sehr groß. Sogar bei einer sehr gut streuenden Hashfunktion und einer im Vergleich zur Schlüsselzahl großen Hashtabelle. Das zeigt auch das sogenannte Geburtstagsparadoxon (s. nächste Folie) ! Ansätze zur Kollisionsbehandung: - kollidierende Datensätze werden in linear verketteten Listen gehalten. - Offene Hashverfahren: bei Kollision in der Tabelle nach freien Stellen sondieren Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-41 Einschub: Geburtstagsparadoxon ! In einer Gruppe von 23 Personen ist es wahrscheinlich (nämlich Wahrscheinlichkeit p ≈ 0.51), dass 2 Personen am gleichen Tag Geburtstag haben. ! Begründung: Wahrscheinlichkeit p, dass alle n = 23 Personen an unterschiedlichen Tagen Geburtstag haben: 364 363 343 p=€ * ** ≈ 0.49 365 365 365 2.Person 3.Person 23.Person Damit ist die Wahrscheinlichkeit p, dass wenigstens zwei Personen an einem gleichen Tag Geburtstag haben: € p = 1 − p ≈ 0.51 ! Konsequenz: würde man die Daten von 23 Personen in eine Tabelle der Größe 365 mit einem €Hashverfahren abbilden und als Hashfunktion h(x) = Geburtstag der Person x auf {0, 1, 2, …, 364} umgerechnet wählen, dann würde es wahrscheinlich eine Kollision geben. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-42 Teil I: Schlüsselbasiertes Suchen ! Dictionaries ! Elementare Suchverfahren ! Hashverfahren - Idee - - - - - ! ! ! ! Hashfunktion Hashverfahren mit linear verketteten Listen Offene Hashverfahren Dynamische Hashverfahren Hashverfahren in Java Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-43 Hashverfahren mit linear verketteten Listen (1) Idee Der Hashtabellen-Eintrag tab[i] zeigt auf eine verkette Liste, die alle Datensätze mit der gleichen Hashadresse i enthalten. Beispiel Für m = 7 mit h(k) = k mod m ergibt sich nach dem Einfügen von 12, 53, 5, 15, 2, 19, 43 folgende Hashtabelle: tab[0] [1] 15 [2] 2 43 [3] [4] 53 [5] 12 5 19 [6] Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-44 Hashverfahren mit linear verketteten Listen (2) Algorithmen: V search (K key) { suche in tab[ h(key) ] nach Schlüssel key; if (key gefunden) return Daten des gefunden Datensatzes; else return null; } V insert (K key, V value) { suche in tab[ h(key) ] nach Schlüssel key; if (key gefunden) // Schlüssel bereits vorhanden; alte Daten durch value ersetzen; return alte Daten; else { füge key am Ende oder am Anfang der Liste an; return null; } } Prof. Dr. O. Bittel, HTWG Konstanz V remove (K key) { suche in tab[ h(key) ] nach Schlüssel key; if (key gefunden) { entferne Knoten k aus Liste; return Daten von Knoten k; } else return null; } Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-45 Offene Hashverfahren (1) Idee: ! Alle Datensätze werden in einem Feld (d.h. keine verkettete Listen) untergebracht. ! Falls beim Eintragen des Schlüssels k die Adresse h(k) bereits belegt ist, wird gemäß einer Sondierungsfolge die erste freie Adresse gesucht und k dort abgespeichert. tab[0] [1] [2] [3] h(18) = 18 mod 7 = 4 [4] 53 18 [5] 12 Sondierungsfolge [6] 18 ! Beim Suchen von k wird ebenfalls die Sondierungsfolge durchlaufen. ! Zu unterschiedlichen Zeitpunkten können gleiche Schlüssel auf unterschiedliche Adressen abgebildet werden; daher auch die Bezeichnung Hashverfahren mit offener Adressierung. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-46 Offene Hashverfahren (2) Allgemeine Sondierungsfolge h(k) + s(j,k) mod m mit j = 0, 1, …, m-1 Beispiel (lineare Sondierungsfolge) h(k) + j mod m mit j = 0, 1, ..., m-1 und h(k) = k mod m tab[0] tab[0] [1] [1] 12, 53 und 4 eingefügt: Prof. Dr. O. Bittel, HTWG Konstanz [2] 10 und 18 eingefügt: [3] [4] 53 [5] 12 [6] 4 4 Sondierungsfolge für 4 18 [2] [3] 10 [4] 53 [5] 12 [6] 4 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 18 SS 2017 1-47 Offene Hashverfahren (3) Algorithmus zum Suchen: V search (K key) { if ( (adr = searchAdr(key) != -1) return tab[adr].value; else return null; } int searchAdr (K key) { j = 0; do { adr = (h(key) + s(j,key)) % m; j++; } while (tab[adr] != ″leer″ && tab[adr].key != key); if (tab[adr] != ″leer″) return adr; } else return -1; } Wichtig: Um Endlosschleifen bei stark gefüllten Tabellen zu vermeiden, sind die Längen der Sondierungsfolgen zu beschränken. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-48 Offene Hashverfahren (4) Algorithmus zum Löschen: V remove (K key) { if ( (adr = searchAdr(key) != -1) oldValue = tab[adr].value; tab[adr] = ″gelöscht″ ; return oldValue; } else return null; } Wichtig: Eine Hashadresse hat 3 Zustände: • Eintrag vorhanden • Eintrag gelöscht • Eintrag leer Zu Beginn sind alle Einträge leer. Grund: Beim Löschen können Lücken entstehen, die bei einer späteren Suchoperation übersprungen werden müssen. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-49 Offene Hashverfahren (5) Algorithmus zum Einfügen: void insert (K key, V value) { if ( (adr = searchAdr(key) != -1) { oldValue = tab[adr].value; tab[adr].value = value; return oldValue; } // Neueintrag: Es werden zuerst wieder j = 0; die Lücken gefüllt do { adr = (h(k) + s(j,k)) % m; j++; } while (tab[adr] != ″leer″ && tab[adr] != ″gelöscht″ ); tab[adr].key = key; tab[adr].value = value; return null; } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-50 Lineares Sondieren ! s(j,k) = j ! Damit ergibt sich die Sondierungsfolge: h(k) h(k) + 1 mod m h(k) + 2 mod m … ! Lineares Sondieren tendiert aufgrund von Sekundärkollisionen (zwei Sondierungsfolgen überschneiden sich) zu Clusterbildung: Große belegte Cluster haben eine stärkere Tendenz zu wachsen als kleinere. Füge k ein belegt … k Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-51 Quadratisches Sondieren ! s(j,k) = j2 ! Damit ergibt sich die Sondierungsfolge: h(k) h(k) + 1 mod m h(k) + 4 mod m h(k) + 9 mod m … ! Quadratisches Sondieren streut wesentlich besser als lineares Sondieren. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-52 Beachte beim quadratischen Sondieren ! Es wird mit einer quadratischen Sondierungsfolge im allgemeinen nicht jede Adresse erreicht. ! Beispiel: Mit m = 8 und h(k) = 0 erreicht die Sondierungsfolge nur 3 der 8 Einträge: 0 1 4 9 16 25 36 49 = 0 mod m, = 1 mod m = 4 mod m = 1 mod m = 0 mod m = 1 mod m = 4 mod m = 1 mod m ! Wenn m eine Primzahl ist, dann wird mit jeder Sondierungsfolge wenigstens die Hälfte aller Einträge erreicht. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-53 Alternierend quadratisches Sondieren ! Als Tabellengröße m wird eine Primzahl der Form 4i + 3 gewählt. ! Die alternierend quadratische Sondierungsfolge wird definiert durch: s(j,k) = ⎡j/2⎤2(-1)j ! Die Sondierungsfolge lautet konkret: h(k) h(k) – 1 mod m h(k) + 1 mod m h(k) – 4 mod m h(k) + 4 mod m ... ! Aufpassen: mod ist mathematisch definiert und unterscheidet sich für negative Zahlen vom Modolo-Operator % in Java. ! Mit alternierend quadratischem Sondieren werden alle Einträge erreicht. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-54 Einschub: symmetrische und mathematische mod-Funktion Symmetrische mod-Funktion: (siehe % in Java und C/C++) x mod m = x – (x/m)*m (/ ist hier eine Ganzzahldivision) Beispiele: -11 mod 7 = -11 – (-11/7)*7 = -11 – (-1)*7 = -4 11 mod 7 = 11 – (11/7)*7 = 11 – 1*7 = 4 Mathematische mod-Funktion: (siehe % in Python) x mod m = x – ⎣x/m⎦*m (/ ist hier eine Gleitkommadivision) Beispiele: -11 mod 7 = -11 – ⎣-11/7⎦*7 = -11 – (-2)*7 = 3 11 mod 7 = 11 – ⎣11/7⎦*7 = 11 – 1*7 = 4 Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-55 Double Hashing ! Für die Sondierungsfolge wird eine weitere Hashfunktion h' gewählt. ! Die Sondierungsfolge wird definiert durch: s(j,k) = j * h‘(k) ! Damit ergibt sich folgende Sondierungsfolge: h(k) h(k) + 1*h‘(k) mod m h(k) + 2*h‘(k) mod m h(k) + 3*h‘(k) mod m … ! Die beiden Hashfunktionen sollten möglichst unabhängig sein. Eine gute Wahl ist: h(k) = k mod m h‘(k) = (k+1) mod (m-2). ! Außerdem ist es wichtig, dass die Tabellengröße m eine Primzahl ist. Nur dann erreicht jede Sondierungsfolge alle Einträge. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-56 Analyse der Hashverfahren (1) Analyse im schlechtesten Fall ! Alle n Einträge haben die gleiche Hashadresse. Daher muss jede Operation eine Liste mit bis zu n Einträgen ablaufen. ! Dies ist in der Praxis so unwahrscheinlich, dass hier die Analyse im mittleren Fall wesentlich wichtiger ist. Analyse im mittleren Fall ! Wichtige Maßzahlen: C Anzahl der durchschnittlich betrachteten Einträge bei erfolgreicher Suche. C‘ Anzahl der durchschnittlich betrachteten Einträge bei nicht erfolgreicher Suche. ! Es zeigt sich, dass C und C‘ nur abhängig sind vom Belegungsfaktor α = n/m, wobei n = Anzahl der Einträge und m = Tabellengröße. ! Beachte, dass der Belegungsfaktor α bei Hashverfahren mit Verkettung größer 1 sein kann, jedoch bei bei offenen Hashverfahren kleiner gleich 1 sein muss. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-57 Analyse der Hashverfahren (2) C und C‘ für verschiedene Hashverfahren Verfahren C Hashverfahren mit Verkettung C’ α Offenes Hashverfahren mit linearem Sondieren Offenes Hashverfahren mit quadratischem Sondieren double hashing mit unabhängigen h u. h’ ! Die Angaben für C und C‘ setzen eine ideale Hashfunktion voraus. D.h. die Schlüssel sind gleichmäßig über die Tabelle verstreut. ! Die umfangreichen Herleitungen können in [Ottmann u. Widmayer 2002] nachgelesen werden. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-58 Analyse der Hashverfahren (3) C und C‘ für konkrete Belegungsfaktoren: α = 0.5 α = 2/3 α = 0.8 Verfahren C C’ C C’ C C’ Hashverfahren mit Verkettung 1.25 0.5 1.33 0.66 1.4 0.8 Offenes Hashverfahren mit linearem Sondieren 1.5 2.5 2 5 3 13 Offenes Hashverfahren mit quadratischem Sondieren 1.44 2.19 1.77 3.43 2.21 5.81 double hashing mit unabhängigen h u. h’ 1.38 2 1.65 3 2.01 5 ! Bei offenen Hashverfahren wird in der Praxis üblicherweise ein Belegungsfaktor von α ≤ 2/3 angestrebt. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-59 Teil I: Schlüsselbasiertes Suchen ! Dictionaries ! Elementare Suchverfahren ! Hashverfahren - Idee - - - - - ! ! ! ! Hashfunktion Hashverfahren mit linear verketteten Listen Offene Hashverfahren Dynamische Hashverfahren Hashverfahren in Java Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-60 Dynamische Hashverfahren (1) Problem ! Bei den bisher besprochenen Hashverfahren wird die Tabellengröße m einmalig festgelegt und die Hashfunktion damit parametrisiert. ! Wird ein bestimmter Füllungsgrad überschritten, dann kann das eingesetzte Hashverfahren sehr langsam werden, so dass eine Umorganisation notwendig wird. Vergrößern der Tabelle mit sofortigem Umkopieren: ! Falls beim Einfügen ein bestimmter Füllungsgrad überschritten wird, werden folgende Operationen durchgeführt: - Lege neue Tabelle tabʹ mit einer in etwa doppelten Größe mʹ an (mʹ sollte Primzahl sein); - Füge alle Einträge aus alter Tabelle tab in die neue Tabelle tabʹ ein; - Das alte Feld tab wird gelöscht und durch das neue ersetzt; ! Problem: Die insert-Operation, die zu einer Umorganisation der Hashtabelle führt, wird singulär sehr aufwendig. Wenn danach jedoch noch viele weitere insertOperationen durchgeführt werden, wird der Aufwand jedoch amortisiert. Problematisch kann der singulär erhöhte Aufwand bei einem interaktiven System sein. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-61 Dynamische Hashverfahren (2) Vergrößern der Tabelle mit verzögertem Umkopieren (lazy copying) ! nur für Hashverfahren mit Verkettung ! Falls beim Einfügen ein bestimmter Füllungsgrad überschritten wird: - Lege neue Tabelle tabʹ mit einer in etwa doppelten Größe mʹ an (mʹ sollte Primzahl sein). - Bei jedem Zugriff auf die Tabelle (search, insert bzw. remove) wird zusätzlich der nicht-leere Tabelleneintrag (komplette verkettete Liste) mit kleinstem Index min in die neue Tabelle übertragen. - Entscheide bei jedem Zugriff, ob Daten in alter oder neuer Tabelle stehen: Falls h(k) = k mod m ≤ min, dann greife auf neue Tabelle tabʹ zu, sonst auf alte Tabelle tab. - Beachte, dass bei alter Tabelle tab mit h(k) = k mod m und bei neuer Tabelle tabʹ mit hʹ(k) = k mod mʹ zugegriffen wird. - Falls alte Tabelle tab nicht mehr gebraucht wird (d.h. min = Größe der Tabelle tab), wird tab gelöscht und durch tabʹ ersetzt. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-62 Dynamische Hashverfahren (3) tab[0] Beispiel [1] 16 41 [2] 12 7 [3] 23 48 21 26 8 13 [4] insert(9): • Lege neue Tabelle tabʹ der Größe 11 an, da Füllungsgrad von tab überschritten wird; • Übertrage kleinsten Tabelleneintrag tab[min] mit min = 1 nach tabʹ; • Füge nun 9 in alte Tabelle ein (da h(9) = 9 mod 5 = 4 > min). tab‘[0] [1] min tab[0] [2] [1] [3] [2] 12 7 [3] 23 48 [4] 9 8 13 [4] 26 [5] 16 [6] [7] [8] 41 [9] [10] Prof. Dr. O. Bittel, HTWG Konstanz 21 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-63 Dynamische Hashverfahren (4) Beispiel (Fortsetzung): insert(32): • Übertrage kleinsten Tabelleneintrag tab[min] mit min = 2 nach tabʹ; • Füge nun 32 in neue Tabelle ein (h(32) = 32 mod 5 = 2 ≤ min). Da hʹ(32) = 32 mod 11 = 10 ist, wird 32 beim Index 10 eingetragen. tab‘[0] [1] min 12 tab[0] [2] [1] [3] [2] [4] 26 [5] 16 [3] 23 [4] 9 48 8 13 [6] [7] 7 [8] 41 [9] [10] Prof. Dr. O. Bittel, HTWG Konstanz 21 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 32 SS 2017 1-64 Hashverfahren in Java Iterable<K> Interface Klasse Collection<K> Set<K> Map<K,V> HashSet<K> HashMap<K,V> LinkedHashSet<K> LinkedHashMap<K,V> extends implements ! Bei LinkedHashSet und LinkedHashMap wird zusätzlich mit einer doppelt verketteten Liste über die Reihenfolge des Einfügens Buch geführt. ! Bei LinkedHashMap kann statt der Einfüge-Reihenfolge auch die Zugriffs-Reihenfolge eingestellt werden: least-recently to most-recently accessed ! Außer HashMap gibt es noch die Varianten WeakHashMap und IdentityHashMap. ! Wichtig: hashCode und equals muss für den Schlüsseltyp geeignet überschrieben werden. Falls o1.equals(o2), dann muss auch o1.hashCode() == o2.hashCode() sein. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-65 Teil I: Schlüsselbasiertes Suchen ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume - Begriffe, Eigenschaften und Traversierung (Wiederholung PROG 2) - Binäre Suchbäume (Wiederholung PROG 2) - Traversierung von Bäumen mit Elternzeiger ! Ausgeglichene Bäume ! Digitale Suchbäume ! kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-66 Bäume: Begriffe und Eigenschaften (1) Wurzel Elternknoten zu s und t Kante Rechtes Kind von k k Linkes Kind von k s t Linker Teilbaum von k ! Jeder Knoten hat maximal ein Elternknoten. Rechter Teilbaum von k Blatt Knoten ! Die Wurzel ist der einzige Knoten, der kein Elternknoten hat. ! Jeder Knoten kann beliebig viele Kinder haben. ! Keine Zyklen ! Blätter haben keine Kinder. ! Spezialfall Binärbaum: Jeder Knoten hat maximal 2 Kinder (linkes bzw. rechtes Kind) Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-67 Bäume: Begriffe und Eigenschaften (2) Höhe eines Baums Maximale Anzahl von Kanten von seiner Wurzel zu einem Blatt. Beispiel 10 3 5 3 0 2 7 6 Höhe für jeden Teilbaum 12 1 1 11 0 15 0 0 Beachte • Ein Baum, der nur aus einem Knoten besteht, besitzt die Höhe 0. • Aus technischen Gründen wird die Höhe eines leeren Baums (d.h. Anzahl Knoten = 0) als -1 definiert. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-68 Bäume: Begriffe und Eigenschaften (3) Vollständiger Binärbaum Ein vollständiger Binärbaum ist ein Binärbaum, bei der jede Ebene (bis auf die letzte) vollständig gefüllt und die letzte Ebene von links nach rechts gefüllt ist. Beispiele Eigenschaften • Ein vollständiger Binärbaum mit n Knoten hat die Höhe h = ⎣log2n⎦. • Vollständige Binärbäume sind (bei einer gegebenen Knotenzahl) Binärbäume mit einer minimalen Höhe. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-69 Bäume: Begriffe und Eigenschaften (4) Implementierung von Binärbäumen mit verketteten Knoten root ! Jeder Knoten hat jeweils eine Referenz für das linke und das rechte Kind. A B B D C E D H E F H I I Einfachheitshalber sind im Baum nur die Schlüssel dargestellt. Prof. Dr. O. Bittel, HTWG Konstanz C F G G A class Node<K,V> { K key; V value; Node<K,V> left; Node<K,V> right; } // linkes Kind // rechtes Kind Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-70 Traversierung von Binärbäumen Ziel ! Das Besuchen aller Knoten in einer bestimmten Reihenfolge ist eine oft benötigte Operation. Durchlaufreihenfolge ! ! ! ! PreOrder: besuche Wurzel, besuche linken Teilbaum; besuche rechten Teilbaum; PostOrder: besuche linken Teilbaum; besuche rechten Teilbaum; besuche Wurzel; InOrder: besuche linken Teilbaum; besuche Wurzel; besuche rechten Teilbaum; LevelOrder: besuche Knoten ebenenweise Bemerkungen ! Die Präfixe Pre, Post bzw. In bedeuten vorher, nachher und dazwischen. Gemeint ist damit der Zeitpunkt, an dem die Wurzel besucht wird. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-71 PreOrder-Durchlauf static void preOrder(Node<K,V> p) { if (p != null){ bearbeite(p); preOrder(p.left); preOrder(p.right); } } static class Node<K,V> { K key; V value; Node<K,V> left; Node<K,V> right; } private Node<K,V> root; //... static void main(...) { preOrder(root); } Beispiel * - a c Durchlaufreihenfolge: * - a b c b Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-72 PostOrder-Durchlauf static void postOrder(Node<K, V> p) { if (p != null){ postOrder(p.left); postOrder(p.right); bearbeite(p); } } Beispiel * - c Durchlaufreihenfolge: a b – c * a b Prof. Dr. O. Bittel, HTWG Konstanz (Entspricht der sog. Postfix-Notation für arithmetische Ausdrücke) Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-73 InOrder-Durchlauf für Binärbäume static void inOrder(Node<K, V> p) { if (p != null){ inOrder(p.left); bearbeite(p); inOrder(p.right); } } Beispiel * - a c Durchlaufreihenfolge: a - b * c b Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-74 LevelOrder-Durchlauf static void levelOrder(Node<K,V> p) { Queue<Node<K,V>> queue = new ArrayDeque<Node<K,V>>(); queue.add(p); while (!queue.isEmpty()) { Node<K,V> q = queue.remove(); if (q != null) { bearbeite(q); queue.add(q.left); queue.addd(q.right); } } Die Knoten werden ebenenweise in einer Schlange gespeichert und in einer while-Schleife abgearbeitet. } 1 2 4 3 5 Prof. Dr. O. Bittel, HTWG Konstanz 6 Durchlaufreihenfolge: 1, 2, 3, 4, 5, 6 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-75 Definition binärer Suchbäumen ! Ein binärer Suchbaum ist ein Binärbaum, bei dem für alle Knoten k folgende Eigenschaften gelten: - Alle Schlüssel im linken Teilbaum sind kleiner als k - Alle Schlüssel im rechten Teilbaum sind größer als k ! Beachte, dass die Zahlen hier eindeutig sein müssen. Es ist aber auch möglich, dass gleiche Zahlen mehrfach vorkommen dürfen, was kleine Änderungen in den Algorithmen erfordert. Beispiele: 7 6 1 Degenerierter Suchbaum 2 1 8 2 4 3 Prof. Dr. O. Bittel, HTWG Konstanz 8 3 4 6 3 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 4 7 SS 2017 1-76 Klasse BinarySearchTree public class BinarySearchTree<K, V> { static private class Node<K, V> { private K key; private V value; private Node<K, V> left; private Node<K, V> right; private Node(K k, V v) { key = k; value = v; left = null; right = null; } } private Node<K, V> root = null; // ... } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-77 Suchen in binären Suchbäumen public V search(K key) { return searchR(key, root); } Beispiel private V searchR(K key, Node<K,V> p) { if (p == null) return null; else if (key.compareTo(p.key) < 0) return searchR(key, p.left); else if (key.compareTo(p.key) > 0) return searchR(key, p.right); else return p.value; } Prof. Dr. O. Bittel, HTWG Konstanz 7 root 2 1 8 4 3 6 searchR(3, root) liefert true Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-78 Einfügen in binären Suchbäumen (1) Idee ! Um eine Zahl x einzufügen, wird zunächst nach x gesucht. ! Falls x nicht bereits im Baum vorkommt, endet die Suche erfolglos bei einer null-Referenz. ! An dieser Stelle wird dann ein neuen Knoten mit Eintrag x eingefügt. Beispiel 1: füge 6 ein Beispiel 2: füge 8 ein 7 2 1 7 9 4 3 Suche von 6 endet bei null Prof. Dr. O. Bittel, HTWG Konstanz 2 1 7 9 4 3 2 1 6 Ersetzte null durch neuen Knoten mit Eintrag 6 7 9 4 3 2 1 6 Suche von 8 endet bei null 9 4 3 8 6 Ersetzte null durch Knoten mit Eintrag 8 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-79 Einfügen in binären Suchbäumen (2) V oldValue; // Rückgabeparameter public V insert(K key, V value) { root = insertR(key, value, root); return oldValue; } private Node<K,V> insertR(K key, V value, Node<K,V> p) { if (p == null) { p = new Node(key, value); oldValue = null; } else if (key.compareTo(p.key) < 0) p.left = insertR(key, value, p.left); else if (key.compareTo(p.key) > 0) p.right = insertR(key, value, p.right); else { // Schlüssel bereits vorhanden: oldValue = p.value; p.value = value; } return p; } Prof. Dr. O. Bittel, HTWG Konstanz 7 root 2 1 root 9 4 3 7 2 1 9 4 3 6 root = insert(6, root); Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-80 Löschen in binären Suchbäumen (1) Idee ! Um eine Zahl x zu löschen, wird zunächst nach x gesucht. Es sind dann 4 Fälle zu unterscheiden: ! Fall „Nicht vorhanden“: x kommt nicht vor. Dann ist nichts zu tun. ! Fall „Keine Kinder“: x kommt in einem Blatt vor (keine Kinder): dann kann der Knoten einfach entfernt werden. ! Fall „Ein Kind“: Der Knoten, der x enthält, hat genau ein Kind: s. nächste Folie ! Fall „Zwei Kinder“: Der Knoten, der x enthält, hat zwei Kinder: s. übernächste Folie Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-81 Löschen in binären Suchbäumen (2) Fall: der zu löschende Knoten k hat ein Kind ! Überbrücke den Knoten k, indem der Elternknoten von k auf das Kind von k verzeigert wird (Bypass). Beispiel: lösche Knoten k mit Inhalt 4 7 2 1 7 9 4 k 3 Suche 4 Prof. Dr. O. Bittel, HTWG Konstanz 2 1 7 9 4 k 3 Knoten 4 wird überbrückt (Bypass) 2 9 1 3 Knoten 4 wird gelöscht Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-82 Löschen in binären Suchbäumen (3) Fall: der zu löschende Knoten k hat zwei Kinder ! Ersetze den Knoten k durch den kleinsten Knoten kmin im rechten Teilbaum von k. ! Lösche dann kmin. ! Da der Knoten kmin kein linkes Kind haben kann, kann das Löschen von kmin wie im Fall „Ein Kind“ bzw. „Keine Kinder“ behandelt werden. Beispiel: lösche Knoten k mit Inhalt 2 k 2 1 k 2 9 1 5 3 7 7 7 6 4 4 Suche kleinsten Knoten kmin in rechten Teilbaum von k Prof. Dr. O. Bittel, HTWG Konstanz k 3 1 5 kmin 3 6 Suche k mit Inhalt 2 9 7 9 5 kmin 3 3 1 9 5 6 4 Knoten k wird durch kmin ersetzt Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 6 4 kmin wird gelöscht SS 2017 1-83 Löschen in binären Suchbäumen (4) public V remove(K key) { root = removeR(k, root); return OldValue; } private Node<K,V> removeR(K key, Node<K,V> p) { if (p == null) { oldValue = null; } else if(key.compareTo(p.key) < 0) p.left = removeR(key, p.left); else if (key.compareTo(p.key) > 0) p.right = removeR(key, p.right); else { if (p.left == null || p.right == null) { // p hat ein oder kein Kind: oldValue = p.value; p = (p.left != null) ? p.left : p.right; } else { // p hat zwei Kinder: Entry<K,V> min = new Entry<K,V>(); p.right = getRemMinR(p.right, min); oldValue = p.value; p.key = min.key; p.value = min.value; } } return p; } Prof. Dr. O. Bittel, HTWG Konstanz private Node<K,V> getRemMinR(Node<K,V> p, Entry<K,V> min) { assert p != null; if (p.left == null) { min.key = p.key; min.value = p.value; p = p.right; } else p.left = getRemMinR(p.left, min); return p; } private static class Entry<K, V> { private K key; private V value; } ! getRemMinR löscht im Baum p den Knoten mit kleinstem Schlüssel und liefert Schlüssel und Daten des gelöschten Knotens über min zurück ! Entry ist Hilfsdatentyp für Rückgabe-Parameter min von getRemMinR Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-84 Analyse Worst-Case ! Im schlechtesten Fall kann ein binärer Suchbaum mit n Knoten zu einem Baum der Höhe n-1 entarten. ! Damit: Tmax(n) = O(n) Average-Case ! In [Ottmann und Widmayer 2002] werden zwei Ergebnisse hergeleitet, die sich darin unterscheiden, welche Verteilung der Bäume angenommen wird. ! Bäume mit n Knoten entstehen durch eine Folge von Einfüge-Operationen von n unterschiedlichen Elementen. Es wird angenommen, dass jede der n! möglichen Anordnungen der Elemente gleich wahrscheinlich ist. Damit: Tmit(n) = O(log2n) ! Es wird angenommen, dass alle strukturell verschiedenen binären Suchbäume mit n Knoten gleichwahrscheinlich sind. Damit: Tmit(n) = O( Prof. Dr. O. Bittel, HTWG Konstanz ) Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-85 Traversierung von Binärbäumen Problem ! In binären Suchbäumen gibt es für einen Knoten im allgemeinen keinen effizienten Zugriff auf seinen InOrder-Vorgänger bzw. -Nachfolger. ! Daher ist mit den bisher besprochenen Suchbäumen eine effiziente Vorwärts- bzw. Rückwärtstraversierung mit Iteratoren nicht machbar. 10 5 ! Wie erreicht man den Nachfolger von 8? 12 ! Wie erreicht man den Vorgänger von 6? 3 1 Prof. Dr. O. Bittel, HTWG Konstanz 7 6 15 8 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-86 Bäume mit Elternzeigern ! Erweitere jeden Knoten um einen Zeiger auf den Elternknoten. 10 12 5 3 1 Prof. Dr. O. Bittel, HTWG Konstanz 15 7 6 8 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-87 Klasse BinarySearchTree mit Elternzeiger (1) ! Erweitere Klasse Node um Elternzeiger parent: public class BinarySearchTree<K, V> { static private class Node<K, V> { private Node<K, V> parent; // Elternzeiger private K key; private V value; private Node<K, V> left; private Node<K, V> right; private Node(K k, V v) { key = k; value = v; left = null; right = null; parent = null; } } private Node<K, V> root = null; // ... } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-88 Klasse BinarySearchTree mit Elternzeiger (2) ! Überall, wo die Struktur des Baums geändert wird, wird zusätzlich der parent-Zeiger neu gesetzt: expr1.left = expr2; if (expr1.left != null) expr1.left.parent = expr1; expr1.left = expr2; 10 expr1 10 ... 5 expr2 ... expr1 ... expr2 5 ... ... ... ... expr1 und expr2 sind beliebige Ausdrücke vom Typ Node. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-89 Klasse BinarySearchTree mit Elternzeiger (3) ! Analog: expr1.right = expr2; expr1.right = expr2; if (expr1.right != null) expr1.right.parent = expr1; ! Beachte, dass der parent-Zeiger des root-Knotens den Wert null bekommt: root = expr; Prof. Dr. O. Bittel, HTWG Konstanz root = expr; if (root != null) root.parent = null; Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-90 Beispiel: insert-Methode mit Elternzeiger public V insert(K key, V value) { root = insertR(key, value, root); if (root != null) root.parent = null; return oldValue; } Prof. Dr. O. Bittel, HTWG Konstanz private Node<K,V> insertR(K key, V value, Node<K,V> p) { if (p == null) { p = new Node(key, value); oldValue = null; } else if (key.compareTo(p.key) < 0) { p.left = insertR(key, value, p.left); if (p.left != null) p.left.parent = p; } else if (key.compareTo(p.key) > 0) { p.right = insertR(key, value, p.right); if (p.right != null) p.right.parent = p; } else { // Schlüssel bereits vorhanden: oldValue = p.value; p.value = value; } return p; } Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-91 Traversierung von Bäumen ! Hier: nur Traversierung zum InOrder-Nachfolger. Traversierung zum InOrder-Vorgänger geht analog. ! Sei p der aktuelle Knoten. Gesucht ist der Nachfolger von p. Wir unterscheiden zwei Fälle: if (p.right != null) p = leftMostDescendant(p.right); else p = parentOfLeftMostAncestor(p); parentOfLeftMostAncestor(p) p ... ... ... ... p leftMostDescendant(p.right) ... Prof. Dr. O. Bittel, HTWG Konstanz ... Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-92 leftMostDescendant 10 InOrder-Nachfolger von p = 5: leftMostDescendant(p.right) = 6 12 5 3 1 15 7 6 Prof. Dr. O. Bittel, HTWG Konstanz 8 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-93 parentOfLeftMostAncestor 10 InOrder-Nachfolger von p = 8: parentOfLeftMostAncestor(p) = 10 12 5 3 1 15 7 6 Prof. Dr. O. Bittel, HTWG Konstanz 8 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-94 Implementierung von leftMostDescendant private Node<K,V> leftMostDescendant(Node<K,V> p) { assert p != null; while (p.left != null) p = p.left; return p; } p ... leftMostDescendant(p.right) ... Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-95 Implementierung von parentOfLeftMostAncestor private Node<K,V> parentOfLeftMostAncestor(Node<K,V> p) { assert p != null; while (p.parent != null && p.parent.right == p) p = p.parent; return p.parent; // kann auch null sein } parentOfLeftMostAncestor(p) ... ... p ... Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-96 Traversierungsschleife für InOrder-Nachfolger // Erster Knoten: Node<K,V> p = leftMostDescendant(root); System.out.print(p.key + ", "); 10 while( p != null) { if (p.right != null) p= leftMostDescendant(p.right); else p = ParentOfLeftMostAncestor(p); System.out.print(p.key + ", "); } 12 5 3 1 15 7 6 8 ! Traversersierung-Schleife ergibt: 1, 3, 5, 6, 7, 8, 10, 12, 15 Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-97 Teil I: Schlüsselbasiertes Suchen ! ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume Ausgeglichene Bäume - AVL-Bäume - B-Bäume - 2-3-4-Bäume und Rot-Schwarz-Bäume ! Digitale Suchbäume ! kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-98 Definition von AVL-Bäumen Definition ! Ein binärer Suchbaum heißt AVL-Baum oder (höhen)balanzierter Baum, falls sich für jeden Knoten k die Höhen der beiden Teilbäume um höchstens 1 unterscheiden. ! Die Abkürzung AVL geht zurück auf Adelson-Velskij und Landis. Beispiel für AVL-Baum: 5 10 3 10 3 -1 -2 2 5 12 1 +1 0 3 0 7 0 -1 6 Beispiel für Nicht-AVL-Baum 1 0 0 11 0 0 2 12 0 0 +2 15 0 0 7 Höhe des Teilbaums Höhenunterschied = Höhe rechter Teilbaum − Höhe linker Teilbaum 1 +1 8 0 0 (Beachte: Höhe eines leeren Teilbaums ist -1.) Eigenschaft ! Ein AVL-Baum hat eine maximale Höhe von etwa 1.5 log2n. [Ottmann u. Widmayer]. ! Damit lassen sich alle Dictionary-Operationen in O(log n) realisieren. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-99 Rotationen (1) Zentrale Eigenschaft von AVL-Bäumen: Ein unbalanzierter Baum, der einen Höhenunterschied von -2 (linkslastiger Baum) oder +2 (rechtslastiger Baum) hat und dessen Teilbäume ausbalanziert sind, lassen sich durch eine einfache Rotationsoperation ausbalanzieren. Fall A: Baum ist linkslastig, d.h. Höhenunterschied = -2 Unterfall A1: linker Teilbaum hat Höhenunterschied -1 oder 0: q p -2 q h-1 h+1 -1 oder 0 A B Prof. Dr. O. Bittel, HTWG Konstanz h p h od. h+1 A C h od. h-1 h 1 od. 0 Rotate Right Teilbäume A, B, und C sind ausbalanziert und können auch leer sein. h od. h-1 B Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen h-1 C SS 2017 1-100 Rotationen (2) Fall A: Baum ist linkslastig, d.h. Höhenunterschied = -2 Unterfall A2: linker Teilbaum hat Höhenunterschied +1: 2. p 1. -2 q h+1 h-1 +1 Rotate Left Right r 0 D h-1 r h h-1 od. h-2 A B q h p h h-2 od. h-1 C Prof. Dr. O. Bittel, HTWG Konstanz A B Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen C D SS 2017 1-101 Rotationen (3) Fall B: Baum ist rechtslastig, d.h. Höhenunterschied = +2 Unterfall B1: rechter Teilbaum hat Höhenunterschied 0 oder +1: q p +2 h-1 q h+1 p 0 oder +1 A B h od. h+1 0 oder +1 h od. h-1 Prof. Dr. O. Bittel, HTWG Konstanz 0 od. -1 Rotate Left h-1 h C A h C h od. h-1 B Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-102 Rotationen (4) Fall B: Baum ist rechtslastig, d.h. Höhenunterschied = +2 Unterfall B2: rechter Teilbaum hat Höhenunterschied -1: 2. p +2 h-1 h+1 Rotate Right Left 1. q r 0 -1 A r h h-2 od. h-1 B h-1 h-1 od. h-2 C Prof. Dr. O. Bittel, HTWG Konstanz p h q h D A B Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen C D SS 2017 1-103 Einfügen und Löschen in AVL-Bäumen Idee für Algorithmus: 1) Füge ein bzw. lösche wie bei binären Suchbäumen. 2) Gehe dann von der Einfügestelle bzw. Löschstelle bis zur Wurzel und balanziere falls notwendig mit einer Rotatationsoperation lokal aus. Bemerkung: ! Da der Baum vor dem Einfügen bzw. Löschen ausbalanziert war, haben alle Teibäume einen Höhenunterschied von -1, 0 oder +1. Damit können nach dem Schritt 1) nur die Fälle A1, A2, B1 oder B2 auftreten. ! Schritt 2) läßt sich geschickt beim Aufstieg aus der Rekursion durchführen. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-104 Einfügen in AVL-Bäumen (1) Beispiel: 2 2 Füge 5 ein 3 1 3 1 Gehe vom Knoten 5 in Richtung Wurzel bis zum ersten unbalanzierten Teilbaum. 4 4 5 2 1 2 Rotate Left (Fall B1) 3 1 4 +2 4 3 5 +1 5 0 Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-105 Einfügen in AVL-Bäumen (2) Beispiel: 4 4 2 2 6 Gehe bis zum ersten unbalanzierten Teilbaum. 6 Füge 13 ein 3 1 5 7 3 1 14 5 14 7 15 15 13 4 4 2 6 2. +2 1 3 1. 5 2 1 14 7 Rotate Right Left (Fall B2) 15 7 3 6 5 14 13 15 13 Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-106 Algorithmen (1) ! Node wie bei binären Suchbäumen. Zusätzlich wird für jeden Knoten im Baum noch die Höhe des entsprechenden Teilbaums abgespeichert. static private class Node<K, V> { int height; K key; V value; Node<K, V> left; Node<K, V> right; private Node(K k, V v) { height = 0; key = k; value = v; left = null; right = null; } } Prof. Dr. O. Bittel, HTWG Konstanz private int getHeight(Node<K,V> p) { if (p == null) return -1; else return p.height; } private int getBalance(Node<K,V> p) { if (p == null) return 0; else return getHeight(p.right) - getHeight(p.left); } Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-107 Algorithmen (2) ! Erweitere die rekursive insertR-Operation für binäre Suchbäume um eine Balanzieroperation, die beim rekursiven Aufstieg durchgeführt wird. private Node<K,V> insertR(K key, V value, Node<K,V> p) { if (p == null) { p = new Node(key, value); oldValue = null; } else if (key.compareTo(p.key) < 0) p.left = insertR(key, value, p.left); else if (key.compareTo(p.key) > 0) p.right = insertR(key, value, p.right); else { // Schlüssel bereits vorhanden: oldValue = p.value; p.value = value; } p = balance(p); return p; } ! Die removeR- und getRemMinR-Operation werden analog erweitert. ! Die searchR-Operation bleibt unverändert. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-108 Algorithmen (3) private Node<K,V> balance(Node<K,V> p) { if (p == null) return null; p.height = Math.max(getHeight(p.left), getHeight(p.right)) + 1; if (getBalance(p) == -2) { if (getBalance(p.left) <= 0) Fall A1 p = rotateRight(p); else p = rotateLeftRight(p); Fall A2 } else if (getBalance(p) == +2) { if (getBalance(p.right) >= 0) p = rotateLeft(p); Fall B1 else p = rotateRightLeft(p); Fall B2 } return p; } Prof. Dr. O. Bittel, HTWG Konstanz Höhe aktualisieren. Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-109 Algorithmen (3) private Node<K,V> rotateRight(Node<K,V> p) { assert p.left != null; Node<K, V> q = p.left; p.left = q.right; q.right = p; p.height = Math.max(getHeight(p.left), getHeight(p.right)) + 1; q.height = Math.max(getHeight(q.left), getHeight(q.right)) + 1; return q; } Rotate Right p q q p C A Prof. Dr. O. Bittel, HTWG Konstanz B A B Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen C SS 2017 1-110 Algorithmen (4) private Node<K,V> rotateLeft(Node<K,V> p) { // analog zu rotateRight } private Node<K,V> rotateLeftRight(Node<K,V> p) { assert p.left != null; p.left = rotateLeft(p.left); return rotateRight(p); } private Node<K,V> rotateRightLeft(Node<K,V> p) { assert p.right != null; p.right = rotateRight(p.right); return rotateLeft(p); } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-111 Teil I: Schlüsselbasiertes Suchen ! ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume Ausgeglichene Bäume - AVL-Bäume - B-Bäume - 2-3-4-Bäume und Rot-Schwarz-Bäume ! Digitale Suchbäume ! kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-112 Definition von B-Bäumen der Ordnung m 1. Balanzierung: Jedes Blatt hat die gleiche Tiefe. 2. Anzahl Schlüssel: - Jeder Knoten hat maximal m-1 Schlüssel. - Jeder Knoten außer der Wurzel hat minimal ⎡m/2⎤ -1 Schlüssel. 3. Anzahl Kinder: Jeder Knoten (außer den Blättern) mit i vielen Schlüsseln hat genau i+1 Kinder. (Beachte, dass ein B-Baum aus genau einem Knoten – die Wurzel – bestehen darf) 4. Schlüsselordnung: Für jeden Knoten K mit i-1 Schlüsseln k0, k1, …, ki-2 und i Kindern mit ihren Teilbäumen B0, B1, …, Bi-1 gilt folgende Beziehung: Schlüssel in B0 < k0 < Schlüssel in B1 < k1 < Schlüssel in B2 < … Schlüssel in Bi-2 < ki-2 < Schlüssel in Bi-1 . k0 k1 … ki-2 Knoten K mit seinen i-1 Schlüsseln i Kinder mit ihren Teilbäumen B0 Prof. Dr. O. Bittel, HTWG Konstanz B1 B2 … Bi-2 Bi-1 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-113 Beispiele für B-Bäume der Ordnung m = 4 B-Baum der Höhe 0 20 B-Baum der Höhe 1 20 60 10 60 90 B-Baum der Höhe 2 20 10 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 30 21 24 33 60 35 50 38 43 45 70 52 55 90 65 67 69 80 85 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 96 SS 2017 1-114 Suchen in B-Bäumen V search(K key, Node<K,V> p) { if (p == 0) return null; // nicht gefunden; suche key in Schlüsselmenge von Knoten p (beispielsweise mit binärer Suche); if (key gefunden) return Daten; } p = Teilbaum, in dem key liegen könnte; return search(key, p); Rekursiver Aufruf Beispiel: Suchen nach k = 43 20 10 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 30 21 24 33 60 35 50 38 43 45 70 52 55 90 65 67 69 80 85 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 96 SS 2017 1-115 Einfügen in B-Bäumen V insert(K key, V value, Node<K,V> p) { suche Schlüssel key im Baum p; if (key gefunden) { speichere Daten in oldValue; ersetze Daten durch value; return oldValue; } füge key,value an der Blattstelle ein, wo die Suche beendet wurde; if (kein Schlüsselüberlauf) return null; ! Schlüsselüberlauf, falls Anzahl der Schlüssel gleich m. ! insert kann wie bei AVL-Bäumen rekursiv realisiert werden. Die Behandlung der Schlüsselüberläufe geschieht dann beim rekursiven Aufstieg. ! Muss die Wurzel gesplittet werden, dann entsteht eine neue Wurzel (siehe Beispiel) for (alle Knoten k vom aktuellen Knoten bis zur Wurzel) { if (Schlüsselüberlauf bei k) führe Splittoperation für k durch; else return null; } } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-116 Splitoperation ... ... Elternknoten k0 k1 … Kmit-1 kmit Kmit+1 … km-1 Splitoperation ... k0 k1 … Kmit-1 Prof. Dr. O. Bittel, HTWG Konstanz Aktueller Knoten mit Schlüsselüberlauf (Knoten enthält genau m Schlüssel) Teile Schlüsselmenge an der Stelle mit = ⎣m/2⎦ kmit ... Kmit+1 … km-1 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-117 Beispiel 1: Einfügen von 82 20 10 11 12 15 5 7 30 21 24 35 33 10 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 30 21 24 33 50 38 43 45 20 1) Suchen von 82 60 70 52 55 65 67 69 80 85 50 38 43 45 96 2) 82 einfügen; keine Splittoperation notwendig, da genügend Platz 60 35 90 70 52 55 90 65 67 69 80 82 85 96 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-118 Beispiel 2: Einfügen von 39 20 10 11 12 15 5 7 30 35 33 21 24 1) Suchen von 39 60 50 38 43 45 70 52 55 65 67 69 90 80 82 85 96 2) 39 einfügen 20 10 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 30 21 24 33 60 35 3) Splittoperation anwenden, da nicht genügend Platz 50 38 39 44 45 52 55 70 90 65 67 69 80 82 85 96 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-119 Beispiel 2: Einfügen von 39 (Fortsetzung) 20 30 10 5 7 11 12 15 21 24 33 35 38 39 20 30 10 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 21 24 33 4) weitere Splittoperation anwenden 60 44 35 38 39 44 50 45 70 52 55 65 67 69 80 82 85 96 5) Fertig! 60 50 45 90 70 52 55 90 65 67 69 80 82 85 96 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-120 Beispiel 3: Erzeugen eines neuen Knotens bei einer Einfüge-Operation B-Baum mit genau einem Knoten mit 3 Schlüsseln 20 10 wird gesucht und eingefügt 50 10 20 40 50 Splittoperation anwenden, da nicht genügend Platz. Dabei wird ein neuer Konten erzeugt. Prof. Dr. O. Bittel, HTWG Konstanz 40 40 10 20 50 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-121 Löschen in B-Bäumen V remove(K key, Node<K,V> p) { suche Schlüssel key im Baum; if (key nicht gefunden) return null; if (key befindet sich im Blatt) speichere Daten in oldValue und lösche key; else speichere Daten in oldValue und ersetze key (mit value) durch nächst größeren Schlüssel key’ (mit value') und lösche key’; // key’ befindet sich im rechten Teilbaum von key ganz links in einem Blatt for (alle Knoten k vom aktuellen Knoten bis zur Wurzel) { if ( Schlüsselunterlauf bei k) { if (Geschwisterknoten kann Schlüssel abgeben) Übernahme von Schlüsseln vom Geschwisterknoten; else Verschmelze Knoten k mit einem Geschwisterknoten; } else return oldValue; } ! Schlüsselunterlauf, falls Anzahl der Schlüssel kleiner als ⎡m/2⎤ -1. ! remove kann wie bei AVLBäumen rekursiv realisiert werden. Die Behandlung der Schlüsselunterläufe geschieht dann beim rekursiven Aufstieg. ! Hat die Wurzel genau 2 Kinder, die verschmolzen werden müssen, dann wird die Wurzel leer und muss entfernt werden (siehe Beispiel). } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-122 Behandlung eines Schlüsselunterlaufs: Übernahme von Schlüssel k1 k2 Linker Geschwisterknoten Aktueller Knoten; Schlüsselmenge ist zu klein Elternknoten Rechter Geschwisterknoten ! Prüfe zuerst, ob linker Geschwisterknoten Schlüssel abgeben kann. Falls nicht, dann prüfe rechten Knoten. ! Abgabe von Schlüsseln so, dass Knoten in etwa gleich groß werden. ! Verfahren kann dann abgebrochen werden, da im Elternknoten kein Schlüsselunterlauf entstehen kann. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-123 Übernahme von Schlüssel (hier vom linken Geschwisterknoten) ... ... ... k1 k2 ... ... ... ... Elternknoten ... ... ... ... ... Aktueller Knoten Linker Geschwisterknoten Rechter Geschwisterknoten Linker Geschwisterknoten gibt dem aktuellen Knoten gerade soviele Schlüssel ab, so dass sich die Knotenanzahl um maximal 1 unterscheidet. k2 ... ... ... ... Prof. Dr. O. Bittel, HTWG Konstanz ... ... k1 ... ... ... ... Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-124 Behandlung eines Schlüsselunterlaufs: Verschmelzung k1 k2 Linker Geschwisterknoten Aktueller Knoten; Schlüsselmenge ist zu klein Elternknoten Rechter Geschwisterknoten ! Beide Geschwisterknoten haben zu wenig Schlüssel, um Schlüssel abgeben zu können. ! Führe daher Verschmelzung mit einem Geschwisterknoten durch. ! Fasse aktuellen Knoten mit linkem Geschwisterknoten (falls vorhanden) zusammen. Sonst fasse mit rechtem Geschwisterknoten zusammen. ! Bei Elternknoten wird ein Schlüssel nach unten verschoben. Daher kann nun beim Elternknoten ein Schlüsselunterlauf vorkommen. Führe daher Verfahren gegebenenfalls rekursiv beim Elternknoten fort. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-125 Verschmelzung (hier mit linkem Geschwisterknoten) ... ... k1 k2 ... Elternknoten ... ... ... ... Aktueller Knoten Linker Geschwisterknoten Rechter Geschwisterknoten Fasse aktuellen Knoten mit linkem Geschwisterknoten zusammen ... ... k2 ... ... k1 ... Prof. Dr. O. Bittel, HTWG Konstanz ... ... Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-126 Beispiel 1: Löschen von 96 20 30 10 5 7 11 12 15 21 24 33 35 30 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 21 24 33 1) Suchen und Löschen von 96 60 50 38 39 20 10 44 44 35 38 39 45 70 52 55 65 67 69 80 82 85 96 2) Schlüsselunterlauf: Geschwisterknoten kann Schlüssel abgeben. Fertig! 60 50 45 90 70 52 55 85 65 67 69 80 82 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 90 SS 2017 1-127 Beispiel 2: Löschen von 80 20 30 10 5 7 11 12 15 21 24 33 35 30 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 21 24 33 60 1) Suchen und Löschen von 80 50 38 39 20 10 44 44 35 38 39 45 70 52 55 65 80 2) Schlüsselunterlauf: Verschmelzung mit Geschwisterknoten. 60 50 45 52 55 65 70 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-128 Beispiel 2: Löschen von 80 (Fortsetzung) 20 30 10 5 7 11 12 15 21 24 33 35 30 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 21 24 33 3) Weiterer Schlüsselunterlauf: Verschmelzung mit Geschwisterknoten. 60 50 38 39 20 10 44 45 52 55 65 70 4) Kein weiterer Schlüsselunterlauf: daher fertig! 44 35 50 60 38 39 45 52 55 65 70 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-129 Beispiel 3: Löschen von 60 20 10 11 12 15 5 7 30 21 24 33 30 5 7 11 12 15 Prof. Dr. O. Bittel, HTWG Konstanz 21 24 33 60 1) Suchen und Löschen von 60 50 35 45 38 39 20 10 44 44 35 38 39 65 70 52 55 65 67 69 80 82 90 2) Schüssel 60 befindet sich nicht in Blatt: daher durch nächst größeren Schlüssel 65 ersetzen und Schlüssel 65 löschen. 3) Kein Schlüsselunterlauf, daher fertig! 50 45 85 70 52 55 85 65 67 69 80 82 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 90 SS 2017 1-130 Beispiel 4: Wurzel muss entfernt werden 40 1) Suchen und Löschen von 50 20 50 2) Schlüsselunterlauf; Verschmelzung mit Geschwisterknoten 20 40 3) Schlüsselunterlauf in der Wurzel; Wurzel entfernen 20 Prof. Dr. O. Bittel, HTWG Konstanz 40 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-131 Analyse (1) Höhe eines B-Baumes mit n Schlüsseln: ! Im schlechtesten Fall hat die Wurzel 2 Kinder und jeder andere Nicht-BlattKnoten ⎡m/2⎤ viele Kinder. ! Damit ergibt sich eine maximale Höhe von (siehe [Ottmann u. Widmayer]): h ≤ log ⎡m/2⎤ (n+1)/2 Beispiel: ! Für m = 256 und n = 109 ergibt sich eine maximale Höhe von h ≤ 4.1 Aufwand für Suchen, Einfügen und Löschen: ! Da die Höhe eines B-Baums durch log ⎡m/2⎤ (n+1)/2 beschränkt ist, ergibt sich eine maximale Laufzeit von: T(n) = O( log ⎡m/2⎤ (n) ) Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-132 Analyse (2) Speicherplatzausnutzung: ! Eine naheliegende Implementierung eines B-Baums sieht für jeden Knoten ein statisches Feld der Größe m für die Schlüssel und Zeiger auf die Kinder vor. ! Da jeder Knoten (außer Wurzel) zwischen ⎡m/2⎤ -1 und m-1 viele Schlüssel enthält, stellt sich die Frage, wie groß die Speicherplatzausnutzung ist. ! Es gilt folgende Eigenschaft (siehe [Ottmann u. Widmayer]): Wenn eine zufällig gewählte Folge von n Schlüsseln in einen anfangs leeren B-Baum der Ordnung m eingefügt werden, dann ist eine Speicherplatzausnutzung zu erwarten von: ln(2) ≈ 69 %. ! Falls eine absteigend oder aufsteigend sortierte Folge in einen leeren B-Baum einfügt wird, ergibt sich eine besonders schlechte Speicherplatzausnutzung, die aber immer noch bei ca. 50 % liegt. ! Etwas lindern lässt sich das Problem, indem nicht bei jedem Knotenüberlauf eine Splittoperation durchgeführt wird, sondern zuvor geprüft wird, ob Schlüssel an Geschwisterknoten abgegeben werden können. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-133 Anwendungen Externe Suchverfahren ! Eine der wichtigsten Anwendungen von B-Bäume sind externe Suchverfahren (z.B. im Datenbankbereich). Die Menge der Datensätze ist dabei so groß, dass sie nur auf Hintergrundspeicher wie beispielsweise Festplatte abgespeichert werden kann. ! Die Knotengröße m wird dann so gewählt, dass mit einem Festplattenzugriff die gesamten Daten eines Knotens (d.h. Schlüssel und Zeiger; Indexseite) gelesen werden können. ! Die Datensätze selbst sind auf der Platte gespeichert. Beispiel: ! Wählt man beispielsweise m = 256 und sind ca. n = 109 Datensätze zu verwalten, dann ergibt sich ein B-Baum etwa der Höhe 4. Speichert man die beiden obersten Ebenen des B-Baums (d.h. Wurzel und seine Kinder) im Hauptspeicher (das sind maximal 255 + 256*255 = 65535 Schlüssel), dann kommt man im schlechtesten Fall bei einer Suchoperation mit 3 Plattenzugriffe aus. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-134 Variante: B*-Baum ! Zusätzliche Forderung: jeder Knoten (außer der Wurzel) muss mindestens zu 2/3 gefüllt sein. ! Vorteil: bessere Speicherplatzausnutzung und geringere Höhe des Baumes. ! Nachteil: bei den Operationen Einfügen und Löschen kommt ein Überschreiten der Maximalbzw. Minimalzahl der Schlüssel je Knoten im Vergleich zum B-Baum häufiger vor. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-135 Variante: B+-Baum ! Die Datensätze werden nur in den Blättern abgespeichert. ! Um die Bereichssuche zu unterstützen (finde alle Datensätze mit Schlüssel k ∈ [k1, k2]), werden die Blätter miteinander verkettet. ! In den Nicht-Blatt-Knoten stehen nur Schlüssel und Zeiger zu Navigationszwecken. Der i-te Schlüssel ki befindet sich noch zusätzlich als kleinster Schlüssel im Teilbaum Bi+1 ! Für die Anzahl der Schlüssel in den Blättern kann eine andere Minimal- und Maximalzahl gelten. 21 48 8 246 8 12 15 21 24 48 66 51 51 53 87 72 66 95 72 75 87 88 89 97 95 96 97 Datensätze mit Schlüssel und Nutzdaten Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-136 Teil I: Schlüsselbasiertes Suchen ! ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume Ausgeglichene Bäume - AVL-Bäume - B-Bäume - 2-3-4-Bäume und Rot-Schwarz-Bäume ! Digitale Suchbäume ! kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-137 2-3-4-Bäume ! B-Bäume der Ordnung 4 nennt man auch 2-3-4-Bäume. Ein Nicht-Blatt-Knoten hat 2, 3 oder 4 Kinder. 20 10 5 7 11 12 15 30 21 24 33 60 35 50 38 43 45 70 52 55 90 65 67 69 80 85 96 ! 2-3-4-Bäume sind eine sehr gut geeignete Alternative zu AVL-Bäumen. ! 2-3-4-Bäume lassen sich auch als Top-Down-Variante realisieren: beim Einfügen bzw. Löschen wird der Baum mit einer Schleife von der Wurzel in Richtung der Blätter durchlaufen. Dabei werden Split- bzw. Verschmelzungsoperationen prophylaktisch durchgeführt. Eingefügt bzw. gelöscht wird nur im Blatt. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-138 2-3-4-Bäume und Rot-Schwarz-Bäume (1) ! Eine beliebte Implementierungsvariante von 2-3-4-Bäumen sind Rot-Schwarz-Bäume, die mit einer wesentlich einfacheren Datenstruktur auskommen (wie bei binären Suchbäumen) und außerdem top-down realisiert werden können. Beispiel: Java-Collections und STL-Bibliothek von C++. ! Rot-Schwarz-Bäume sind binäre Suchbäume, dessen Knoten entweder rot oder schwarz sind und für die bestimmte Färbungsregeln gelten. ! Ein 2-3-4-Baum wird in ein Rot-Schwarz-Baum transformiert, indem 3-Knoten (Knoten mit 3 Schlüsseln) und 2-Knoten mittels einer einfachen Regel ersetzt werden. 1-Knoten bleiben unverändert. k2 k1 k2 k3 k1 k3 k2 k1 k2 oder k1 Prof. Dr. O. Bittel, HTWG Konstanz k1 k2 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-139 2-3-4-Bäume und Rot-Schwarz-Bäume (2) Beispiel 30 15 5 10 50 20 55 40 45 60 70 65 80 85 90 30 60 15 10 5 50 20 40 55 45 Prof. Dr. O. Bittel, HTWG Konstanz 70 65 85 80 90 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-140 Färbungsregeln von Rot-Schwarz-Bäumen Ein Rot-Schwarz-Baum ist ein binärer Suchbaum mit folgenden Färbungsregeln: (1) Jeder Knoten ist entweder rot oder schwarz. (2) Die Wurzel ist immer schwarz. (3) Ein roter Knoten darf kein rotes Kind haben. (4) Jeder Pfad von der Wurzel zu einem Blatt hat die die gleiche Anzahl an schwarzen Knoten. Bemerkung: ! Durch die 4 Färbungsregeln wird gewährleistet, dass ein Rot-Schwarz Baum mit den Transformationsregeln von Seite 1-131 auch wieder einem 2-3-4 Baum entspricht. ! Rot-Schwarz-Bäume werden mit derselben Datenstruktur wie binäre Suchbäume realisiert. Jedoch wird zusätzlich ein Bit für die Farbe (rot oder schwarz) verwendet. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-141 Einfügen eines Schlüssels in einen Rot-Schwarz-Baum (1) ! durchlaufe Rot-Schwarz-Baum rsb mit einer Schleife von seiner Wurzel bis zu demjenigen Blatt, unter dem der neue Knoten mit Schlüssel key eingefügt werden soll; ! füge neuen roten Knoten mit Schlüssel key ein; q = Wurzel des Baums rsb; while (q ist nicht das Blatt, unter dem key eingefügt werden soll) { evtl. Farbwechsel (FW); evtl. eine der Rotationsoperationen R, LR, L, RL; if (key < q.key) q = q.left; else if (key > q.key) q = q.right; else // Schlüssel bereits vorhanden; mache nichts; } if (key < q.key) q.left = new RedNode(key); else q.right = new RedNode(key); evtl. eine der Rotationsoperationen R, LR, L, RL; ! da der neu eingefügte Knoten rot ist, ist die Einhaltung der Regel (4) gewährleistet(Anzahl der schwarzen Knoten sind auf jedem Pfad identisch). ! ein Verstoss gegen Regel (3) (keine zwei rote Knoten übereinander) wird durch Farbwechsel und Rotationsoperationen prophylaktisch vermieden. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-142 Einfügen eines Schlüssels in einen Rot-Schwarz-Baum (2) ! Wende auf Knoten q, der zwei rote Kinder hat, einen Farbwechsel FW durch. Falls q die Wurzel ist, dann bleibt q schwarz. q c1 q FW c2 c1 c2 ! Falls durch den Farbwechsels (FW) oder durch Einfügen eines neuen roten Knotens zwei hintereinanderfolgende roten Knoten entstehen, dann wird eine der Rotationsoperationen R, LR, L oder RL durchgeführt. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-143 Rotationsoperationen (1) p g p s q C R g q s A B B C A ! Rotation wird um Knoten p nach rechts durchgeführt. p und g werden dabei umgefärbt. ! Der Knoten s (Geschwister von p) muss schwarz sein. Begründung in [Weiss 2007] auf Seite 501. Daher keine weitere Verletzung der Farbregel. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-144 Rotationsoperationen (2) g p q s LR p C q A s A B1 g B1 B2 C B2 ! Rotation wird um Knoten p nach links und dann um q nach rechts durchgeführt. q und g werden dabei umgefärbt. ! Die anderen beiden Rotationsoperationen L und RL werden für den Fall, dass p rechtes Kind von g ist, entprechend symmetrisch formuliert. ! Der Knoten s (Geschwister von p) muss schwarz sein. Begründung in [Weiss 2007] auf Seite 501. Daher keine weitere Verletzung der Farbregel. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-145 Beispiel (1) ! Einfügen von 45. ! Der Baum wird bei der Wurzel 30 beginnend top down durchlaufen: 30, 70, 60, 50. ! Bei 50 wird ein Farbwechsel FW durchgeführt. 30 30 70 15 10 60 20 5 50 40 Prof. Dr. O. Bittel, HTWG Konstanz 55 10 85 65 70 15 80 90 60 20 5 50 40 85 65 80 90 55 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-146 Beispiel (2) ! Durch den Farbwechsel wird eine Rechtsrotation R notwendig 30 30 70 15 10 60 20 5 50 40 Prof. Dr. O. Bittel, HTWG Konstanz 10 85 65 60 15 80 90 5 50 20 40 70 55 65 85 80 55 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 90 1-147 Beispiel (3) ! Der Durchlauf kann bei Knoten 50 fortgesetzt werden. Dadurch wird als nächstes Knoten 40 besucht. ! Der neue Knoten 45 kann rechts von 40 als roter Knoten eingefügt werden. ! Fertig! 30 30 60 15 10 5 50 20 40 65 5 85 80 Prof. Dr. O. Bittel, HTWG Konstanz 10 70 55 60 15 90 50 20 40 70 55 45 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 65 85 80 SS 2017 90 1-148 Löschen eines Schlüssels in einem Rot-Schwarz-Baum ! Grundsätzliche Idee: Gelöscht wird immer ein rotes Blatt. ! Zunächst wird der zu löschende Knoten durch iterativen top-down-Durchlauf gesucht. ! Hat der zu löschende Knoten zwei Kinder, dann wird er durch den kleinsten Knoten min im rechten Teilbaum ersetzt. min kann nur ein Kind haben und wird als nächstes gelöscht. ! Hat der zu löschende Knoten genau ein rechtes Kind, dann wird er durch den kleinsten Knoten min im rechten Teilbaum ersetzt. min kann höchstens ein Kind haben und wird als nächstes gelöscht. ! Hat der zu löschende Knoten genau ein linkes Kind, dann wird es durch den größten Knoten max im linken Teilbaum ersetzt. max kann höchstens ein Kind haben und wird als nächstes gelöscht. ! Das Verfahren terminiert damit, dass ein Blatt gelöscht werden muss. Damit die Färbungsregeln erfüllt sind, muss gewährleistet sein, dass das zu löschende Blatt immer rot ist. ! Dies wird durch Farbwechsel und Rotationsoperationen erreicht, die beim iterativen top-Down-Durchlauf (ähnlich wie bei beim Einfügen) durchgeführt werden. ! Näheres siehe in [Weiss 2007] auf Seite 506 und 507. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-149 Teil I: Schlüsselbasiertes Suchen ! ! ! ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume (Tries) kd-Bäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-150 Tries (1) Problem mit den bisherigen Suchbäumen ! Beim Suchen wird bei jedem durchlaufenen Knoten immer der komplette Schlüssel mit dem im Knoten abgespeicherten Schlüssel verglichen. ! Bei langen Schlüsseln kann das sehr aufwendig werden. weiss Suche nach „wenn“: wer was ist wenn 3 Vergleiche mit komplettem Schlüssel wie Tries (aus retrieval; wird wie „try“ ausgesprochen) ! Die Daten sind bei den Blättern abgespeichert. ! Der Schlüssel wird ziffern- bzw. zeichenweise betrachtet (daher auch der Name digitale Suchbäume) und entschieden, in welchem Teilbaum rekursiv weitergesucht wird. ! Erst beim Blatt wird der komplette Schlüssel verglichen. i w ist was weiss a e i i r n wenn Prof. Dr. O. Bittel, HTWG Konstanz Suche nach „wenn“: wie 3 Zeichen-Vergleiche und 1 Vergleich mit komplettem Schlüssel wer Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-151 Tries (2) Präfixproblematik: ! Kein Schlüssel darf Präfix (Anfangsstück) eines anderen Schlüssels sein. ! Dies kann künstlich erreicht werden, indem jeder Schlüssel mit einem Spezialzeichen – z.B. '$' – abgeschlossen wird. i w ist$ was$ weiss$ a e i i r n $ wen$ Prof. Dr. O. Bittel, HTWG Konstanz wie$ n wer$ wenn$ Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-152 Tries (3) Einfügen: ! Beim Einfügen eines neuen Schlüssels müssen evtl. mehrere neue Knoten eingefügt werden. i i w wenn einfügen wenden$ ist$ w ist e n d n wenn$ wenden$ Kontrahierte Tries ! Eliminiere aufeinander folgende Knoten, die nur ein Kind haben. Für eine Verzweigung ist dann im allgemeinen nicht nur 1 Zeichen, sondern ein ganzer String relevant. i w ist e ist wenden$ Prof. Dr. O. Bittel, HTWG Konstanz d wenden$ n d wen i n wenn$ n wenn$ Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-153 Anwendung: Indexierungsverfahren ! Ziel: Finde alle Vorkommen eines Musters (pattern) p in einem (typischerweiser längerem) Text t. ! Dabei ist der Text t statisch (ändert sich nicht), so dass eine einmalige Vorverarbeitung (Indexierung) des Textes in Betracht kommt. ! Die eigentliche Mustersuche geschieht im Index und ist nur noch von der Länge des Musters abhängig. Text t Vorverarbeitung (Indexierung) Muster p Index alle Vorkommen von p in t ! Ein wichtiger Ansatz für Indexierungsverfahren sind Suffixbäume Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-154 Suffixbaum ! Ein Suffixbaum für ein Text t ist ein kontrahierter Trie, der alle Suffixe des Textes t enthält. ! Um die Präfixproblematik zu vermeiden (kein Suffix darf Präfix eines anderen Suffixes sein), wird der Text mit einem Sonderzeichen '$' abgeschlossen, das sonst nicht im Text vorkommt. ! Jedes Blatt im Suffixbaum stellt genau ein Suffix des Textes dar. Im Blatt wird die Position des Suffix im Text abgespeichert. ! Beispiel: Text t: abababcab$ Suffixe von t: abababcab$ bababcab$ ababcab$ babcab$ abcab bcab cab$ ab$ b$ Prof. Dr. O. Bittel, HTWG Konstanz Suffixbaum des Textes t: ab $ ab abcab$ 0 $ cab$ 7 cab$ 4 8 abcab$ 2 cab$ b 1 ab cab$ 6 5 cab$ 3 a b a b a b c a b $ Text t 0 1 2 3 4 5 6 7 8 Position Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-155 Suche im Suffixbaum ! Um die Positionen eines Musters p in einem Text t zu ermitteln, wird das Muster p im Suffixbaum von t gesucht. ! Im allgemeinen endet die Suche bei einem inneren Knoten k. ! Alle Blätter des Teilbaums mit Wurzel k geben die gesuchten Positionen an. ! Beispiel: Suffixbaum des Textes t = abababcab$ Suche nach Muster p = ab $ ab ab abcab$ 4 cab$ 0 $ cab$ 7 8 abcab$ 2 cab$ b ab cab$ 6 5 cab$ 1 3 a b a b a b c a b $ Text t 0 1 2 3 4 5 6 7 8 Position Die gesuchten Positionen sind gelb markiert. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-156 Komplexität ! Der Suffixbaum für ein Text t der Länge n hat genau n Blätter und maximal n-1 innere Knoten. Der String an den Kanten des Suffixbaums sind Teile des Textes und können durch Speicherung der Anfangs- und Endposition im Text dargestellt werden. Die Speicherung des Suffixbaums benötigt daher O(n). ! Die Konstruktion eines Suffixbaums für ein Text t der Länge n kann in T = O(n) durchgeführt werden. (siehe [Ottmann und Widmayer]) ! Die Suche eines Musters p der Länge m in einem Suffixbaum benötigt T = O(m). Sollen alle Vorkommen des Musters p im Text t ermittelt werden, dann ist T = O(m+k) notwendig, wobei k die Anzahl der Vorkommen von p in t ist. ab $ ab abcab$ 0 Prof. Dr. O. Bittel, HTWG Konstanz $ cab$ 7 cab$ 4 8 abcab$ 2 cab$ b 1 ab cab$ 5 cab$ 6 Suffixbaum des Textes t = abababcab$ mit 8 Blättern und 5 inneren Knoten. 3 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-157 Teil I: Schlüsselbasiertes Suchen ! ! ! ! ! ! ! Dictionaries Elementare Suchverfahren Hashverfahren Binäre Suchbäume Ausgeglichene Bäume Digitale Suchbäume kd-Bäume (k-dimensionale Bäume) Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-158 Motivation (1) Ziel ! Verwaltung von Punkten bzw. Daten aus einem k-dimensionalen Raum in einem Suchbaum ! Hier: k = 2 (2-dimensional) Verallgemeinerung auf beliebiges k ist dann offensichtlich. Typische Operationen ! (Orthogonale) Bereichssuche: finde alle Punkte (x,y) mit xmin ≤ x ≤ xmax und ymin ≤ y ≤ ymax ! finde denjenigen Punkt (x,y), der zu einem gegebenen Punkt (x0, y0) am nächsten liegt (nearest neighbour search). y x Prof. Dr. O. Bittel, HTWG Konstanz Bereichssuche: suche alle Punkte im rot umrandeten Bereich Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-159 Motivation (2) Beispiel: 2-dimensionale Bereichssuche bei Personendaten ! Suche alle Personen mit Einkommen zwischen 3000 und 4000 USD und einem Geburtsdatum zwichen 01.01.1950 (= 19500101) und 31.12.1955 (= 19551231) Beispiel aus Berg et al., Computational Geometry, Springer Verlag. Bemerkung ! Bereichssuche lässt sich geometrisch interpretieren. ! Daher sind kd-Bäume in der Literatur auch oft im Bereich der geometrischen Algorithmen (computational geometry) zu finden. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-160 kd-Baum Definition 2d-Baum ! ein 2d–Baum ist ein binärer Baum mit folgenden Eigenschaften: ! jeder Knoten speichert einen 2-dimensionalen Punkt (Daten) ! für jeden Knoten p in einer gradzahligen Tiefe gilt, dass alle Knoten im linken Teilbaum von p eine kleinere (oder gleiche) x-Koordinate und alle Knoten im rechten Teilbaum von p eine größere (oder gleiche) x-Koordinate haben. ! für jeden Knoten p in einer ungradzahligen Tiefe gilt, dass alle Knoten im linken Teilbaum von p eine kleinere (oder gleiche) y-Koordinate und alle Knoten im rechten Teilbaum von p eine größere (oder gleiche) y-Koordinate haben. Beispiel: Tiefe (x,y) 53,14 0 67,51 27,28 30,11 31,85 40,26 29,16 38,23 Prof. Dr. O. Bittel, HTWG Konstanz 7, 39 99,90 70, 3 32,29 15,61 1 2 82,64 3 73,75 Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen 4 SS 2017 1-161 Geometrische Interpretation eines kd-Baums ! Die Wurzel (Tiefe 0 ist gradzahlig) teilt die Ebene vertikal in zwei Teilebenen. ! Die beiden Kinder der Wurzel (Tiefe 1 ist ungradzahlig) teilen die beiden Teilebenen jeweils horizontal ! Die Enkel der Wurzel (Tiefe 2) teilen die Teilebenen wieder vertikal. ! In der nächsten Tiefe wird wieder horizontal geteilt; usw. A C B E A D B E C D Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-162 kd-Baum in Java als Klasse public class KdTree { private class KdNode { double [ ] data; KdNode left; KdNode right; KdNode(double[ ] d, KdNode l, KdNode r) { data = d; left = l; right = r; } } Feld der Größe 2 für 2-dimensionalen Punkt. private KdNode root = null; // ... } Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-163 2-dimensionale Bereichsuche public void printRange(double[ ] min, double[ ] max) { printRangeR(root, 0, min, max); } Gibt alle Punkte (x,y) aus, für die gilt: min[0] <= x <= max[0] und min[1] <= y <= max[1] private void printRangeR(KdNode p, int komp, double[ ] min, double[ ] max) { if (p == null) return; if (min[0] <= p.data[0] && p.data[0] <= max[0] && min[1] <= p.data[1] && p.data[1] <= max[1]) { System.out.println("(" + p.data[0] + "," + p.data[1] + ")"); } komp = 0: gradzahlige Tiefe; es wird if (min[komp] <= p.data[komp]) nach der x-Achse geordnet. printRangeR(p.left, 1-komp, min, max); if (max[komp] >= p.data[komp]) komp = 1: ungradzahlige Tiefe; es printRangeR(p.right, 1-komp, min, max); wird nach der y-Achse geordnet. } E Suche alle Punkte im roten Bereich Prof. Dr. O. Bittel, HTWG Konstanz A A B max C B C min D D Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen E SS 2017 1-164 Konstruktion eines balanzierten 2d-Baums (1) ! Ziel: konstruiere für ein Feld von Punkten einen balanzierten 2d-Baum. ! Teile-und-Herrsche-Verfahren: ! Bestimme bzgl. x (bzw. y) den Median M und ordne Punkte im Feld so um, dass in der linken Hälfte L die Punkte <= M und der rechten Hälfte R die Punkte >= M sind. ! Konstruiere durch rekursiven Aufruf aus L und R jeweils einen kd-Baum BL und BR ! Baue Baum mit M als Wurzel und BL und BR als linken bzw. rechten Teilbaum. M Median M bzgl. x BL L Prof. Dr. O. Bittel, HTWG Konstanz BR R Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-165 Konstruktion eines balanzierten 2d-Baums (2) public KdTree(double[ ][ ] pts) { root = createKdTree(pts, 0, pts.length-1, 0); } Konstruiere aus einem Feld pts von 2-dimensionalen Punkten einen kd-Baum private KdNode createKdTree(double[ ][ ] pts, int li, int re, int komp) { if (li > re) createKdTree konstruiert aus dem Punkteteilfeld return null; pts[li] ... pts[re] einen balanzierten kd-Baum. else if (li == re) Funktion arbeitet nach dem Teil-und-Herrschereturn new KdNode(pts[li], null, null); Prinzip. else { int m = (li+re)/2; quickSelect(pts, li, re, m, komp); quickSelect ordnet das Punkteteilfeld pts[li] ... pts[re] um, so dass in pts[m] der Median return steht und links von m alle Elemente <= pts[m] new KdNode(pts[m], und rechts von m alle Elemente >= pts[m] sind. createKdTree(pts, li, m-1, 1-komp), createKdTree(pts, m+1, re, 1-komp)); } } Prof. Dr. O. Bittel, HTWG Konstanz Bei komp = 0 wird bzgl. des x-Werts verglichen und bei komp = 1 bzgl. des y-Werts. Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-166 kd-Baum mit n = 100 zufällig gewählten Punkten und Bereichsabfrage Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-167 Analyse Bereichssuche für balanzierten kd-Baum mit n Punkten: ! Tmax(n) = O(√n) Konstruktion eines balanzierten kd-Baums mit n Punkten: ! Tmax(n) = O(n log n) ! Begründung: Teile-und-Herrsche-Vefahren, wobei sich quickSelect in O(n) realisieren lässt (siehe z.B. [Cormen et al.]). ! Alternativ lässt sich quickSelect auch als quickSort-Variante formulieren (einfach zu realisieren; siehe Programmiertechnik II). Jedoch ist dann für quickSelect nur die durchschnittliche Laufzeit O(n). Einfügen und Löschen: ! Einfügen und Löschen lässt sich ähnlich wie bei binären Suchbäumen realisieren. ! Jedoch sind keine effiziente Operationen für Einfügen und Löschen bekannt, die den Baum balanziert lassen. ! Rotationstechnik wie bei AVL-Bäumen lässt sich hier nicht anwenden. ! Daher: nach einer Folge von Einfüge- und Lösch-Operationen, Baum rebalanzieren mit einer Technik wie bei der Baum-Konstruktion. Prof. Dr. O. Bittel, HTWG Konstanz Algorithmen und Datenstrukuren – Schlüsselbasiertes Suchen SS 2017 1-168