Kapitel 6. Suchverfahren Wir betrachten nun Listen, in denen die einzelnen Elemente (Knoten) sowohl einen Inhalt als auch einen Schlüssel besitzen. In C kann jetzt der Strukturtyp Knoten z. B. in folgender Weise definiert sein: typedef struct Knoten { int key; char *Inhlt; /*Inhlt ist Zeiger auf eine Zeichenkette. */ struct Knoten *next; }ListElmt; Definieren wir nun eine Variable des Typs ListElmt ListElmt *elmnt; Dann beinhaltet elmnt->key den Schlüsselwert (hier eine integer Größe). Für ein Wörterbuch würde man char key; statt int key; vereinbaren, so daß elmnt->key (Schlüsselwert) dann ein beliebige Zeichenkette sein kann. R. Der 1 Algorithmen und Datenstrukturen (Magister) Sequentielle Suche Suche nach Element mit Schlüsselwert K Falls nicht bekannt, ob die Elemente der Liste nach ihren Schlüsselwerten sortiert sind, besteht nur die Möglichkeit, die Liste sequentiell zu durchlaufen und elementeweise zu überprüfen (sequentielle Suche) Kosten: •Erfolglose Suche erfordert n Schleifendurchläufe • erfolgreiche Suche verlangt im ungünstigsten Fall n -1 Schleifendurchläufe ( und n Schlüsselvergleiche) • mittlere Anzahl von Schleifendurchläufen bei erfolgreicher Suche: 1 n-1 n-1 Cavg (n) = i = n i=0 2 R. Der 2 Algorithmen und Datenstrukturen (Magister) Binäre Suche Auf sortierten Listen können Suchvorgänge effizienter durchgeführt werden Sequentielle Suche auf sortierten Listen bringt nur geringe Verbesserungen (für erfolglose Suche durchschnittlich N/2 Vergleiche). Binärsuche wesentlich effizienter durch den Einsatz der Divide-and-conquer-Strategie. Suche nach Schlüssel K in Liste mit aufsteigend sortierten Schlüsseln: R. Der 3 Algorithmen und Datenstrukturen (Magister) 1. Falls Liste leer ist, endet die Suche erfolglos. Sonst: Betrachte Element der Liste an mittlerer Position m. 2. Falls K = Schlüsselwert dieses Elementes, dann ist das gesuchte Element gefunden . 3. Falls K < Schlüsselwert, dann durchsuche die linke Teilliste von Position 1 bis m-1 nach demselben Verfahren. 4. Sonst (K > Schlüsselwert) durchsuche die rechte Teilliste von Position m + 1 bis Listenende nach demselben Verfahren. R. Der 4 Algorithmen und Datenstrukturen (Magister) Binäre Suche (2) Iterative Lösung int binsearch(int v) { int l=1; int r= N; int x; while (r>=1) { x = (l+r)/2; if (v < a[x].key) r = x-1; else l = x+1; if (v == a[x].key) return a[x].data; } return -1 } R. Der 5 Algorithmen und Datenstrukturen (Magister) Kosten Cmin ( n ) = 1 Cmax ( n ) = [ log2 (n+1)] Cavg ( n ) log2 (n+1) -1, für große n R. Der 6 Algorithmen und Datenstrukturen (Magister) Fibonacci-Suche Ähnlich der Binärsuche, jedoch wird Suchbereich entsprechend der Folge der Fibonacci-Zahlen geteilt. Definition der Fibonacci-Zahlen F0 = 0 F1 = 1 Fk = F k-1 + F k-2 für k >= 2. Teilung einer Liste mit n = Fk-1 sortierten Elementen: 1 i n F k-2 -1 F k-1 -1 F k- 1 R. Der 7 Algorithmen und Datenstrukturen (Magister) - Element an der Position i = F k-2 wird mit dem Schlüssel K verglichen - Wird Gleichheit festgestellt, endet die Suche erfolgreich. - Ist K größer, wird der rechte Bereich mit F k-1 -1 Elementen, ansonsten der linke Bereich mit F k-2 -1 Elementen auf dieselbe Weise durchsucht. Kosten - für n = F k -1 sind im schlechtesten Fall k-2 Suchschritte notwendig, d. h. O ( k ) Schlüsselvergleiche - Da gilt F k c + 1.618 k , folgt C max ( n ) = O ( log 1.618 ( n+1 ) ) = O ( log2 n) . R. Der 8 Algorithmen und Datenstrukturen (Magister) Sprungsuche Prinzip - Zunächst wird der sortierte Datenbestand in Sprüngen überquert, um den Abschnitt zu lokalisieren, der ggf. den gesuchten Schlüssel enthält, - danach wird der Schlüssel im gefundenen Abschnitt nach irgendeinem Verfahren gesucht. ... ... L 1 R. Der ... m ... 2m ... 3m 9 n Algorithmen und Datenstrukturen (Magister) Einfache Sprungsuche - konstante Sprünge zu Positionen m, 2 m, 3 m, ... - Sobald K <= Schlüsselwert[i] mit i = j * m (j = 1, 2, ...), wobei a[i].key der Wert des inspizierten Schlüssels ist wird im Abschnitt von (j-1)m+1 bis j*m sequentiell nach dem Suchschlüssel K gesucht. Mittlere Suchkosten ein Sprung koste a ; ein sequentieller Vergleich b Einheiten Cavg (n) = (a*n/m + b*(m-1))/2 R. Der 10 Algorithmen und Datenstrukturen (Magister) Optimale Sprungweite m=V(a/b)n bzw. m = V n falls a = b C avg ( n ) = a V n - a / 2 Komplexität O (V n ) R. Der 11 Algorithmen und Datenstrukturen (Magister) Exponentielle Suche Anwendung wenn Länge des sortierten Suchbereichs zunächst unbekannt bzw. sehr groß ist. Vorgehensweise - für Suchschlüssel K wird zunächst obere Grenze für den zu durchsuchenden Abschnitt bestimmt i = 1; while (K > Schlüsselwert[i]) i *= 2; - Für i > 1 gilt für den auf diese Weise bestimmten Suchabschnitt Schlüsselwert[i DIV 2] < K <= Schlüsselwert[i] - Suche innerhalb des Abschnitts mit irgendeinem Verfahren R. Der 12 Algorithmen und Datenstrukturen (Magister) Sind in der sortierten Liste nur positive, ganzzahlige Schlüssel ohne Duplikate gespeichert, wachsen Schlüsselwerte mindestens so stark wie die Indizes der Elemente. - i wird höchstens log2 K mal verdoppelt - Bestimmung des gesuchten Intervalls erfordert maximal log2 K Schlüsselvergleiche - Suche innerhalb des Abschnitts (z. B. mit Binärsuche ) erfordert auch höchstens log2 K Schlüsselvergleiche Gesamtaufwand O ( log2 K ) R. Der 13 Algorithmen und Datenstrukturen (Magister) Interpolationssuche Schnellere Lokalisierung des Suchbereichs indem Schlüsselwerte selbst betrachtet werden, um „Abstand“ zum Schlüssel K abzuschätzen nächste Suchposition pos wird aus den Werten ug und og der Unter- und Obergrenze des aktuellen Suchbereichs wie folgt berechnet: K - Schlüsselwert[ug] pos = ug + R. Der * (og - ug) Schlüsselwert[og]- Schlüsselwert[ug] 14 Algorithmen und Datenstrukturen (Magister) Sinnvoll, wenn der Schlüsselwert im betreffenden Bereich einigermaßen gleichverteilt ist erfordert dann im Mittel lediglich log2 log2n + 1 Schlüsselvergleiche Im schlechtesten Fall (stark ungleichmäßige Werteverteilung) entsteht jedoch linearer Suchaufwand ( O ( n ) ) R. Der 15 Algorithmen und Datenstrukturen (Magister) Anmerkungen zur Implementierung in C Strukturierte Typen in C Struktur ist eine Ansammlung von mehreren Variablen unter einem gemeinsamen Namen. Syntax für einen Strukturtyp: struct Bezeichner {Komponenten}. Komponenten (im einfachsten Falle): Typ Komponentenname; Danach können gleich noch Variable des eigeführten Typ vereinbart werden. Beispiel: Implementierung einer Liste durch ein Array: struct List { double A[10]; int length; } l1,l2; l1 und l2 sind jetzt Variable des vereinbarten Typs. Ansprache über Variablenname.Komponentenname. Beispiele: l1.A[3]=3.2; l1.length = 10; R. Der 16 Algorithmen und Datenstrukturen (Magister) Rekursive Definition eines Strukturtyps Rekursive Definition nur über Zeiger möglich. struct List { double int struct List } l1,l2; A[10]; length; *liste; liste ist damit einZeiger auf eine Struktur vom Typ List. R. Der 17 Algorithmen und Datenstrukturen (Magister) Verwendung von typedef Mit typedef kann ein neuer Name nach Bedarf für einen Datentyp eingeführt werden. Syntax: typedef typ declarator Im einfachsten Falle ist der declarator einfach ein Bezeichner: typedef typ bezeichner Beispiel für letzteren Fall: typedef struct List { double A[10]; int length; } Meine_Liste; Dabei ist struct List {double A[10]; int length;} der typ und Meine_Liste ist jetzt der bezeichner für den durch die struct Deklaration eingeführten Datentyp List. Man kann dann Variablen dieses Typs als Meine_Liste L1; deklarieren. Speicher anfordern (Objekt erzeugen) mit L1 = (Meine_Liste *)malloc (sizeof L1); Nun geht z. B. L1->length = 10; R. Der 18 Algorithmen und Datenstrukturen (Magister)