Grundzüge der Informatik 1 Teil 6: Algorithmen und Datenstrukturen 6.3 Suchalgorithmen Prof. Dr. Max Mühlhäuser FG Telekooperation TU-Darmstadt Agenda Grundlegendes zu Suchalgorithmen • Kennenlernen der wichtigsten Suchalgorithmen – Grundidee – Implementierung für int-Felder – Komplexität (meist ohne Analyse) • Experimentieren mit den Algorithmen – In der Vorlesung: Beispiele und Animationen – In der Übung: praktische Auseinandersetzung – Zum Selbststudium: eigene Erstellung von Animationen • Anpassung für die Suche nach Objekten 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 2 Ausgangslage der Suchalgorithmen • Eine wichtige Kasse von Algorithmen beschäftigt sich mit der Suche nach Elementen • Bei den Elementen kann es sich um Zahlen, Zeichen, Zeichenfolgen oder Objekte handeln • Die Elemente sind in einer Datenstruktur abgelegt • Es kann auch nach Elementen gesucht werden, die eine bestimmte Bedingung erfüllen – z.B. soll eine boole'sche Funktion true liefert 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 3 Mögliche Fälle bei der Suche • Bei der Suche können die folgenden Fälle eintreten: 1. Das Element ist nicht in der Datenstruktur enthalten • Es ist darauf zu achten, dass keine Zugriffe außerhalb der Datenstruktur erfolgen, die zu Fehlern führen können 2. Das Element ist genau einmal enthalten • In diesem Fall soll das Ergebnis - je nach Anwendung - true, der Wert des Elements oder seine Position / Adresse sein 3. Das Element ist mehr als einmal enthalten • In einigen Datenstrukturen ist dies nicht zulässig • In den anderen muss definiert werden, welches Element das Ergebnis sein soll 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 4 Sequentielle Suche • Die einfachste und allgemeinste Methode der Suche von Elementen ist die sequentielle Suche • Die Elemente werden dabei der Reihe nach überprüft • Aufgrund des überaus einfachen Vorgehens wird sie auch als brute force search ("Suche mit brutaler Gewalt") bezeichnet • Die sequentielle Suche ist einfach zu implementieren: – Durch eine Schleife auf Feldern oder Listen – Durch Tiefensuche auf Bäumen oder Graphen • Die Komplexität ist mit O(n) sehr ungünstig • Dennoch ist das Verfahren bei unsortierten Daten notwendig – hier rechnet man mit einem durchschnittlichen Aufwand von 2003 © MM ... GdI 1 6.3 – Suchalgorithmen n 2 5 Algorithmus der Sequentiellen Suche /** * Sequentielle Suche nach Element x im Feld a, Komplexitaet O(n). * * @return Index des Elements oder -1 (Feld null/leer/x nicht enthalten) */ public int bruteForceSearch(int[] a, int x) { int i = 0; if (a == null || a.length == 0) return -1; // Feld null oder leer while (i<a.length && a[i] != x) i++; // Laufe durch Feld bis Ende des Feldes oder a[i] == x if (i<a.length) return i; else return -1; // gefunden an Position i // nicht enthalten... } 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 6 Erweiterung der Sequentiellen Suche • Eine Alternative zum Schleifenabbruch wie oben ist die Sentinel-Methode • Hierbei wird das gesuchte Element an die letzte Position eingefügt und anschließend gesucht • Spätestens an der letzten Position wird dann das Element gefunden • Wird bei der Suche die letzte Position erreicht, so war die Suche erfolglos, andernfalls hatte sie Erfolg • Man spart dabei eine Abfrage pro Schleifendurchlauf • Ggf. muss allerdings vorher das ganze Feld in ein größeres Feld kopiert werden (mit O(n)) 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 7 Sequentielle Suche auf sortierten Daten • Was passiert bei (bekanntermaßen) sortierten Daten? – Die sequentielle Suche kann abgebrochen werden, sobald das aktuelle Element größer als das gesuchte Element ist • Diese Möglichkeit setzt voraus, dass die Elemente sich nach einem gegebenen Kriterium (bzw. einer gegebenen Funktion) in eine Ordnung bringen lassen • Bei Objekten ist dies im allgemeinen nicht möglich • Die Komplexitätsabschätzung der sequentiellen Suche bleibt aber auch bei sortierten Daten bei O(n) • Für die nachfolgenden Suchverfahren wird immer unterstellt, dass die Daten sortiert vorliegen 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 8 Binäre Suche • Die binäre Suche ist ein sehr einfaches, gleichzeitig aber auch sehr effizientes Verfahren, das in der Praxis oft eingesetzt wird • Die binäre Suche vergleicht in jedem Schritt das gesuchte Element mit dem mittleren Element des Suchraums • Bei Gleichheit bricht die Suche mit Erfolg ab • Da die Daten sortiert sind, wird sonst nur noch eine Hälfte betrachtet • Das Verfahren teilt in jedem Schritt den Suchraum in zwei Hälften • Es wird nur eine der beiden Hälften betrachtet – – bis das Element gefunden wurde oder der Suchraum sich nicht mehr halbieren lässt • In diesem Fall wurde das Element nicht gefunden • Die Komplexität der binären Suche ist O(log2 n) – Sowohl für erfolglose wie auch für erfolgreiche Suche 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 9 Binäre Suche: Schematischer Algorithmus 1. Betrachte zunächst das gesamte Feld 2. Bestimme die mittlere Position des Teilfeldes 3. Ist das Element in der Mitte gleich dem gesuchten? • Falls ja, weiter bei Schritt 6 4. Ist das mittlere Element kleiner als das gesuchte? – – Falls ja, betrachte die rechte Hälfte als neues Teilfeld Andernfalls betrachte die linken Hälfte als neues Teilfeld 5. Gehe zu Schritt 2 mit dem neuen Teilfeld 6. Gib den entsprechenden Wert zurück • -1 falls nicht gefunden • Ansonsten die (erste gefundene) Position des Elements 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 10 Beispiel zur Binären Suche 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 11 Implementierung der Binären Suche /** * Binaere Suche nach Element x im Feld a, Komplexitaet: O(log n). * @return Index des Elements oder -1 (Feld null/leer/x nicht enth.) */ public int binSearchIterativ(int[] a, int x) { if (a == null || a.length == 0) return -1; // Feld null oder leer int l = 0, r = a.length, midElem = (l+r)/2; while (r > l && a[midElem] != x) { if (x < a[midElem]) r = midElem - 1; else l = midElem + 1; midElem = (l + r) / 2; } if (a[midElem] == x) return midElem; else return -1; // Grenzen und mittleres Element // Suchen solange Elemente da und nicht gefunden // zu klein --> links weitersuchen // zu gross --> rechts weitersuchen // Berechnen der Mitte // gefunden bei midElem // nicht gefunden } 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 12 Rekursive Implementierung Binärer Suche /** * Binaere Suche nach Element x im Feld a, Komplexitaet: O(log n). * Aufruf: int position = binSearch(array, searchFor, 0, array.length); * * @return Index des Elements oder -1 (Feld null/leer/x nicht enth.) */ public int binSearch(int[] a, int x, int l, int r) { int midElem = (l+r)/2; // Mittlere Position bestimmen if (a == null || a.length == 0 || r < l) return -1; // Feld null oder Bereich leer? if (a[midElem] == x) return midElem; // Element gefunden – Ende! if (a[midElem] > x) return binSearch(a, x, l, midElem - 1); // Mitte zu gross - suche links weiter else return binSearch(a, x, midElem + 1, r); // Mitte zu klein - suche rechts } 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 13 Interpolationssuche • Wie kann man die binäre Suche so verbessern, dass statt der Mitte eine „bessere“ Position betrachtet wird? – Der Teilungsindex sollte abhängen… • vom gesuchten Wert • und den Werten an den Intervallgrenzen • Die Interpolationssuche entspricht konzeptuell der Suche im Telefonbuch – – Aufschlagen einer „möglichen“ Stelle auf und schauen, wo man sich befindet Je nachdem, wie weit der aktuelle Eintrag von dem gewünschten ist, wird man nun entsprechend weit vor- oder zurückblättern. • Die Interpolationssuche arbeitet am besten bei etwa gleichverteilten Werten • Bei der binären Suche wurde der Suchindex wie folgt berechnet: – midElem = l + (r - l) / 2 • Die Interpolationssuche bestimmt den neuen Suchindex wie folgt: – midElem = l + ((x - a[l] * (r - l)) / (a[r] - a[l]) • Der Rest des Algorithmus kann unverändert übernommen werden – Daher verzichten wir auf eine erneute Angabe 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 14 Beispiel zur Interpolationssuche 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 15 Anmerkungen • Die Interpolationssuche benötigt bei zufälliger Verteilung log2(log2(n+1)) Vergleiche – der Beweis ist schwierig zu führen und nachzuvollziehen und wird daher hier weggelassen • Die Funktion wächst extrem langsam mit größer werdendem n • Beispiel: log2(log2(109)) ˜ log2(29.897353) ˜ 4.902 • Für (sehr) große, sortierte Felder mit etwa gleichverteilten Werten ist die Interpolationssuche daher sehr gut geeignet • Gegenüber der binären Suche ist lediglich die Berechnung des neuen mittleren Elements aufwändiger • Aufgrund der Integer-Arithmetik ist dabei darauf zu achten, dass die Multiplikation vor der Division durchgeführt wird 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 16 Selbstanordnende Listen • Annahme: Einige Elemente werden viel öfter gesucht als andere • Es kann sich lohnen, die am häufigsten gesuchten Elemente weit vorne in einer Liste anzuordnen • Als Suchstrategie wird dann sequentielle Suche verwendet • Hierzu gibt es drei Vorgehensweisen: – MF-Regel (Move to Front) • Das zuletzt gesuchte Element wird zum Kopf der Liste. Der Rest der Liste bleibt unverändert – T-Regel (Transpose) • Vertausche das gesuchte Element mit dem unmittelbar vorhergehenden – FC-Regel (Frequency Count) • Führe für jedes Element einen Zähler der Zugriffe. Jeder Zugriff erhöht den Elementzähler um 1 • Ordne nach dem Zugriff auf ein Element dieses so ein, dass die Zählerwerte absteigend sortiert in der Liste auftreten • Falls die Zugriffe etwa gleichverteilt sind, ist dieses Vorgehen durch die Reorganisation noch ineffizienter als einfache sequentielle Suche! 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 17 Suchen im binären Suchbaum • Die Elemente eines binären Suchbaums sind immer geordnet: – – • alle Elemente des linken Teilbaums eines Knotens sind kleiner als der Wert des Knotens alle Elemente des rechten Teilbaums hingegen sind größer als der Wert des Knotens Daher kann man im binären Suchbaum wie folgt suchen: 1. Betrachte die Wurzel des gesamten Baums als aktuellen Knoten 2. Vergleiche den aktuellen Knoten mit dem gesuchten Element: 1. Falls die Elemente gleich sind, ist die Suche beendet. 2. Falls das gesuchte Element kleiner als der aktuelle Knoten ist, mache den Wurzelknoten des linken Teilbaums zum aktuellen Knoten, gehe zu Schritt 2.4 3. Falls das gesuchte Element größer als der aktuelle Knoten ist, mache den Wurzelknoten des rechten Teilbaums zum aktuellen Knoten, weiter bei 2.4 4. Falls der aktuelle Knoten nicht null ist, gehe zu Schritt 2; sonst Ende • Falls der aktuelle Knoten nach Schritt 2.2 oder 2.3 null ist, also kein entsprechender Teilbaum existiert, ist die Suche erfolglos beendet 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 18 Algorithmus: Suche im binären Suchbaum /** * Suche Wert in einem Binaeren Suchbaum. * @return Der Knoten, der das Element enthaelt oder null, falls * das Element nicht enthalten ist. */ public BinaryTree treeSearch(int x) { if (value == x) return this; // falls das der richtige Knoten ist, zurueckgeben! if (x < value && left != null) return left.treeSearch(x); // links weitersuchen, falls Wert kleiner und linker Teilbaum da if (value < x && right != null) return right.treeSearch(x); // rechts weitersuchen, falls Wert kleiner und rechter Teilbaum da return null; // wenn wir hier anlangen, ist der Wert nicht im Baum enthalten! } 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 19 Einfügen im binären Suchbaum • Der Algorithmus zum Suchen im binären Suchbaum kann so modifiziert werden, dass er auch zum (sortierten) Einfügen von neuen Elementen benutzt werden kann. • Dazu wird zunächst nach dem Element gesucht. – Ist der zu verfolgende Teilbaum null, wird das Element als neuer Knoten eingefügt – Andernfalls wird im Teilbaum weitergesucht • Hierzu sind nur die Abfragen auf < und > anzupassen • Zusätzlich wird Rückgabetyp BinaryTree durch void ersetzt. • Wichtig: wurde der neu einzufügende Knoten gefunden, wird er nicht erneut eingefügt, da in einem binären Suchbaum jeder Schlüssel nur einmal vorkommen darf. 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 20 Beispiel zum binären Suchbaum 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 21 Anmerkungen zum Anpassen an Objekte • Die vorgestellten Algorithmen dienen zur Suche nach intWerten • Für Objekte sind die Algorithmen wie folgt anzupassen: – Anpassen des Typs der Variablen und des Feldes – Ersetzen von == durch equals der Klasse Object oder andere Methoden • Bei Verwendung eines String-Attributs „name“ : – (person.getName()).equals(otherPerson.getName()) • Implementieren der Methoden lessThan, lessOrEqual und equals – Manchmal kann auch anhand des Suchattributs verglichen werden – Etwa die Methode compareTo der Klasse String liefert int-Werte 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 22 Zusammenfassung • Sequentielle Suche ist für unsortierte Daten geeignet • Für sortierte Daten ist die Binäre Suche gut geeignet – da einfach zu implementieren und effizient • Interpolationssuche ist noch besser als Binäre Suche… – … falls die Daten sortiert und ungefähr gleichverteilt sind • Für Listen o.ä. ist oft nur Sequentielle Suche nutzbar • Alle Algorithmen sind leicht an andere Typen – double statt int oder Objekte – anpassbar 2003 © MM ... GdI 1 6.3 – Suchalgorithmen 23