Praktische Einführung und Programmierung
Stefan Bosse
Universität Koblenz - FB Informatik
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen ::
Überblick in die Einführung einfachster Abstrakter Datentypen mit Tabellen:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Lineare Arrays
Ein Array ist eine Tabelle mit einer Spalte und n Zeilen.
T=⎛⎜ ⎜ ⎜⎝v0v1..vn−1⎞⎟ ⎟ ⎟⎠Addr=I(T)={0,1,2,..,n−1}
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Lineare Arrays
Elementare (eingbaute) Operationen:
T[index]
T[index]=ε
Abstrakte Operationen:
Es gibt beliebig viele Operationen die man auf Tabllen anwenden und implementieren kann. Suche und Sortierung (kommt noch) sind die wichtigsten.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Lineare Arrays
Der Index einer Tabelle ist ein Schlüssel zur Erlangung eines Werts. So könnte man numerischen Indexwerten eine Zecihenkette zuordnen.
T=['Zero','One','Two']print(T[0])
Schlüssel-Wert Paare für Lookup-Tabellen
Der große Vorteil: "Suchzeit", d.h. Abbildung eines Schlüssels (Index) auf einen Wert geschieht in konstanter Laufzeit O(1)!
Der große Nachteil: Die Schlüsselfolge ist fest vorgegeben und startet bei 0. Bei dünn besetzten Schlüsselmengen bräuchte man dünnbesetzte riesig Tabellen!
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Lineare Arrays
┌――――┐ 0 ┌――――┐ k=0 ――――▶ │ a │ ――――▶ a k=10――――▶H(k)│ a │ ――――▶ a ├――――┤ 1 ├――――┤ k=1 ――――▶ │ b │ ――――▶ b │ f │ ├――――┤ 2 ├――――┤ │ c │ k=2 ――――▶H(k)│ e │ ――――▶ e ├――――┤ 3 ├――――┤ │ d │ │ d │ ├――――┤ 4 ├――――┤ │ e │ │ b │ ├――――┤ 5 ├――――┤ │ f │ │ b │ │ │ └――――┘ │ │ └――――┘
Lineare versa Hashtabellen als Suchtabellen (Look-up tables)
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
Ein Kellerspeicher oder auch Stapelspeicher alias Stack genannt ist eine lineare Tabelle (Array), aber
Nur abstrakte Operationen:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
type Stack(T)operators empty : → Stack(n) new : n → Stack(n) push : Stack × T → Stack pop : Stack → Stack top : Stack → T is_empty : Stack → Bool is_full : Stack → Boolaxioms ∀s : Stack, ∀x : T pop (push (s, x)) = s top (push (s, x)) = x is_empty (empty) = true is_empty (push (s, x)) = false is_full (empty) = false is_full (n*push (s, x)) = true
ADT-Spezifikation eines Stacks
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
LIFO Prinzip: (Last-In-First-Out-Speicher): Beim Auslesen eines Elementes wird das jeweils zuletzt gespeicherte Element zuerst ausgelesen, danach das vorletzte etc. Der Kellerspeicher wird daher auch als Stapel bezeichnet: Elemente werden sozusagen übereinander gestapelt und dann wieder in umgekehrter Reihenfolge vom Stapel genommen.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
addp Verdeutlichung des Stack-Prinzips
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
Eine besondere Behandlung erfordert noch der Fall, dass versucht wird, ein Element vom leeren Stack zu lesen oder wenn es zu einem Überlauf des Stacks kommt.
Dies kann über eine Ausnahme (Exception, Signal) des Typs StackException signalisiert werden, die beim Aufruf der Methoden pop, push und top auftreten kann.
Der Datentyp der Datenelemente kann beliebig sein:
Statisch typisierte Programmiersprachen erfordern entweder ein virtuelles Template oder für jeden Datensatztyp eine eigene Implementierung. Dynamisch typisierte und teils auch funktionale Sprachen haben dieses Problem nicht.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
Algorithmisch brauchen wir wieder eine Indexabbildungsfunktion, die jetzt aber parameterlos und zustandsbehaftet wird (d.h. zum Stapelobjekt gehört).
Am einfachsten ist sowohl beim LIFO als noch beim folgenden FIFO Speicher die Einführung zweier Indexzeiger:
Man kann hier aber einfach nur einen Zeiger verwenden:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
function Stack(N) { return {SP:0,N:N,T:Array(N)}}function push(S,x) { if (full(S)) throw "Full"; S.T[S.SP]=x; S.SP++;}function pop(S) { if (empty(S)) throw "Empty"; x=S.T[S.SP-1]; S.SP--; return x}function full(S) { return S.SP==S.N-1 } function empty(S) { return S.SP==0 }
Implementierung der Stackoperationen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stapelspeicher
Die Komplexitätsklasse der Push und Pop Operationen liegt weiterhin bei konstanten O(1)!
Hardware: Fast jeder Mikroprozessor hat einen Hardware Stack (also in Register-Transfer Logik implementierten Stack)
Die Stack Maschine
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stackprozessor
Wir benötigen noch eine weitere Stack Operation nth die den n-obersten Werte (n>0) vom Stapel liest:
function nth(S,n) { return S.T[S.SP-n] } // n > 0
Jetzt müssen wir Befehle für unsere einfache Numerik VM definieren:
function LITERAL(x) { return x /*number*/ }function ADD() { return '+' }function END() { return '.' }
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stackprozessor
Auf dem Stack sind Daten und Code abgelegt. Code ist ein String, Daten ein numerischer Wert. That's all folks.
Wir brauchen nun einen Stackprozessor der das Programm vom Stack abarbeitet:
function run() { next=pop(C) while (next!='.') { switch (next) { case '+': a=pop(D); b=pop(D) push(D,a+b) break; default: if (typeof next=='number') push(D,next); else throw "EEXEC" } next=pop(C); }}
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stackprozessor
Jetzt folgt der Stack und ein kleines Beispielsprogramm:
C=Stack(10)D=Stack(10)// Programmpush(C,END())push(C,ADD())push(C,LITERAL(3))push(C,LITERAL(2))run(C);print(pop(D))
Jetzt wollen wir noch die Subtraktion hinzufügen. Bei der Addition ist die Reihenfolge der Operanden nicht relevant, bei der Subtraktion schon.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Der Stackprozessor
function SUB() { return '-' }function SWAP() { return '%' }function run() { ... case '-': a=pop(D);b=pop(D);push(D,a-b); break; case '%': a=pop(D);b=pop(D);push(D,a);push(D,b); break; ...
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
Um die Prinzipien der Spezifikation von parametrisierten Datenstrukturen zu vertiefen, betrachten wir kurz eine weitere Datenstruktur, nämlich die Warteschlange oder englisch Queue.
Eine Queue realisiert das sogenannte FIFO-Prinzip, also einen First-In-First-Out-Speicher.
Es gibt wieder zwei wesentliche Operationen, die zwar auf einer Tabelle operieren, aber ebenfalls wie beim Stack keine explizite Indizierung der Tabelle benörigen (daher auch kein RAM Modell):
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
type Queue(T)operators empty : → Queue new : n → Queue(n) enter : Queue × T → Queue leave : Queue → Queue bottom : Queue → T top : Queue → T is_empty : Queue → Bool is_full : Queue → Boolaxioms ∀q : Queue, ∀x, y : T leave (enter (empty, x)) = empty leave (enter (enter(q, x), y)) = enter (leave (enter(q, x)), y) top (enter (empty, x)) = x top (enter (enter(q, x), y)) = top (enter (q, x)) is_empty (empty) = true is_empty (enter(q, x)) = false is_full (empty) = false is_full (enter(q, x)) = false is_full (n * enter(q, x)) = true
Die Spezifikation einer Queue sieht auf den ersten Blick sehr ähnlich zu der eines Stacks aus, obwohl das Verarbeitungsprinzip komplett entgegengesetzt ist
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
Bezüglich Spezifikation: Im Unterschied zum Stack kann jetzt bei leave und bei front nicht der jeweilig letzte gespeicherte Parameter bearbeitet werden. Stattdessen ist es nötig, durch rekursiv aufgebaute Gleichungen sich bis zum jeweils »innersten« Wert des Queue-Terms vorzuarbeiten.
addp Verdeutlichung des Queue-Prinzips
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
Warteschlangen werden ebenfalls in vielfältiger Weise in Softwaresystemen eingesetzt, da sie das Abarbeiten von Aufträgen in Eingangsreihenfolge realisieren können:
https://mungfali.com/explore/FIFO-Method-Diagram Beispiel: Einsatz einer Queue im Nachrichtenmuliplexing (Multi-Queue System)
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
Das erste Element befindet sich immer auf der Position 0. Dies hat zur Folge, dass nach jedem Herausnehmen eines Elementes alle verbliebenen Elemente nach links verschoben werden müssten, was natürlich eine eher ungeeignete Variante darstellt.
Wir brauchen jetzt tatsächlich zwei Zeiger um eine Queue mit einer linearen Tabelle zu implementieren:
Die zeiger werden bei enter und leave nur inkrementoiert:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
BOTTOM ┌―――――――――┐ ――┬―▶ │ Used │ ◀――――┐ │ ├―――――――――┤ HEAD │ │ │ Free │――┬―▶ │ leave │ ├―――――――――┤ │ │ enter │ │ │ │ │ │ │ ├―――――――――┤ │ │ │ │ │ │ │ │ │ │ ├―――――――――┤ │ │ └―― ▼ │ │ ▼ ―――┘ └―――――――――┘
Simulierter Ringpuffer mit zwei umlaufenden Zeigern.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
Es wird ein zyklischer Ringpuffer durch die Modulo-Operation erzeugt: H%N, B%N mit N als Anzahl der Speicherzellen.
Den Füllstand einer Queue über die beiden Zeigen zu bestimmen (empty, full) ist nicht eindeutig. Gleichheit von HEAD und BOTTOM liegt bei Leere und vollständiger Füllung vor! Daher wird ein Zähler verwendet (pder es dürfen nur N-1 Feldelemente befüllt werden).
function Queue(N) { return { H:0,B:0,T:Array(N), N:N, n:0 } }function enter(Q,x) { Q.T[Q.H]=x; Q.H=(Q.H+1) % Q.N; Q.n++; }function leave(Q,x) { x=Q.T[Q.B]; Q.B=(Q.B+1) % Q.N; Q.n--; return x}function empty(Q) { return Q.n==0 }function full(Q) { return Q.n==Q.N }function bottom(Q) { return Q.n>0 && Q.T[Q.B] }function top(Q) { return Q.n>0 && Q.T[abs((Q.H-1)%Q.N)] }
Queue Operationen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Die Warteschlange
Ein Problem der hier vorgestellten Implementierung ist die Größenbeschränkung des Feldes. So werden nur so lange Elemente eingefügt, bis die Kapazität des Feldes erschöpft ist. Ein Ausweg wäre das Erzeugen eines neuen, größeren Feldes und das Umkopieren der Elemente aus dem alten Feld. Dennoch haben Felder durchaus vorteilhafte Eigenschaften, wie etwa den günstigen Platzverbrauch (keine zusätzlichen »Verwaltungsinformationen«) oder die kompakte und dadurch Cache-freundliche Organisation der Daten.
Die Zeitkomplexität ist konstant und beträgt O(1) für alle Operationen.
Der Speicherplatzbedarf wird dagegen durch das Anlegen eines Feldes einer vorgegebenen Größe N verursacht und ist somit unabhängig von der tatsächlichen Anzahl der Elemente im Stack bzw. in der Queue. Für die Speicherplatzkomplexität kann demnach O(N) angegeben werden.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Das Grundprinzip von Hashverfahren lässt sich mit wenigen Aussagen charakterisieren:
Da es in der Regel sehr viel mehr möglicherweise zu speichernde Elemente als die N Positionen im Feld gibt und die zu speichernden Elemente vorher unbekannt sind, kann die Funktion nicht allen möglichen Elementen unterschiedliche Positionen zuweisen.
Wird zwei Elementen dieselbe Position zugewiesen, spricht man von einer Kollision. Neben einer möglichst gut streuenden Hashfunktion ist die Behandlung von Kollisionen ein zentrales Merkmal von Hashverfahren.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
AlgoSkript.pdf p 56 Algorithmen und Datenstrukturen, Reith
Algorithmen und Datenstrukturen (Thomas Ottmann, Peter Widmayer) (Z-Library).pdf p 169 Kap. 4 hashverfahren