190 likes | 332 Vues
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;
E N D
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.
n-1 1 n n - 1 2 • Cavg (n) = i = i=0 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) 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:
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:
1. Falls Liste leer ist, endet die Suche erfolglos. Sonst: Betrachte Element der Listean 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.
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 } Binäre Suche (2)
Kosten Cmin ( n ) = 1 Cmax ( n ) = [ log2 (n+1)] Cavg ( n ) log2 (n+1) -1, für große n
Ä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: Fibonacci-Suche 1 i n F k-2 -1 F k-1 -1 F k - 1
- 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) .
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. Sprungsuche . . . . . . . . . . . . . . . L 1 m 2m 3m n
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
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 )
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 Exponentielle Suche
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 )
Schnellere Lokalisierung des Suchbereichs indem Schlüsselwerte selbst betrachtet werden, um „Abstand“ zum Schlüssel K abzuschätzen nächste Suchpositionpos wird aus den Werten ug und og der Unter- und Obergrenze des aktuellen Suchbereichs wie folgt berechnet: Interpolationssuche K - Schlüsselwert[ug] pos = ug + * (og - ug) Schlüsselwert[og]- Schlüsselwert[ug]
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 ) )
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): TypKomponentenname; 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;
Rekursive Definition eines Strukturtyps Rekursive Definition nur über Zeiger möglich. struct List { double A[10]; int length; struct List *liste; } l1,l2; liste ist damit einZeiger auf eine Struktur vom Typ List.
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;