1661 Definitionen Algorithmus: Ein Algorithmus ist ein Verfahren zur Lösung eines Problems. Ein Problem besteht jeweils in der Zuordnung eines Ergebnisses zu jedem Element aus einer Klasse von Probleminstanzen; insofern realisiert ein Algorithmus eine Funktion. Die Beschreibung eines Algorithmus besteht aus einzelnen Schritten und Vorschriften, die die Ausführung dieser Schritte kontrollieren. Jeder Schritt muß klar und eindeutig beschrieben sein und mit endlichem Aufwand in endlicher Zeit ausführbar sein. man definiert man einen Algorithmus so abstrakt wie möglich und sollte so wenig implizite Annahmen über die verwendeten Objekte machen, wie möglich. Datentyp: Datentypen entsprechen den Algorithmen. Allerdings beschreiben sie die Objekte mit denen ein Algorthmus arbeitet. Datentypen entsprechen den Objekten in der objektorientierten Programmierung. Zu den Objekten gehören immer Operationen, die auf ihnen arbeiten. Die in einer Algebra verwendeten Mengen und Operationen sind so zu verwenden, wie sie in der Mathematik definiert werden. Ein Datentyp macht keine Angaben über seine Implementierung. Datenstruktur: Implementierung eines Datentyps auf algorithmischer Ebene Graph: Ein Graph stellt eine Menge von Objekten zusammen mit einer Beziehung (Relation) auf diesen Objekten dar. Objekte werden als Knoten, Beziehungen als Kanten dargestellt. Eine Beziehung wird im allgemeinen durch eine gerichtete Kante (einen Pfeil) dargestellt. Der Spezialfall einer symmetrischen Beziehung wird durch eine ungerichtete Kante dargestellt. 1 grundlegendes Schritte zur Abstrahierung der Laufzeit: 1. keine Unterscheidung der Art der Elementaroperationen 2. Aufteilung der Mengen aller Eingaben in Komplexitätsklassen 3. Betrachtung der Spezialfälle, vor allem average case und worst case 4. durch Weglassen von multiplikativen und additiven Konstanten wird nur noch das Wachstum einer Laufzeitfunktion T (n) betrachtet (dies geschieht mit Hilfe der O-Notation) Definition O-Notation: seien f : N → R+ , g : N → R+ f = O(g) :⇔ ∃n0 ∈ N, c ∈ R, c > 0 : ∀n ≥ n0 f (n) ≤ c · g(n) O(g) ist eine so definierte Funktionsmenge: O(g) = {f : N → R+ |∃n0 ∈ N, c ∈ R, c > 0 : ∀n ≥ n0 f (n) ≤ c · g(n)} sprich: f ist ein Element von O(g), genau dann wenn es ein n0 und ein c gibt, für die gilt, daß ab einer Stelle n ≥ n0 gilt, bei der f (n) immer kleiner als c · g(n) ist. O-Notation setzt obere Grenze, Ω-Notation untere Grenze, Θ grenzt einen Bereich genau ein Datentyp (Algebra): ein System, welches aus einer oder mehreren Objektklassen mit dazugehörigen Operationen besteht, man muß festlegen, wie die Objektmengen (sorts) und Operationen (ops) heißen, wieviele und was für Objekte die Operationen als Argumente benötigen und welche Art von Objekt sie als Ergebnis liefern (Signatur, ist syntaktisch, nicht semantisch). Zur Festlegung der Semantik ist jeder Sorte eine Trägermenge (sets) zuzuordnen und jedem Operationssymbol eine Funktion (functions) mit entsprechenden Argument- und Wertebereichen. Bei der Spezifikation als Algebra gibt man Trägermengen und Funktionen direkt an, unter Verwendung der in der Mathematik üblichen Notationen. Spezifikation als abstrakter Datentyp adt: verwendet anstelle von sets und functions Axiome (axs), welche die interessierenden Aspekte der Wirkungsweise der Operationen charakterisieren (Standardbeispiel Stack); ist aber problematisch, da die Anzahl der Gesetze sehr groß werden kann, die Bedeutung des Datentyps intuitiv schwer erkennbar ist und schwierig zu überprüfen ist, ob die Menge der Gesetze vollständig und widerspruchsfrei ist. 2 grundlegende Datentypen Listen: auf Listen ist im Gegensatz zu Mengen eine Ordnung definiert (Reihenfolge mit Vorgänger und Nachfolger), es dürfen Duplikate auftreten Modelle von Listen: list1 : einfache Liste für rekursive Verarbeitung mit erstem Element und Restliste bietet Operation empty, isempty, first, rest, append und concat list2 Liste mit expliziten Positionen erlaubt wahlfreien Zugriff auf Listenelemente; Operationen: empty, isempty, concat, front, last, next previous, bol, eol, insert, delete, find (liefert Position des ersten Elementes, das Bedingung erfüllt), retrieve Implementierungen von Listen: (a) doppelt verkettete Listen unterstützen list2 vollständig: type list = ^listelem; pos = ^listelem; listelem = record value : elem; pred, succ : ^listelem end; (b) einfach verkettete Listen unterstützen kein previous type list = record head, last: ^listelem listelem = record value: elem; succ ^listelem end; end; (c) sequentielle Darstellung im Array hat Nachteile (Einfügen und Entfernen verlangt Verschieben der Folgeelemente, Maximalgröße muß vorher festgelegt werden, Positionszeiger nicht stabil) (d) einfach oder doppelt verkettete Listen im Array: Elemente bestehen aus Wert und Zeiger auf Folgeelement (die Zeiger werden durch Array-Indizes ersetzt), es können mehrere Liste in einem Array dargestellt werden, zusätzlich wird noch eine Liste der freien Felder verwendet; Nachteile wie oben, aber Vorteile: Aufrufe des Laufzeit- und Betriebssystems zum Kreieren neuer Elemente werden vermieden, komplette Struktur kann extern gespeichert werden Stacks: LIFO-Struktur, Spezialfall von Listen mit den Operationen top (=first), pop (=rest), push (=append), wird üblicherweise als sequentielle Darstellung im Array implementiert, eignen sich zur Bearbeitung von Klammerstrukturen (einfach Implementation mit getrennten Operator- und Operanden-Stack) Queues: FIFO-Struktur, entspricht list1 ohne concat und append: front, enqueue, dequeue, queue; wird üblicherweise als sequentielle Darstellung im Array implementiert; da die Queue das Array „durchwandert“, wird das Array als zyklisch aufgefaßt; Anwendung besonders zur Realisierung von Warteschlangen Abbildungen: (mappings) ordnet jedem Element des Argumentbereiches ein Element der Zielwertbereichs zu; bei kleiner Anzahl direkte Implementierung im Array 3 binäre Bäume: besteht aus eine Menge von Objekten (Knoten) mit einer Relation auf der Knotenmenge (graphisch durch Kanten dargestellt), die die Knotenmenge hierarchisch organisiert. Minimale Höhe ist dlog2 (n + 1)e − 1, können mit Zeigern: type tree = ^node; node = record key : elem; left, right : tree end; oder zeigerlos als Array der Länge 2h+1 − 1 implementiert werden: Index (p) = i -> Index (p.left) = 2·i Index (p.right) = 2·i + 1 allgemeine Bäume: Implementierung über Arrays (Voraussetzung, daß alle Knoten höchstens einen festen Grad d haben), oder über Binärbäume, bei denen der Zeiger auf den linken Sohn zeigt und von dort aus jeweils auf dessen rechten Bruder 4 Datentypen zur Darstellung von Mengen Mengen mit Durchschnitt, Vereinigung, Differenz: Bitvektor (Aufwand ist proportional zur Größe des Universums); ungeordnete Liste (sehr ineffizient, da obige Operationen für zwei Listen der Größen n und m die Komplexität O(n ∗ m) haben; geordnete Liste (Aufwand proportional zur Größe der Mengen) paralleler Durchlauf O(n + m) Dictionaries: Mengen mit INSERT, DELETE, MEMBER: Hashing: Abbildung: h : D → {0, . . ., m − 1} Grundidee des Hashverfahrens ist es, aus dem Wert eines Elementes seine Adresse zu berechnen; bei komplexen Records wird ein Schlüssel gewählt; gewünschte Eigenschaften: surjektiv, gleichmäßige Verteilung der Schlüssel in den Behältern, effizient berechenbar; haben (allgemein) sehr schlechtes worst-case-Verhalten (O(n), aber sehr gutes average-case-verhalten (O(1)), sind relativ einfach zu implementieren bei offenen Hashverfahren kann ein Behälter beliebig viele Schlüssel aufnehmen, bei geschlossenen nicht (Kollision, PKollision = 1 − m(m−1)(m−2)...(m−n+1) (m mn Behälterzahl, n - Schlüsselzahl); bei offenem Hashing ist jeder Behälter eine Liste von Schlüsseln, die Behälter werden durch ein Array von Zeigern verwaltet, worst case ist sehr schlecht: O(n), average case ist O(1 + n/m) = O(1) bei geschlossenen Hashing werden Elemente in Array gespeichert (begrenzte Aufnahmekapazität, sollte Auslastung von 80% nicht übersteigen, eignet sich nur, wenn Gesamtanzahl beschränkt und kaum dynamisch ist), zur Kollisionsbehandlung werden weitere Hashfunktionen, die für jeden Wert jede Tabellenzelle inspizieren sollten, benutzt – bei der Suche wird die gleiche Folge von Zellen betrachtet wie beim Eintragen und bei der ersten Leerzelle abgebrochen, d. h. Felder mit gelöschten Elementen müssen markiert werden (und werden nicht wieder freigegeben) Kollisionsstrategien: lineares Sondieren hi (x) = (h(x) + c ∗ i) mod m (c und m teilerfremd, damit alle Zellen getroffen werden), Nachteil: Kettenbildung; quadratisches Sondieren vermeidet Clusterbildung bei Sekundärkolissionen (Suchen nach freien Feldern in immer größeren Abständen abwechselnd vor und nach dem ursprünglichen Feld); Doppel-Hashing (Verwendung von zwei unabhängigen Hashfunktionen (d. h. Wahrscheinlichkeit für Doppelkollision m12 ) Hash-Funktionen: Divisionsmethode h(k) = k mod m, aufeinanderfolgende Werte werden dabei auf aufeinanderfolgende Zellen abgebildet; Mittel-Quadrat-Methode: Es wird eine Ziffernfolge aus der Mitte der quadrierten Zahl genommen, dadurch werden aufeinanderfolgende Werte besser gestreut. Man muß für jede Anwendung überprüfen, ob eine gewählte Hashfunktion die Schlüssel gleichmäßig verteilt. Ein Nachteil von Hash-Verfahren ist, daß die Menge der gespeicherten Schlüssel nicht effizient in sortierter Reihenfolge aufgelistet werden kann. 5 binäre Suchbäume: Sei T ein Baum. Die Knotenmenge von T wird ebenso mit T bezeichnet. Eine Knotenmarkierung ist eine Abbildung µ : T → D für irgendeinen geordneten Wertebereich D. In einem binären Suchbaum ist der Wert Schlüssels des linken Sohnes kleiner und der des rechten Sohnes größer als die jeweilige Wurzel. Algorithmen delete, insert und member folgen jeweils einem einzigen Pfad im Baum von der Wurzel zu einem Blatt oder inneren Knoten, der Suchaufwand ist proportional zur Länge dieses Pfades, d. h. O(log n) im balancierten bzw. O(n) im degenerierten (entsteht durch Einfügen sortierter Schlüssel) Baum. Mittlerer 2 wahrscheinlicher Suchaufwand √ ist 2 ∗ ln 2 ∗ log n = O(log n), nach n Update-Operationen aber Θ( n) (Baum wird allmählich linkslastig, d. h. Löschprozedur sollte zufällig Vorgänger oder Nachfolger des zu löschenden Schlüssels an freie Position setzen Algorithmen: Überprüfen, ob ein Element im Baum vorkommt (member, rekursiv, t ist der Baum, x das gesuchte Element): Wenn t = nil dann false zurückgeben; oder wenn aktueller Eintrag = dem gesuchten Element, dann true zurückgeben; oder wenn aktueller Eintrag größer als das gesuchte Element, dann Rückgabe von member(linkerT eilbaum, W ert); oder wenn aktueller Eintrag kleiner als das gesuchte Element, dann Rückgabe von member(rechterT eilbaum, W ert) Einfügen eines Elementes (insert): zuerst nach x im Baum suchen, wenn es nicht vorhanden ist, dann an der Stelle, an der die Suche erfolglos endete, den neuen Knoten einfügen Löschen (delete, p ist der Zeiger auf das aktuelle Element): nach x im Baum suchen, wenn es gefunden wird und es sich dabei um einen Blatt handelt, dann Blatt löschen; oder wenn es ein Knoten mit einem Sohn ist, Sohn an die Stelle des Elementes setzen (p entfernen und den Zeiger von p auf den Sohn p setzen); oder wenn es sich um einen Knoten mit zwei Söhnen handelt, im Teilbaum vom p den Knoten q bestimmen, der das kleinste Element y > x enthält, im Knoten p Schlüssel x durch y ersetzen; den Schlüssel y aus dem Teilbaum mit Wurzel q entfernen Nachteil: besitzen schlechtes Worst-Case-Verhalten, deswegen: AVL-Bäume: ist ein balancierter Suchbaum, garantiert O(log n) für alle drei Dictionary-Operationen. Im AVL-Baum unterscheidet sich für jeden Knoten die Höhe seiner zwei Teilbäume um höchstens 1. Nach Update wird der Knoten zur Wurzel zurückverfolgt und nötigenfalls rebalanciert. Rebalancieren: von der aktuellen Position aus wird der Pfad zur Wurzel gelaufen, in jedem Knoten wird dessen Balance geprüft (d. h. auch, daß alle Teilbäume unterhalb des gerade betrachteten (aus der Balance geratenen) Knotens selbst balanciert sind, Ordnungseigenschaften bleiben dabei erhalten einfache Rotation (wenn bei einem aus der Balance geratenen Knoten ein äußerer Teilbaum am tiefsten hängt); doppelte Rotation (wenn der mittlere Teilbaum am tiefsten ist) 6 Algorithmen: folgen jeweils einem Pfad von der Wurzel zu einem Blatt und dann evtl. den Pfad vom Blatt zurück und nehmen dabei evtl. (Doppel-)Rotation vor. Jede Operation hat O(1), insgesamt also O(h) (h=Höhe des Baumes) maximale Höhe ist etwa 1, 44 ∗ log n = O(log n), d. h. höchstens 44% höher als ein vollständig ausgeglichener binärer Suchbaum, benötigt O(n) Speicherplatz. Externes Suchen: B-Bäume: extern bedeutet die vollständige Darstellung auf den Hintergrundspeicher, benötigt O(1) internen Speicherplatz (aber natürlich Ω(n) externen); Hauptproblem also: Datensätze mit Schlüsseln aus geordnetem Wertebereich so organisieren, daß ein Datensatz mit gegebenem Schlüssel mit möglichst wenig Seitenzugriffen bearbeitet werden kann; Lösung: Speicherseiten werden als Knoten eines Suchbaums aufgefaßt, da die Suchkosten proportional zur Pfadlänge sind, wählt man Bäume mit hohem Verzweigungsgrad (Vielweg-Suchbaum). Ein B-Baum der Ordnung m ist ein Vielweg-Suchbaum mit speziellen Eigenschaften (Anzahl der Schlüssel pro Knoten liegt zwischen m und 2m, alle Pfadlängen von der Wurzel zu einem Blatt sind gleich, jeder innere Knoten mit s Schlüsseln hat s + 1 Söhne); Höhe = O(logm+1 n); die Struktur ließe sich so festlegen: type BNode = record used: 1..2m; keys: array[1..2m] of keytype; sons: array[0..2m] of BNodeAddress end (used gibt an, wieviele Schlüssel im Knoten gespeichert sind) Es kann innerhalb eines Knotens binär gesucht werden. Die Kosten für eine Suche sind beschränkt durch die Höhe des Baumes. Die Struktur kann bei Änderungsoperationen recht einfach aufrecht erhalten werden. Die Algorithmen für das Einfügen und Entfernen verletzen temporär die Struktur des B-Baumes, indem Knoten mit 2m + 1 Schlüsseln entstehen (diese Verletzung heißt Overflow, der Knoten ist überfüllt) oder Knoten mit m − 1 Schlüsseln (Underflow, der Knoten ist unterfüllt). Es wird dann eine Overflow- oder Underflow-Behandlung eingeleitet, die jeweils die Struktur in Ordnung bringt. Algorithmen: insert (root, x): suche nach x im Baum mit Wurzel root; wenn x nicht gefunden, dann sei p das Blatt, an dem die Suche endete, füge x an der richtigen Position in p ein; wenn p jetzt 2m + 1 Schlüssel hat, dann overflow delete (root, x): suche nach x im Baum mit Wurzel root; wenn x in einem inneren Knoten liegt, dann suche x0 , den Nachfolger von x (den nächstgrößeren gespeicherten Schlüssel) im Baum (x0 liegt in einem Blatt); vertausche x mit x0 ; wenn x in stattdessen in einem Blatt liegt, dann lösche x aus p; wenn nun p nicht die Wurzel ist, dann wenn p hat m − 1 Schlüssel underflow overflow: Ein Overflow eines Knotens p wird mit einer Operation split(p) behandelt, die den Knoten p mit 2m + 1 Schlüsseln am mittleren Schlüssel km+1 teilt, so daß Knoten mit Schlüsselfolgen k1 . . .km und km+2 . . .k2m+1 entstehen, die 7 jeweils m Schlüssel enthalten. km+1 wandert „nach oben“, entweder in den Vaterknoten oder in einen neuen Wurzelknoten. Dadurch kann der Vaterknoten überlaufen. Die Behandlung eines Overflow kann sich also von einem Blatt bis zur Wurzel des Baumes fortpflanzen. underflow: Um einen Underflow in p zu behandeln, werden der oder die Nachbarn von p betrachtet. Wenn einer der Nachbarn genügend Schlüssel hat, wird seine Schlüsselfolge mit der von p ausgeglichen (Operation balance), so daß beide etwa gleichviele Schlüssel haben. Andernfalls wird p mit dem Nachbarn zu einem einzigen Knoten verschmolzen (Operation merge). balance: der Vaterknoten und die benachbarten Söhne werden so umsortiert, daß der mittlere Schlüssel der gesamten Schlüsselfolge mit dem Schlüsselwert, den beide Söhne im Vaterknoten einrahmen, ausgetauscht wird. Die beiden Söhne werden dementsprechend neu aufgeteilt. merge: wenn Vaterknoten nicht die Wurzel ist und m − 1 Schlüssel hat, dann underflow (q); wenn er aber die Wurzel ist, dann werden die verschmolzenen Nachbarn zum neuen Wurzelknoten Priority Queues: Mengen mit INSERT, DELETEMIN: Warteschlangen, deren Elemente gemäß einer Priorität eingeordnet werden, es wird das Element mit der höchsten Priorität entnommen. Wird über Multimengen (Mengen, die Duplikate zulassen) realisiert, Darstellung als Auflistung oder als Paare (Wert, Anzahl). Lassen sich effizient mit modifizierten AVL-Bäumen oder partiell geordneten Bäumen (wird bei Heapsort eingesetzt) realisieren. Partiell geordneter Baum (Heap) bedeutet, daß in der Wurzel jeweils das Minimum (Maximum) eines Teilbaums steht. In einem Heap ist die Folge der Knotenmarkierungen auf einem Pfad monoton steigend. Algorithmen: Folgendes gilt für links-vollständige partiell geordnete Bäume. Das heißt, alle Ebenen bis auf die letzte sind voll besetzt (vollständiger Baum), und auf der letzten Ebene sitzen die Knoten so weit links wie möglich. insert: erzeuge neuen Knoten und füge ihn auf der ersten freien Position der untersten Ebene ein; falls diese besetzt ist, beginne neue Ebene; vergleiche den erzeugten Knoten mit dem Vater und vertausche gegebenfalls, solange, bis der jeweilige Vater nicht mehr größer als der neue knoten ist deletemin: gib den Eintrag der Wurzel als Minimum aus und ersetze ihn durch den Eintrag der letzten besetzten Position im Baum; vergleiche die neue Wurzel mit ihren Söhnen und vertausche sie solange möglich mit dem kleineren Eintrag der beiden 8 Sortieralgorithmen allgemeines: Berechnung einer Folge von geordneten Werten (Permutation der Ausgangsfolge), Unterscheidung nach intern (alle Records werden im Hauptspeicher gehalten) und extern, Unterscheidung nach Methodik (Sortieren durch Einfügen, Auswählen; Divide-and-Conquer; Fachverteilen), Unterscheidung nach Effizienz (einfache Verfahren O(n2 ), gute Verfahren O(n log n)), Unterscheidung nach „im Array oder nicht“ (Ziel durch Vertauschen erreichen, oder z. B. zwei Arrays benutzen), Unterscheidung nach allgemeinem oder eingeschränkten (Records müssen spezielle Eigenschaften haben) Verfahren; in der Analyse werden die wesentlichen Operationen Vergleich zweier Schlüssel und vertauschen zweier Datensätze gezählt; ein Verfahren ist stabil, wenn die Reihenfolge von Schlüsseln gleichen Wertes nicht geändert wird. Jeder Schlüsselvergleichssortieralgorithmus benötigt Ω(n log n) Vergleiche im worst case Sortieren durch direktes Auswählen: entnimmt jeweils der unsortierten Folge das Minimum und hängt es an die sortierte Folge an, d. h. n + (n − 1) + (n − 2) + . . . + 1 = O(n2 ) Schleifendurchläufe, dazu O(n2 ) Vergleiche und O(n) Vertauschungen Sortieren durch Einfügen: entnimmt der unsortierten Folge das erste Element und fügt es Position in der sortierten Folge ein, d. h. im worst case Pnan der richtigen 2 2 2 i=1 i = O(n ) Schleifendurchläufe, dazu O(n ) Vergleiche und O(n ) Vertauschungen Bubblesort: zwei benachbarte Elemente eines Arrays werden vertauscht, wenn sie in der falschen Reihenfolge sind; äußere Schleife läuft über alle Elemente, innere vertauscht evtl. aufeinanderfolgende Elemente, Laufzeit O(n2 ) Divide-and-Conquer-Methoden: DAC-Paradigma: if die Objektmenge ist klein genug then löse das Problem direkt else Divide: Zerlege die Menge in mehrere Teilmengen (wenn möglich, gleicher Größe). Conquer: Löse das Problem rekursiv für jede Teilmenge. Merge: Berechne aus den für die Teilmengen erhaltenen Lösungen eine Lösung des Gesamtproblems. ein balancierter DAC-Algorithmus (Teilmengen sind etwa gleich groß) hat eine Laufzeit O(n) falls Divide- und Merge-Schritt jeweils nur O(1) Zeit brauchen, O(n log n) falls Divide- und Merge-Schritt in O(n) durchführbar sind. fi Mergesort: O(n log n) im worst case, sortiert nicht in situ, wird vor allem extern benutzt; benutzt einfachen Divide- und arbeitet im Merge-Schritt: Array oder verkettete Liste wird rekursiv bis auf Einzelelemente geteilt und hernach rekursiv 9 sortierend (Verschmelzung der einzelner Listen, bis alles wieder zusammengefügt ist) zusammengesetzt (Laufzeit O(m + n) bei parallelem Durchlauf) Quicksort: O(n log n) im average case und O(n2 ) im Wurstkäse (da nicht vorausgesetzt werden kann, daß im Divide-Schritt eine balancierte Aufteilung erreicht wird; bei Clever-Quicksort werden zufällig drei Elemente ausgewählt und dabei das mittlere als Pivot benutzt, hat sich als praxistauglich erwiesen. Algorithmus: wähle irgendeine Schlüsselwert der Eingabefolge aus und berechne zwei Teilfolgen, von denen eine kleiner ist als der Schlüsselwert und die andere nicht, mache dies solange rekursiv, bis keine Unterfolgen mehr entstehen, hänge dann alles aneinander. Platzbedarf für Rekursionsstack kann O(n) werden. Heapsort: benutzt partiell geordneten Baum (Heap, siehe priority Queue), garantiert O(n log n) im worst case Baumsortieren: Einfügen in eine geordnete Folge durch einen AVL-Baum – ein inorder-Durchlauf liefert die sortierte Ergebnisfolge Sortieren durch Fachverteilen: wenn die Schlüsselwerte bestimmten Einschränkungen unterliegen und andere Operationen als Vergleiche angewandt werden können, kann man schneller als Ω(n log n) sortieren; Bucketsort verwendet eine Liste von Records (ähnliches offenem Hashing), bei denen sich ein Key in O(1) einem Behälter zuordnen läßt, es benötigt also O(n). Radixsort: Man kann das Sortieren auch in mehreren Phasen ablaufen lassen, z. B. erst nach dem letzte Zeichen eines Keys konstanter Länge, dann nach dem vorletzten, oder auch durch Datumsdarstellung. Es ist hierbei wesentlich für alle Phasen außer der ersten, daß Records ans Ende einer Bucketliste angefügt werden. Man benutze eine Listenimplementierung, die Anhängen ans Ende in O(1) Zeit erlaubt. Radixsortieren eignet sich z. B. recht gut, wenn die Schlüssel Zeichenketten fester, geringer Länge sind; wir fassen dann jeden Buchstaben als eine Ziffer zur Basis 26 auf. 10 Graphen grundsätzliches: Ein Graph stellt eine Menge von Objekten zusammen mit einer Beziehung (Relation) auf diesen Objekten dar. Objekte werden als Knoten, Beziehungen als Kanten dargestellt. Eine Beziehung wird im allgemeinen durch eine gerichtete Kante (einen Pfeil) dargestellt. Der Spezialfall einer symmetrischen Beziehung wird durch eine ungerichtete Kante dargestellt. Ein gerichteter Graph ist ein Paar G = (V, E), wobei gilt: V ist eine endliche, nichtleere Menge (die Elemente werden Knoten genannt), E ⊆ V × V (die Elemente heißen Kanten). Bäume sind Spezialfälle von Graphen. Darstellung durch Adjazenzmatrix: eine Adjazenzmatrix ist eine boolesche n × n-Matrix (darin werden nur vorhandene Relationen markiert); bei kantenmarkierten Graphen kann man direkt die Kantenbeschriftung statt der booleschen Werte in die Matrix (markierte Adjazenzmatrix) eintragen; Vorteil: man kann in O(1) feststellen, ob eine Kante existiert, Nachteil: hoher Platzbedarf (O(n2 )), Auffinden aller Nachfolger eines Knotens benötigt O(n) Darstellung durch Adjazenzlisten: Man verwaltet für jeden Knoten eine Liste seiner (Nachfolger-) Nachbarknoten. Über einen Array der Länge n = |V | ist jede Liste direkt zugänglich (bei markierten Graphen können die Listenelemente weitere Einträge enthalten), Vorteil: geringerer Platzbedarf von O(n + e). Man kann alle k Nachfolger eines Knotens in Zeit O(k) erreichen. Ein wahlfreier Test, ob v und w benachbart sind, kann aber nicht mehr in konstanter Zeit durchgeführt werden, da die Adjazenzliste von v durchlaufen werden muß, um das Vorhandensein von w zu überprüfen. Graphdurchlauf: für den systematischen Durchlauf eines Graphen wird angenommen, daß alle Knoten von einer Wurzel aus erreichbar sind (Wurzelgraph); Expansion in einen Baum, dann Tiefendurchlauf (entspricht einem Preorder-Durchlauf der Expansion, der jeweils in einem bereits besuchten Knoten abgebrochen wird) und den Breitendurchlauf (besucht die Knoten der Expansion ebenenweise, das bedeutet, daß im Graphen zunächst alle über einen Pfad der Länge 1, dann der Länge 2 usw. erreichbaren Knoten besucht werden, bei schon besuchten Knoten wird wie beim Tiefendurchlauf ebenfalls abgebrochen); In einem allgemeinen Graphen (ohne die Bedingung, daß alle Knoten von einem Wurzelknoten aus erreichbar sein müssen) können bei einem Durchlauf unbesuchte Knoten verbleiben; 11 1801 Betriebssysteme Stichworte: Prozessor, Hauptspeciher, Ein-/Ausgabegeräte, von-Neumann-Architektur, Befehlsausführung, Mindestbreite des Adreßbusses, Caches, Cache-Konsistenz, Cache-Verdrängungsstrategien, Sekundär-/Tertiärspeicher, Controller, Gerätetreiber, Controllerregister (Ein-, Aus-, Status-, Kontroll-), memory mapped I/O, virtuelles Gerät (Abstraktion, Kapselung, Schichtenmodell) Festplattenstrategien: FCFS (first come first serve); SSTF (shortest seek time first) bearbeitet den Auftrag zuerst, dessen Spuren am nächsten liegen; SCAN bewegt des Kopf abwechselnd von innen nach außen und zurück und führt dabei die Aufträge aus, deren Spuren gerade überquert werden DMA: direkter Speicherzugriff ohne CPU-Arbeit, Gerätekontroller handelt Zugriff mit DMA-Kontroller aus Prozesse: Prozeßkontext besteht aus Registerinhalten (z. B. Befehlszähler), Grenzen des Adreßraums, Prozeßnummer usw.; Prozeßzustände sind rechnend, bereit und blockiert; Schedulingstrategien FCFS (Nachteil bei langen Jobs), SJF, Prioritäten, round robin, Kommunikation zwischen Prozessen: geschieht entweder über Versand von Nachrichten oder gemeinsamen Speicherbereich; verbindungslos in einem Rechner über Signale, im Client-Server-betrieb über Systemrufe send und receive, verbindungsorientiert über Pipes; Speicherverwaltung Buddy-Strategie: Der Hauptspeicher besteht aus zusammenhängenden Stücken, die jeweils eine Zweierpotenz viele Seiten enthalten.EinStück ist entweder frei oder belegt. Wenn der Allokierer einen zusammenhängenden Bereich einer bestimmten Länge benötigt, nimmt er das kleinste freie Stück, das mindestens die erforderliche Länge aufweist. Wenn es mehr als doppelt so lang ist wie der benötigte Bereich, so wird es halbiert, das eine halbe Stück wird benutzt, das andere bleibt frei. Wenn Zwei benachbarte Stücke frei werden, so werden sie wieder verschmolzen. Prozeßsynchronisation: ist z. B. notwendig, wenn mehrere Prozesse ein betriebsmittel belegen wollen, wird mit Semaphoren gehandhabt. Ein Semaphor ist ein abstrakter Datentyp mit Zählvariable (enthält Anzahl freier Betriebsmittel) und Prozeßmenge (merkt sich anfragende Prozesse), benutzt zwei Operationen up() und down(); down() belegt das Betriebsmittel oder fügt den Prozeß in die wartendende Prozeßmenge ein, up() gibt Betriebsmittel frei und holt sich ggf. nächsten Prozeß aus der Menge. Während der Semaphorenarbeit sperrt das Betriebssystem die Unterbrechung derselben. Falls count nur 0 oder 1 werden kann, spricht man von wechselseitigem Ausschluß (mutual exclusion) Threads: verwenden gleichen Adreßraum wie ihr Erzeuger und unterscheiden sich nur durchunterschiedliche Stacks und Register; Vorteile: schnelle Kontextwechsel, 12 Datensharing, Nachteile: fehlender Speicherschutz, I/O blockiert u. U. (wenn Threads nicht im Kernel implementiert sind) ganzen Task; Anwendung bei Servern und als Gerätetreiber für langsame Geräte FAT: ist ein Array, in dem für jeden Block ein Eintrag steht, im Dateiverzeichnis steht die Adresse i für den ersten Block einer Datei, in F AT [i] die Adresse j des zweiten Blockes, in F AT [j] die Adresse des nächsten Blockes, für den letzten Block eof , für freie Blöcke f ree; wird in aufeinanderfolgenden Blöcken bespeichert, sollte so groß bemessen sein, daß sie komplett in den Hauptspeicher oder Cache paßt; bietet gute Übersicht, welche Blöcke noch frei sind; Größe berechnet sich: b × logB2 b (b =Anzahl der Blöcke, log2 b = Länge der Blockadresse in Bit, B =Länge eines Blocks in Bit) inodes: Variante des Index-Prinzips (Tabelle, die zu jeder logischen die physische Blockadresse enthält); sind an fester Position auf Platte gespeichert; bieten keine Übersicht, welche Blöcke noch frei sind, dafür kann man zusätzlich Bitvektor verweden; ist aufgebaut aus einem Attribut-Block (Zugriffsrechte, Größe, UID, GID,. . . ), dann die physischen Adressen der ersten zwölf Blöcke einer Datei, dann die Adresse eines Blocks, der die Adressen der nächsten logischen Dateiblöcke enthält (einfach-indirekter Index), dann die Adressen des zweifach-indirekten und dreifach-indirekten Indexblocks; berechnet sich: 12 + logB b + ( logB b )2 + ( logB b )3 (ein Block kann logB b Blockadressen enthalten) 2 2 2 2 13 Speicherverwaltung grundsätzliches: virtuelle Speicherverwaltungssystem nutzen Lokaliltätseigenschaft von Programmen aus (Prozesse benutzen i. A. nicht sämtliche Daten und Programme, sondern in jedem Zeitintervall nur einen Teil Davon); sind dem Benutzer transparent; Verfahren: Segmentierung (Segmente enthalten logisch zusammenhängende Informationen und können relativ groß sein, entsprechen (Unter-)Programmen und Datenmodulen, werden von Benutzer oder Compiler festgelegt, variable Länge); Seitenwechselverfahren (logischer und physikalischer Adreßraum werden in Seiten fester Länge unterteilt, Kapazität kann klein sein; ist dem Benutzer transparent Probleme: Einlagerungszeitpunkt (Betriebssystem muß festlegen, wann Daten eingelagert werden), Zuweisungsproblem (an welcher Stelle im Speicher sollen Daten eingelagert werden; bei Segmenten Problem, da diese keine einheitliche Größe besitzen ; Zuweisungsstrategien: first-fit, best-fit, worst fit, Speicher fragmentiert extern (Lücken für Segmente zu klein, muß in regelmäßigen Abständen verdichtet werden); bei Seiten kein Problem, da Seitengröße konstant, dabei aber interne Fragmentierung (Datenmengen entsprechen i. A. nicht dem Vielfachen einer Seitengröße), Ersetzungsproblem (welche Daten sollen ausgelagert werden – bei Seiten größeres Problem (häufigere Auslagerung), Strategien: FIFO, LIFO, LRU, LFU (least frequently used – Seite, auf die am seltensten zugegriffen wurde, wird ausgelagert) Zusammenspiel Cache-MMU: virtueller Cache (liegt zwischen CPU und MMU, höherwertige Bits der virtuellen Adresse werden als Tags abgelegt; keine Verzögerung durch MMU, aber viel größerer Adreßraum), physikalischer Cache (zwischen MMU und Speicher, Bits der physikalischen Adresse), Mischformen (z. B. bei DEC Alpha, z. B. Index virtuell und Tag physikalisch) Segmentorientierte Speicherverwaltung: virtuelle Adresse besteht aus zwei Teilen, Selektor zeigt auf das Segment, Offset auf genaue Position innerhalb des Segmentes; wird durch Segmentdeskriptor beschrieben (enthält Basisadresse, Segmentgröße und Zugriffsrechte), mit dessen Hilfe eine 32 bit lange lineare Adresse berechnet; Globale Deskriptor-Tabelle enthält Deskriptoren der Segmente, die von allen Prozessen genutzt werden dürfen, Lokale Deskriptortabelle enthält Segmentdeskriptoren für eigenen Code und Daten von Prozessen Seitenorientierte Speicherverwaltung: physikalische Adressen werden mit Hilfe eines zweistufigen hierarchischen Tabellensystems berechnet; lineare Adresse ist logisch in verschiedene Teile zerlegt (Directory -Offset bei 4-MByte-Seiten, Directory-PageTable-Offset bei 4-KByte-Seiten), Directory=Seitentabellenverzeichnis, Page Table=Seitentabelle; jeder Prozeß hat eigenes Seitentabellenverzeichnis; Seitentabellen(verzeichnis)-Einträge enthalten weiterhin Steuerbits 14 Ausnahmebehandlung grundsätzliches: werden meist softwaremäßig behandelt, außer RESET oder BERR; entspricht Unterprogramm, welches durch externes Signal oder bei Fehlerbedingung im µP aufgerufen wird Abarbeitung einer Ausnahme: 1. feststellen, ob interne oder externe Anforderung, bei externer über IE schauen, ob Unterbrechung zugelassen ist 2. wenn sinnvoll, augenblickliche Operation beenden 3. PC, SR, evtl. weitere Register, auf Stack retten 4. Feststellen der Quelle der Unterbrechungsanforderung 5. Informieren der Systemkomponenten entweder über Signal INTA oder über Statussignale 6. Deaktivieren bestimmter andere Unterbrechungsanforderungen 7. Ermitteln der Startadresse der Ausnahmeroutine, Laden dieser Adresse in PC 8. Ausführen der Ausnahmeroutine 9. Restaurieren der gesicherten Registerinhalte, Reaktivierung der zeitweilig gesperrten Unterbrechungsanforderungen, Fortsetzen des Programmes Interrupts: NMI werden nach Abschluß des gerade ausgeführten Befehls unbedingt durchgeführt; während Behandlung eines NMI ist kein weiterer Interrupt (egal ob NMI oder IRQ) zugelassen; IRQ (bzw. INT) werden nur ausgeführt, wenn IE gesetzt ist; Verschachteln von IRQ ist möglich, wenn Programmierer IE nicht zurücksetzt Traps: führen zu Abbruch des Befehls (z. B. nach Division durch 0, Überlauf, Bereichsüberschreitung in Arrays, Access Violation, invalid opCode, Stack Overflow Faults: führen idR zu Wiederholung des Befehls nach Beseitigung der Ursache, z. B. Pagefault mehrere Interruptquellen: früher Polling, heute: Interrupt-Vektortabelle liegt meist an niederwertigsten Adresse, enthält 2n Einträge der Länge m und relative Adresse jedes Eintrags, bezogen auf der Wert des Basisregisters. Komponente sendet INT, µP liefert INTA, Komponente setzte IVN in Register, µP kann Interrupt nun ausführen; Softwareinterrupts SWI n (oder INT n): Startadresse ist an festgelegter Speicherstelle abgelegt Prioritäten bei mehreren Anforderungen: NMI und Traps haben hohe Priorität interrupt controller 15 Rechnernetze grundsätzliches: Einsatz zum Daten-, Programm-, Geräteteilen, Rechenlastverteilen, Client-Server-Betrieb; Rechnernetze unterschiedlichster Konfigurationen und Betriebsweisen lassen sich mit Hilfe von Brücken und Gateways miteinander vernetzen, ISO-Referenzmodell: logische Aufteilung eines Systems impliziert in keiner Weise die physikalische Struktur, es müssen nicht sämtliche Schichten in allen Systemen realisiert sein; beschreibt zu jeder Schicht die allgemeinen Aufgaben, die der nächsthöheren Schicht bereitzustellenden Dienste und die zu realisierenden Funktionen; Schicht 7: Anwendungsschicht (application layer): verwendet die Funktionen aller unteren Schichten, um die gestellten Aufgaben zum Datenaustausch zu lösen Schicht 6: Darstellungsschicht (presentation layer): dient zur identische Interpretation der in Form von Bitfolgen ausgetauschten Datenobjekte Schicht 5: Kommunikationssteuerungsschicht (session layer): stellt einerseits zusätzliche Dienste zur Verfügung, die es den kommunizierenden Prozessen erlauben, ihren Dialog zu kon-trollieren und zu synchronisieren, zum anderen reicht sie Dienste der Transport-schicht, die die Güte einer Verbindung und den eigentlichen Datenaustausch betreffen, unverändert an die Darstellungsschicht durch. Schicht 4: Transportschicht (transport layer): erweitert die Dienste der Vermittlungsschicht um Wiederanordnung, Güteanforderungen (Verzögerungszeiten, Fehlerrate, Durchsatz, Sicherheit), Prioritäten; Im Gegensatz zur Vermittlungsschicht, die Verbindungen zwischen Endsystemen bereitstellt, liefert die Transportschicht Verbindungen zwischen in verschiedenen Endsystemen aktiven, miteinander kommunizierenden Anwendungsprozessen. Schicht 3: Vermittlungsschicht (network layer): stellt Instanzen übergeordneter Schichten, die in beliebi-gen Endsystemen residieren, logische Übertragungskanäle zur Verfügung, ohne daß die betreffenden Endsysteme direkt physikalisch miteinander verbunden sein müssen. Die Dienste der Vermittlungsschicht bieten den benutzenden Instanzen der Transportschicht Funktionen zur Flußkontrolle an, die es den Instanzen gestatten, die Rate der im Endsystem eintreffenden Daten zu kontrollieren. Schicht 2: Sicherungsschicht (data link layer): stellt unter Verwendung der Dienste der Bitübertragungsschicht weitgehend sichere Übertragungskanäle zur Verfügung (Fehlererkennung, -korrektur) Schicht 1: Bitübertragungsschicht (physical layer): stellt physikalische Übertragungskanäle zur Verfügung, die es gestatten, beliebige Bitfolgen zu übertragen. Hierbei können seitens der anfordernden Instanzen Anforderungen an den bereitzustellenden Übertragungskanal wie Fehlerrate, Verfügbarkeit, Übertragungsgeschwindigkeit und Über-tragungsverzögerung vorgegeben werden. 16 untere drei Schichten ermöglichen den Transport von Daten zwischen den Endsystemen der eigentlichen kommunizierenden Instanzen der Anwendungsschicht, obere vier Schichten sind für einen reibungslosen Verlauf des Dialogs zwischen Anwendungsprozessen sind; Signalübertragung: analog, digital, binär, (an-)isochron, Schrittdauer bei isochron: vS = T1 , Schrittgesachwindigkeit in Baud ( 1s ); Übertragungsverfahren Amplitudenmodulation, Frequenzmodulation, Phasendifferenzmodulation, PCM-Verfahren Datenübertragung: synchrone Übertragung in festem Zeitraster, asynchrone mit Stopp- und Startzeichen; Datenübertragungseinrichtungen können besitzen: Signalumsetzer (paßt die Signale der Datenendeinrichtung den Forderungen des Übertragungskanals an), Verwürfler/Entwürfler (vermeiden lange Ketten selber Werte), Anschalteinheit (baut Verbindung auf/ab), Prüfeinheit, Synchronisierungseinheit, Asynchron-/Synchronumsetzer, Fehlerschutzeinheit (nutzt redundante Information), Datenkompression/-dekompression; Datensicherung: Übertragungsfehler sollen erkannt werden, als Verfahren werden zeichenweise Informationssicherung (jedes codewort erhält Parität, man kann damit kann jede ungerade Anzahl verfälschter Binärzeichen erkennen), blockweise Informationssicherung (das jeweils i. Bit mehrere Codewörter wird mit einem Paritätszeichen gesichert), kreuzweise Informationssicherung (beide vorgherige Verfahren kombiniert) und blockweise zyklische Informationssicherung (benutzen binäre Polynome) verwendet einfache Sicherungsprotokolle: ARQ (Automatic Repeat ReQuest) setzen voraus, daß sich Rahmen während ihrer Übertragung nicht überholen können; Sende-und-Warte-Protokolle senden ACK oder NAK; Fensterprotokolle erlauben weitere Rahmen zu senden (bestimmte Anzahl), verwenden Befehle go-back-n (alles ab n nochmal senden) und selective-reject (einen Rahmen wiederholen) 17