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 new : n → Stack(n) push : Stack × T → Stack pop : Stack → T top : Stack → T is_empty : Stack → Bool is_full : Stack → Boolaxioms ∀s : Stack, ∀x : T pop (push (s, x)) = x 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 } 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 → T 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
Simulierter Ringpuffer mit zwei umlaufenden Zeigern. O: Start; A,B,C: Zustand nach enter/leave Operationen
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 (oder 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
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Man kann sich leicht klar machen, dass bei einem genügend großen Wertevorrat auch immer n Elemente gefunden werden können, die durch h auf dieselbe Position abgebildet werden – ein Hashverfahren kann entarten. Durch eine gute Hashfunktion kann man die Wahrscheinlichkeit einer Entartung verringern, aber nie ganz ausschließen – wohl ein Grund, weshalb viele Programmierer Hashverfahren etwas misstrauisch betrachten und sie trotz ihrer guten Eigenschaften relativ selten einsetzen.
Einfügen, Löschen, Suche:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
h(k)=k mod N
Dies funktioniert in der Regel allerdings nur dann gut, wenn N eine (große) Primzahl ist, die nicht nahe an einer großen Zweierpotenz liegt, da sonst unbeabsichtigt besondere algebraische Eigenschaften der eingegebenen Schlüssel eine Gleichverteilung verhindern.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Für andere Datentypen kann eine Rückführung auf Integerwerte erfolgen:
h(s)=(a0⋅s[0]+a1⋅s[1]+…+al−1⋅s[l−1]) mod N
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Hashfunktionen sollen die Werte natürlich besonders gut »streuen«. Daher ist es oft sinnvoll, eventuell von Besonderheiten der Eingabewerte abhängige Funktionen zu wählen (Buchstaben des Alphabets tauchen in Namen unterschiedlich häufig auf!). Allerdings müssen Hashfunktionen auf jeden Fall effizient berechenbar sein (konstanter Zeitbedarf, auf keinen Fall abhängig von der Anzahl der gespeicherten Werte!).
Das Matrikelnummer-Beispiel zeigt, dass eine schlecht gewählte Hashfunktion die Güte des Verfahrens schnell negativ beeinflussen kann. Die Hashtabelle besteht hierbei aus 100 Buckets, gespeichert werden sollen Studentendatensätze, identifiziert durch die Matrikelnummer MATRNR. Die 6-stellige MATRNR wird fortlaufend vergeben.
Die von der Hashfunktion zu gegebenen Schlüsseln gelieferten Hashadressen sollten also über dem Adreßbereich gleichverteilt sein, und zwar selbst dann, wenn die Schlüssel aus 𝕂 alles andere als gleichverteilt sind (etwa bei der Vorliebe von Programmierern für Namen wie x, x1, x2, y1, y2, z1, z2).
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Der gegebene Schlüssel wird mit einer irrationalen Zahl multipliziert; der ganzzahlige Anteil des Resultats wird abgeschnitten. Auf diese Weise erhält man für verschiedene Schlüssel verschiedene Werte zwischen 0 und 1; für Schlüssel 1, 2, 3,.., n sind diese Werte ziemlich gleichmäßig im Intervall [0,1) verstreut
Von allen Zahlen Θ, 0 ≤ Θ ≤ 1, führt der goldene Schnitt zur gleichmäßigsten Verteilung:
ϕ−1=√5−12≈0.6180339887
Damit erhalten wir folgende Hashfunktion:
h(k)=⌊N(kϕ−1−⌊kϕ−1⌋)⌋
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Man kann die Berechnung von h(k) noch beschleunigen, wenn man ganze Zahlen im Rechner als Bruchzahlen mit Dezimalpunkt vor der höchstwertigen Ziffer ansieht, und wenn man für m eine Zweierpotenz wählt; dann läßt sich die Berechnung von h(k) mit einer ganzzahligen Multiplikation und einer (oder zwei) Shift-Operation(en) vornehmen.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Ist die Anzahl der zu speichernden Schlüssel nicht größer als die Anzahl der zur Verfügung stehenden Speicherplätze, gilt also für die Teilmenge K der Menge 𝕂 aller möglichen Schlüssel |K| ≤ N, so ist eine kollisionsfreie Speicherung von K immer möglich!
Wenn wir K kennen und K fest bleibt, können wir leicht eine injektive Ab-bildung h : K → { 0,..,N-1} z.B. wie folgt berechnen:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Lum, Yuen und Dodd hatten festgestellt, dass das Divisions-Rest-Verfahren im Durchschnitt die besten Resultate liefert!
Bei der Analyse der Effizienz von Hashverfahren geht es uns in erster Linie um die durchschnittliche Laufzeit für die Operationen Suchen, Einfügen und Entfernen.
Wir werden stets zwei Erwartungswerte Cn+ und Cn- angeben, bezogen auf feste Tabellengröße m.
Dabei ist Cn+ der Erwartungswert für die Anzahl der betrachteten Einträge der Hashtabelle bei erfolgreicher Suche, Cn- der Erwartungswert für die Anzahl der betrachteten Einträge der Hashtabelle bei erfolgloser Suche.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul H Tabellen :: Hash Tabellen
Folgende Verfahren können eingesetzt werden wenn eine Kollision festgestellt wird, d.h. der mit h(k1) berechnete Platz ist bereits mit einem Schlüssel k2 mit k1 ≠ k2 belegt, d.h. das einzufügende Element wird zum Überläufer
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