ueb11 - oth

Werbung
Algorithmen und Datenstrukturen
Übung 11: Hash-Tabellen (Hash-Codingverfahren )
Allgemeine Abbildungen
Es geht um die Möglichkeit, den Index einer Arbeitsspeichertabelle direkt aus dem unter dieser
Tabellenposition abzuspeichernden Feldwert (Schlüssel) auszurechnen. Gesucht ist: Eine Abbildung
der Schlüsselwerte auf die Indexmenge. Eine perfekte Abbildung ist allerdings der Ausnahmefall. Die
Forderung nach Eindeutigkeit ist gleichbedeutend mit: Die Tabelle ist so lang auszulegen, daß jeder
mögliche Name genau einen Platz zugeordnet bekommt.
Bsp.: Schlüsselwerte sind intern durch ein Bitmuster, z.B. je Zeichen ein Byte, dargestellt. Dieses
Bitmuster kann als Dualzahl interpretiert werden. Jedem Schlüsselwert entspricht dann eine Zahl.
Namen mit 4 Zeichen umfassen so Dualzahlen zwischen 0 und 231-1, gefordert ist aber eine
Abbildung auf die Indexmenge von Null bis zur „Länge der Tabelle – 1“. Suchschlüssel bestehen in
den meisten Fällen aus Zeichen, die auf ganzzahlige Indexe transformiert werden sollen. Neben der
Schlüsseltransformation sollten geeignete Funktionen für die Abbildung noch zwei weitere Funktionen
erfüllen (, falls es möglich ist):
- einfache Berechnung der Adresse
- Möglichst gleichmäßige Streung der Schlüsselwerte über den Indexbereich
Die Verwendung einer Schlüsseltransformation führt auf ein entscheidendes Problem: Da die Menge
der theoretisch möglichen Schlüsselwerte wesentlich größer ist als die Menge der zur Verfügung
stehenden Adressen, kann die Abbildung der Schlüsselwerte auf Adressen nicht eindeutig sein. Auch
eine kompaktere Darstellung, in der man ein Zahlensystem mit einer passenden Basis aufstellt, führt
zu keiner Lösung.
Bsp.: Die Namen (Schlüssel) werden in der Regel aus 26 Buchstaben und 10 Ziffern gebildet. Man
kann folgende Zuordnung vereinbaren: A = 1, ... , Z = 26, 0 = 27, 1 = 28, ... , 9 = 36. Jedem Namen
entspricht dann eine Zahl im Zahlensystem zur Basis 37. Mit den Annahmen
- nur Namen aus 4 Zeichen sind zugelassen, die mit einem Buchstaben beginnen. Der größte Name
3
2
ist dann: Z999  26  37  36  37  36  37  36
- Der Speicherbereich betrage 1024 Bytes
- Es können 100 verschiedene Namen vorkommen
Da die Abbildung von 1367630 Namen auf 1024 Adressen nicht eindeutig sein kann, wird man
fordern: Möglichst gleichmäßige Abbildung der 100 wirklich vorkommenden Adressen. Wird ein Name
(Schlüsselwert) auf eine Adresse abgebildet, unter der schon ein anderer Name eingetragen ist, ist ein
Ausweichplatz zu bestimmen.
Die vorliegende Beispiele führen zu der folgende Überlegung:
- Direkte Abbildungen führen zur Speicherplatzverschwendung. Der Namensraum muß weniger (oder
höchstens gleich viele) Werte umfassen als der Adreßraum.
- Auf der anderen Seite sind Schlüsselwerte nur eine sehr eingeschränkte Teilmenge des durch den
Schlüssel angesprochenen Wertebereichs:
Der Füllfaktor (Belegungsfaktor) N/M (N: Anzahl der belegten Speicherstellen, M: Anzahl der zur
Verfügung stehenden Speicherplätze) ist dann gering.
Man wählt deshalb im allg. nicht eindeutige Abbildungen (Hash-Funktionen) und nimmt in Kauf, daß 2
Elemente (oder mehr) einer gegebenen Schlüsselwertmenge auf die gleiche Adresse (den gleichen
Index) abgebildet werden. Es ist dann ein Algorithmus zu bestimmen, der im Kollisionsfall (gleiche
Hausadresse) eine Ausweichadresse zuweist (Überlauf).
Überfaufverfahren
Überläufe können folgendermaßen behandelt werden:
1) Alle auftretenden Überläufe werden als lineares Feld außerhalb der unmittelbar zugeordneten
Hash-Tabelle gespeichert.
1
Algorithmen und Datenstrukturen
2) Die Überläufe jeder Speicherzelle (von jedem Tabellenelement) werden als lineares Überlauffeld
außerhalb der unmittelbar zugeordneten Hash-Tabelle gespeichert. Jede Zelle der Tabelle benötigt
einen Zeiger auf den Anfangsknoten des Überlauffelds. Zusätzlich werden noch weitere
Speicherzellen für Überläufer benötigt.
3) Überläufer werden in noch nicht verwendeten Speicherzellen der direkt zugeordneten Hash-Tabelle
gespeichert (offene Adressierung, Open Adressing)
Wahl der Hashfunktion
Eine gute Hash-Funktion sollte möglichst einfach und schnell berechenbar sein und die zu
speichernden Datensätze möglicht gleichmäßig auf den Speicherbereich zur Vermeidung von
Adreßkollisionen verteilen:
1. Die Divisions-Rest-Methode (Restklassenbildung)
Ein naheliegendes Verfahren zum Erzeugen einer Hashadresse H(S) , 0  H ( S )  M  1 , zu
gegebenem Schlüssel S aus dem Bereich der natürlichen Zahlen, ist, den Rest von S bei
ganzzahliger Division durch M (Länge der Tabelle) zu nehmen:
H ( S )  S mod M
Entscheidend ist die gute Wahl von M1. Damit die Adressen möglichst gleichmäßig verteilt sind,
empfiehlt es sich für den rechten Operanden des „Modulo-Operators“ eine Primzahl einzusetzen.
2. Faltung
Der Schlüssel wird in Komponenten zerlegt, z.B. Zahlen in Ziffern, Textworte in Buchstaben. Diese
Komponenten werden dann addiert bzw. multipliziert bzw. logisch verknüpft. Häufig findet man:
Multiplikation (meistens mit 2, Shift-Operation) und anschließendes XOR (exklusives Oder). Das
Hashing geschieht, indem man sich bspw. auf den Typ "word" beschränkt und die Überläufe ignoriert
und über ein nachgeschaltetes Divisions-Restverfahren.
3. „Midsquare“-Verfahren
Aus dem Schlüsselwert muß beim „Midsqare“-Verfahren ein ganzzahliger Wert gebildet werden. Im
Anschluß erfolgt die Bildung des Quadrats von diesem ganzzahligen Wert.
Auflösen von Kollisionen
Eine Hash-Funktion, die keine Kollisionen verursacht, heißt „perfekt“. Praktisch kann perfektes
Hashing nur für eine fest vorgebene Anzahl von Schlüsseln erreicht werden. Das ist bspw. beim
Aufbau einer Symboltabelle mit C++-Schlüsselwörtern (z.B. while, template) der Fall. Mit einem
Zugriff ist ein gesuchtes Schlüsselwort gefunden. In anders gearteten Fällen ist die Suche nach einer
perfekten Hash-Funktion nahezu unmöglich. Jeder neu hinzukommende Schlüssel zerstört den evtl.
erreichten perfekten Zustand.
Generell führen Hash-Funktionen auf Kollisionen. Zur Auflösung gibt es hier zwei unterschiedliche
Vorgehensweisen:
- Die kollidierenden Schlüssel werden aneinander verkettet (chaining)
- Von einer Anfangsadresse (Anfangsindex) wird eine Folge weiterer Adressen (Indizes) durchlaufen
(open adressing)
1. Kettungstechniken
"Seperate Chaining"
1
vgl. Ottman, T. und Widmayer, P.: Algorithmen und Datenstrukturen, B I Wissenschaftsverlag, Mannheim,
1990
2
Algorithmen und Datenstrukturen
Alle Schlüssel, die sich auf denselben Ort abbilden, werden mit Hilfe einer geketteten Liste verwaltet.
Die Hash-Tabelle enthält dann nur noch Zeiger auf Listenketten.
Walter ........
Werner ......
Kurt
.......
Dieter
Ernst
~
Richard
........
Liesel
Gerold
.......
.......
Karl
.........
........
Josef
........
Hans
Gerd
.........
Peter
.........
~
........
Abb.: Aufbau der Hash-Tabelle für „seperate chaining“
Die eigentliche Hash-Tabelle ist ein Array, desen Komponenten Referenzen auf Listenketten
enthalten. Die Listenketten bestehen aus Knoten mit Einträgen, die aus einem Schlüssel und
Datenelementen bestehen.
private static final int DEFAULT_TABELLEN_GROESSE = 19;
/* Das Feld mit den verketteten Listen */
private ArrayList dieListen;
/*
* Erzeuge die Hash-Tabelle.
*/
public SepChainHashTabelle( )
{
this(DEFAULT_TABELLEN_GROESSE);
}
/*
* Erzeuge die Hash-Tabelle.
* Parameter groesse bestimmt die Tabellengroesse.
*/
public SepChainHashTabelle(int size )
{
// dieListen = new ArrayList(23);
dieListen = new ArrayList(naechstePrimzahl(size));
for( int i = 0; i < naechstePrimzahl(size); i++ )
dieListen.add(new LinkedList( ));
System.out.println(dieListen.size());
}
Einträge, die nach dem vorliegenden Schema gebildet werden, werden in eine Hash-Tabelle
eingehangen. Die Einträge werden über den folgenden „abstrakten Datentyp“ beschrieben:
// Hash-Tabellen-Eintrag
public class HashTabEintr implements Hashable
{
private String name;
private String info;
// Konstruktoren
public HashTabEintr(String key)
{
name = key;
}
3
Algorithmen und Datenstrukturen
public HashTabEintr(String key, String daten)
{
name = key;
info = daten;
}
// Hash-Funktion (angepasst fuer Strings, Implementierung des Interface
// Hashable
public int hash(int tabellenGroesse)
{
return SepChainHashTabelle.hash(name,tabellenGroesse);
}
// Gleichheit
public boolean equals(Object rs)
{
return name.equals( ((HashTabEintr) rs).name);
}
// Ausgabe
public String toString()
{
return name + ", " + info;
}
}
Das Aufnehmen, Entfernen bzw. Wiederauffinden der Einträge erfolgt über2
/*
* Einfuegen in die Hash-Tabelle. Falls das einzufuegende
* Element schon da ist, dann tue nichts.
* Parameter x enthaelt das einzufuegende Element.
*/
public void insert(Hashable x)
{
LinkedList welcheListe =
(LinkedList) dieListen.get(x.hash(dieListen.size()));
if (welcheListe.add(x))
System.out.println("Element wurde eingefuegt");
else System.out.println("Element wurde nicht eingefuegt, Fehler");
}
/*
* Entfernen aus der Hash-Tabelle.
* Parameter x ist das entferenende Element.
*/
public void remove(Hashable x )
{
LinkedList welcheListe =
(LinkedList) dieListen.get(x.hash(dieListen.size()));
if (welcheListe.remove(x))
System.out.println("Element wurde entfernt");
else
System.out.println("Element wurde nicht entfernt!");
}
/*
* Bestimme ein Element in der hash-Tabelle.
* Parameter x ist das zu suchende Element.
* RückgabeWert: das passende Element, oder null, falls nicht gefunden.
*/
public Hashable finde(Hashable x )
{
LinkedList welcheListe =
(LinkedList) dieListen.get(x.hash(dieListen.size()));
2
vgl. pr22141
4
Algorithmen und Datenstrukturen
Iterator itr = welcheListe.iterator();
while (itr.hasNext())
{
HashTabEintr hte = (HashTabEintr) itr.next();
if (hte.equals(x))
{
System.out.println(hte.toString());
return x;
}
}
System.out.println(x.toString() + "; Element wurde nicht gefunden");
return null;
}
2. Überlaufverfahren ohne Kettung
Diese Verfahren suchen bei Adreßkollisionen nach einem freien Tabellenplatz. Bei der Bestimmung
solcher Folgen vom Adressen (Indexfolgen) sollen möglichst wenig Überlappungen (Häufungen)
entstehen. Man unterscheidet:
1) Primäre Häufungen
Sie entstehen, falls sich 2 treffende Folgen (von Adressen bzw. Indizes) gemeinsam weiterlaufen
2) Sekundäre Häufungen
Sie liegen vor, falls auf die gleiche Adresse (den gleichen Index) abgebildete Elemente gleiche
Auflösungsfolgen haben. Zwei Synonyme „s“ und „s‘“ durchlaufen stets dieselbe Sondierungsfolge,
behindern sich also auf den Ausweichplätzen.
Eine gute Hash-Funktion lokalisiert die Position i in einer Tabelle mit der Wahrscheinlichkeit 1/N. Ist
die Position i besetzt, dann ist die Wahrscheinlichkeit auf Position i + 1 zu landen: ½*N. Auf Position
i + 3 gelangt mit der Wahrscheinlichkeit: 1/3*N. Häufungen treten dann auf, wenn die
Wahrscheinlichkeit, freie Stellen in einer Gruppe aufeinanderfolgender Positionen zu finden, größer
ist, als irgendeine freie Stelle in der Tabelle.
Zur Bildung von Folgen (für Adressen, Indexe) können beliebige Verfahrensweisen zur Anwendung
kommen. In der Praxis haben sich jedoch einige typische Methoden durchgesetzt:
a) Lineare Fortschaltung (Lineares Sondieren)
Verfahrensweise
Liefert die Namens-Adreß-Transformation eine Adresse „as“, unter der schon ein Name eingetragen
ist, dann wird durch lineares Fortschalten as + 1, as + 2, ... die nächste freie Adresse gesucht.
Dort wird dann das Schlüsselwort eingetragen. Ist as  i  M , so wird die Tabelle zyklisch von vorn
durchlaufen. Die Suche (nach Schlüsseln oder freiem Platz) bricht spätestens dann ab, wenn die
komplette Tabelle durchlaufen wurde, ohne das gesuchte Element bzw. freien Speicherplatz zu
finden.
Bsp.: Der folgende Datensatz ist in einer 11 Elemente umfassenden Tabelle gespeichert:
struct datenSatz
{
int schl;
int daten;
} merkmal;
Die Hash-Funktion H() nutzt den Divisions-Rest-Algorithmus: H(merkmal) = merkmal.schl %
11. Folgende Daten sollen in die Hash-Tabelle aufgenommen werden: { 54, 77, 94, 89, 14,
45, 76 }. Das führt zu folgenden Einträgen:
5
Algorithmen und Datenstrukturen
[0]
77
[1]
89
[2]
45
[3]
14
[4]
76
[5]
[6]
94
[7]
[8]
[9]
[10]
54
Kollision: errechnete Position 0
Kollision: errechnete Position 1
Abb.:
Beurteilung
- Das lineare Suchen (linear probing) einer Ersatzadresse ist eines der ältesten und zugleich
uneffiktivsten Verfahren
- Häufige Fehlerkollisionen
- Bildung klumpenförmiger, primärer Häufungen (Clustering)
Die Hash-Tabelle, die beim Seperate Chaining gezeigt wurde, könnte für das „lineare Sondieren“
folgendermaßen aussehen:
Kurt
~
.........
Hans
........
Werner
.......
Josef
.......
Fritz
...........
Uwe
...........
Dieter
..........
~
Abb.: Hash-Tabellen-Aufbau nach dem linearen Sondieren
b) Zufälliges Suchen
Verfahrensweise
Bei diesem Verfahren wird im Kollisionsfall mit Hilfe einer Zufallszahl ein Ersatzplatz gesucht:
(1) Berechnung einer Tabellenadresse as (nach einer der beschriebenen Transformationen)
(2) Vergleich des vorliegenden Namens mit dem unter as eingetragenen Namen. Im Kollisionsfall
weiter bei (3), andernfalls STOP
(3) Berechnung einer Zufallszahl xi und Bildung der Zuordnung as := as + xi mod M
(4) Weiter bei (2)
Anforderungen an den Zufallszahlengenerator
- Die Zahlenfolge muß reproduzierbar sein, d.h.: Jede erstmalige Kollision eines Namens bewirkt die
Generierung derselben Folge von Zufallszahlen
- Es sollen möglichst viele Zahlen zwischen 1 und M-1 genau einmal erzeugt werden
Beurteilung
Alle Schlüssel, die auf diesselbe Adresse abgebildet werden, generieren diesselbe Zufallsfolge
(sekundäres Clustering)
c) Double Hashing
Verfahrensweise
6
Algorithmen und Datenstrukturen
Hier werden zwei voneinander unabhängige Hash-Funtionen benutzt. Die erste Funktion dient zur
Ermittlung der Position. Die zweite bestimmt, falls die Position belegt ist, die nächste freie Position im
Rahmen des „open adressing“. Die Sondierungsfolge ist statt einer zufälligen Permutation eine zweite
Hash-Funktion und führt für den Schlüssel S auf
H(S), H(S)-H‘(S),H(S)–2*H‘(S), ... ,H(S)–(M–1)*H‘(S)
(jeweils modulo M, H‘(S) berechnet die zweite Hash-Funktion). H‘(S) muß so gewählt werden, daß
für alle Schlüssel S die Sondierungsfolge eine Permutation der Hashadresse bildet, d.h. H‘(S) ist
ungleich Null und darf M nicht teilen. Man sagt: H‘(S) muß relativ prim sein zu M. Falls M eine
Primzahl ist, dann ist sicher jedes H‘(S) für alle „S“ relativ prim zu M. Wählt man H‘(S) abhängig von
H(S), so werden manche (oder sogar alle) Synonyme die gleiche Sondierungsfolge haben, eine
gewisse sekundäre Häufung ist die Folge.
Ist M eine Primzahl und H(S) = S mod M, dann erfüllt H‘(S)=1 + S mod (M-2)3 die
Anforderungen. Andere geeignete Hash-Funktionen für „double hashing“ sind
i+H‘(S)modM
bzw.
M – 2 – S * mod(M-2)
In der Praxis reicht häufig eine einfachere Hash-Funktion, z.B.: H‘(S) = 8 – (S mod 8)4. Das
würde bei der Hashtabelle der beispiele zu folgender Gestalt führen:
Kurt
.........
Werner
Josef
Bernd
~
........
.......
.......
~
Fritz
...........
Dieter
...........
Herbert
..........
Abb.: Hash-Tabellen-Aufbau nach „double hashing“
d) quadratisches Sondierem
Verfahrensweise
Zuerst wird im Kollisionsfall die unmittelbar folgende Position in der Hashtabelle untersucht (as + 1).
Danach, falls die Position besetzt ist, wird der Index nicht um 2 Einheiten sondern um 4 Einheiten
heraufgesetzt (as + 4). Führt das auch nicht zum Erfolg, dann wird der Index um 9 Einheiten erhöht
(as + 9). Die Quadratzahlen kann man sogar über eine einfache Addition bestimmen, falls man
folgende Berechnungsmöglichkeit der Quadratzahl nutzt:
0
1
1
4
3
9
5
16
7
25
9
36
11
49
13
64
15
Man braucht also nur der Zahl, die zur jeweils vorletzten Quadratzahl zur Ermittlung der letzten
aktuellen Quadratzahl addiert wurde, eine Zwei hinzuzufügen. Das Resultat wird zur letzten aktuellen
Quadratzahl addiert und man erhält die neue Quadratzahl.
3
4
das ist besser als 1-S mod(M-1), da M-1 gerade ist
vgl. Sedgewick, Robert: Algorithmen, Addison-Wesley, München, 1. Auflage 1991, S. 282
7
Algorithmen und Datenstrukturen
Mit dem quadratischen Sondieren erreicht man so mindestens die Hälfte aller möglichen Positionen,
falls die Tabellengröße eine Primzahl ist. Das ist in der Regel ausreichend.
Ernst
Karl
........
Hans
.......
Liesel
~
.........
.......
~
Fritz
...........
Walter
...........
Werner
..........
Abb.: Die Datenstruktur der Hashtabelle
Die „Hash-Tabelle“ enthält einen Verweis auf den Datensatz. Die Position für diesen Verweis ist
bestimmt durch eine im Datensatz vereinbarte Feldgrösse, den Schlüssel.
Schluessel
weitere Feldgrößen ......
Der zugehörige abstrakte Datentyp für die Einträge in die Hash-Tabelle ist:
/* Das Interface Hashable
interface Hashable
{
int hash(int tabellenGroesse);
}
*/
// Hash-Tabellen-Eintrag
public class HashEintrag
{
Hashable element;
boolean istAktiv;
// False, falls geloescht
// Konstruktoren
public HashEintrag(Hashable e)
{
this(e,true);
}
public HashEintrag(Hashable e, boolean b)
{
element = e;
istAktiv = true;
}
Die Datenstruktur (abstrakter Datentyp) der „Hash-Tabelle“ umfaßt:
public class QuadProbingHashTabelle
{
// Instanzvariable
private static final int DEFAULT_TABELLEN_GROESSE = 19;
/* Die Hash-Tabelle */
protected HashEintrag [] hashArray;
/* Anzahl der belegten Zellen */
private int aktGroesse;
8
Algorithmen und Datenstrukturen
// Methoden
/*
* Erzeuge die Hash-Tabelle.
*/
public QuadProbingHashTabelle( )
{
this(DEFAULT_TABELLEN_GROESSE);
}
/*
* Erzeuge die Hash-Tabelle.
* Parameter groesse bestimmt die Tabellengroesse.
*/
public QuadProbingHashTabelle(int groesse)
{
belegeArray(groesse);
makeEmpty();
}
/*
* Logisches Leermachen der Hash-Tabelle
*/
public void makeEmpty( )
{
aktGroesse = 0;
for (int i = 0; i < hashArray.length; i++)
hashArray[i] = null;
}
/*
*
*/
public void belegeArray(int arrayGroesse)
{
hashArray = new HashEintrag[arrayGroesse];
}
/*
* Einfuegen in die Hash-Tabelle. Falls das einzufuegende
* Element schon da ist, dann tue nichts.
* Parameter x enthaelt das einzufuegende Element.
*/
public void insert(Hashable x)
{
// Einfuegen von x als aktives Element
int aktPos = findePos(x);
if (istAktiv(aktPos))
return;
hashArray[aktPos] = new HashEintrag(x,true);
// Rehash
if (++aktGroesse > hashArray.length / 2)
rehash();
}
/*
* Entfernen aus der Hash-Tabelle.
* Parameter x ist das zu entfernende Element.
*/
public void remove(Hashable x)
{
int aktPos = findePos(x);
// System.out.println(aktPos);
if (istAktiv(aktPos))
hashArray[aktPos].istAktiv = false;
}
/*
* Bestimme ein Element in der hash-Tabelle.
* Parameter x ist das zu suchende Element.
9
Algorithmen und Datenstrukturen
* RueckgabeWert: das passende Element, oder null, falls nicht gefunden.
*/
public Hashable finde(Hashable x )
{
int aktPos = findePos(x);
return istAktiv(aktPos) ? hashArray[aktPos].element : null;
}
private int findePos(Hashable x)
{
int kollisionsNr = 0;
int aktPos = x.hash(hashArray.length);
while ((hashArray[aktPos] != null) &&
! hashArray[aktPos].element.equals(x))
{
aktPos += 2 * ++kollisionsNr - 1;
if (aktPos >= hashArray.length)
aktPos -= hashArray.length;
}
return aktPos;
}
/*
* Rueckgabe true, falls aktPos aktiv ist
* Parameter aktPos ist Resultat des Aufrufs von findePos
*/
private boolean istAktiv(int aktPos)
{
return hashArray[aktPos] != null &&
hashArray[aktPos].istAktiv;
}
/*
*
*/
public void printHashTabelle()
{
for (int i = 0; i < hashArray.length; i++)
{
if ((hashArray[i] == null) || !istAktiv(i)) System.out.println();
else
System.out.println(hashArray[i].element.toString());
}
}
/*
* Eine Hash-Routine fuer String-Objekte.
* Parameter key ist der String fuer die Hash-Funktion.
* Parameter tabellenGroesse ist die Groesse der Hash-Tabelle.
* Rueckgabewert: der Hash-Wert.
*/
public static int hash(String key, int tabellenGroesse)
{
int hashWert = 0;
for( int i = 0; i < key.length( ); i++ )
hashWert = 37 * hashWert + key.charAt(i);
hashWert %= tabellenGroesse;
if(hashWert < 0)
hashWert += tabellenGroesse;
return hashWert;
}
/*
* Interne Methode zum Bestimmen einer Primzahl, mindesetns so gross wie
* n.
* Parameter n gibt die Zahl an, mit der begonnen wird
* (muss positiv sein).
* Rueckgabe: eine Primzahl groesser oder gleich n.
10
Algorithmen und Datenstrukturen
*/
private static int naechstePrimzahl( int n )
{
if( n % 2 == 0 ) n++;
for( ; ! istPrimzahl(n); n += 2)
;
return n;
}
/*
* Interne Methode fuer den Test, ob eine Zahl eine Primzahl ist.
* Nicht besonders effizient.
* Parameter n ist die zu ueberpruefende Zahl.
* Rueckgabe: das Testrgebnis.
*/
private static boolean istPrimzahl(int n)
{
if (n == 2 || n == 3) return true;
if (n == 1 || n % 2 == 0) return false;
for( int i = 3; i * i <= n; i += 2 )
if (n % i == 0) return false;
return true;
}
/*
* Expansion der Hash-Tabelle
*/
private void rehash()
{
HashEintrag [] alterArray = hashArray;
// Erzeue eine doppelt so grosse, leere Tabelle
belegeArray(naechstePrimzahl(2 * alterArray.length));
aktGroesse = 0;
// Kopiere die Tabelle
for (int i = 0; i < alterArray.length; i++)
if (alterArray[i] != null && alterArray[i].istAktiv)
insert(alterArray[i].element);
}
}
Aufwand
Eine gute Hash-Funktion sorgt für gleichförmige Verteilung der Hash-Werte. In Kombination mit einer
größeren Tabelle ist die Zahl der Kollisionen niedrig. Falls eine Hash-Tabelle M Einträge besitzt und N
Einträge möglich sind, dann ist der Ladefaktor der Tabelle:
 MN.
Für die leere Tabelle ist

Null. Falls mehrere Elemente hinzugefügt werden, wächst  und die Kollisionen nehmen zu. Bei
offener Adressierung erhält  den Wert 1, wenn die Tabelle voll ist. Im Fall des „seperate chaining“
kann  auch größere Werte als 1 annehmen. Im schlimmsten Fall werden alle Datenelemente beim
„seperate chaining“ auf diesselbe Tabellenposition abgebildet. Falls die verkettete Liste N
Datenelemente umfaßt ist die Suchzeit in der Liste O(N). Durchschnittlich wird eine Suchzeit von
O()  O( M N) erwartet. Falls die Anzahl der Elemente fest ist, kann man im Fall des „seperate
chaining“ von einem O(1)-Aufwand ausgehen. Man hat zur Ermittlung rechnerischer Komplexität
folgende Formeln angegeben:
offene Adressierung
„sepearte chaining“
erfolgreiche Suche
nicht erfolgreiche Suche
1
1
 ,  1
2  (1   ) 2

1
2
1
1
 ,  1
2
2  (1   ) 2
e   
11
Algorithmen und Datenstrukturen
Falls  = 1 ist, erwartet man bzgl. erfolgreicher Suche N/2 Versuche. Erfolglose Suche bedeutet N
Versuche. Die Verfahren der offenen Adressierung arbeiten für kleine  gut. Die angegebene Formel
zum „open adressing“ gilt für „linear probing“.
Für „open adressing“ mit „double hashing“ hat Knuth 5 zu nicht vollen, keine Löschungen enthaltende
Hash-Tabellen für erfolgreiche Suche die Formel6
 ln( 1   )

angegeben.
Ladefaktor
0.5
0.6
0.7
0.8
0.9
1.0
2.0
4.0
„open adressing“
mit „linear probing“
„open adressing“ m.
„double hashing“
Chained hashing
1.50
1.75
2.17
3.00
5.50
-
1.39
1.53
1.72
2.01
2.56
-
1.25
1.30
1.35
1.40
1.45
1.50
2.00
3.00
Abb.: Durchschnittliche Anzahl überprüfter Tabellenelemente bis zur erfolgreiche Suche
5
6
vgl. Knuth: The Art of Programming, Volume 3
 ln( 0.2) 1.6

2
0,8
0.8
(Ladefaktor ist hier 0.8)
12
Algorithmen und Datenstrukturen
Lösungen
Separate Chaining7
// Das Interface Hashable
public interface Hashable
{
int hash(int tabellenGroesse);
}
import java.util.*;
import java.io.*;
// Hash-Tabellen-Eintrag
public class HashTabEintr implements Hashable
{
private String name;
private String info;
// Konstruktoren
public HashTabEintr(String key)
{
name = key;
}
public HashTabEintr(String key, String daten)
{
name = key;
info = daten;
}
// Hash-Funktion (angepasst fuer Strings
public int hash(int tabellenGroesse)
{
return SepChainHashTabelle.hash(name,tabellenGroesse);
}
// Gleichheit
public boolean equals(Object rs)
{
return name.equals( ((HashTabEintr) rs).name);
}
// Ausgabe
public String toString()
{
return name + ", " + info;
}
}
import java.util.*;
//
//
//
//
//
//
//
//
//
//
//
//
//
7
Die Klasse SepChainHashTabelle
KONSTRUKTION: mit einer geeigneten Groessenangabe oder dem Defaultwert
****************** PUBLIC OPERATIONEN *********************
void insert( x )
--> Einfuegen x
void remove( x )
--> Entfernen x
Hashable find( x )
--> Rueckgabewert: Element das zu x passt
void makeEmpty( )
--> Entfernen aller Elemente
int hash( String str, int tabellenGroesse )
--> Static Methode fuer Strings
****************** FEHLER ********************************
insert ueberschreibt nicht den vorliegenden Wert, falls dieser ein
vgl. pr22141
13
Algorithmen und Datenstrukturen
// Duplikat ist;
// kein Fehler
/*
* "Separate chaining Tabelle".
* Hinweis: Alles, was passt beruht auf der Methode "equals".
*
*/
public class SepChainHashTabelle
{
private static final int DEFAULT_TABELLEN_GROESSE = 19;
/* Das Feld mit den verketteten Listen */
private ArrayList dieListen;
/*
* Erzeuge die Hash-Tabelle.
*/
public SepChainHashTabelle( )
{
this(DEFAULT_TABELLEN_GROESSE);
}
/*
* Erzeuge die Hash-Tabelle.
* Parameter groesse bestimmt die Tabellengroesse.
*/
public SepChainHashTabelle(int size )
{
// dieListen = new ArrayList(23);
dieListen = new ArrayList(naechstePrimzahl(size));
for( int i = 0; i < naechstePrimzahl(size); i++ )
dieListen.add(new LinkedList( ));
System.out.println(dieListen.size());
}
/*
* Einfuegen in die Hash-Tabelle. Falls das einzufuegende
* Element schon da ist, dann tue nichts.
* Parameter x enthaelt das einzufuegende Element.
*/
public void insert(Hashable x)
{
LinkedList welcheListe =
(LinkedList) dieListen.get(x.hash(dieListen.size()));
if (welcheListe.add(x))
System.out.println("Element wurde eingefuegt");
else System.out.println("Element wurde nicht eingefuegt, Fehler");
}
/*
* Entferenen aus der Hash-tabelle.
* Parameter x ist das entferenende Element.
*/
public void remove(Hashable x )
{
LinkedList welcheListe =
(LinkedList) dieListen.get(x.hash(dieListen.size()));
if (welcheListe.remove(x))
System.out.println("Element wurde entfernt");
else
System.out.println("Element wurde nicht entfernt!");
}
/*
* Bestimme ein Element in der hash-Tabelle.
* Parameter x ist das zu suchende Element.
* RückgabeWert: das passende Element, oder null, falls nicht gefunden.
14
Algorithmen und Datenstrukturen
*/
public Hashable finde(Hashable x )
{
LinkedList welcheListe =
(LinkedList) dieListen.get(x.hash(dieListen.size()));
Iterator itr = welcheListe.iterator();
while (itr.hasNext())
{
HashTabEintr hte = (HashTabEintr) itr.next();
if (hte.equals(x))
{
System.out.println(hte.toString());
return x;
}
}
System.out.println(x.toString() + "; Element wurde nicht gefunden");
return null;
}
/*
* Mache die Hash-Table logisch leer.
*/
public void makeEmpty( )
{
for( int i = 0; i < dieListen.size(); i++ )
{
dieListen.set(i,null);
}
/*
for( int i = 0; i < dieListen.size(); i++ )
dieListen.add(new LinkedList( ));
*/
}
public void printHashTabelle()
{
for (int i = 0; i < dieListen.size(); i++)
{
LinkedList welcheListe = (LinkedList) dieListen.get(i);
if (welcheListe == null) continue;
Iterator itr = welcheListe.iterator();
while (itr.hasNext())
{
// String s = ((HashTabEintr) itr.next()).toString();
String s = itr.next().toString();
System.out.print(" " + s + "-->");
}
System.out.println();
}
}
/*
* Eine Hash-Routine fuer String-Objekte.
* Parameter key ist der String fuer die Hash-Funktion.
* Parameter tabellenGroesse ist die Groesse der Hash-Tabelle.
* Rueckgabewert: der Hash-Wert.
*/
public static int hash(String key, int tabellenGroesse)
{
int hashWert = 0;
for( int i = 0; i < key.length( ); i++ )
hashWert = 37 * hashWert + key.charAt(i);
hashWert %= tabellenGroesse;
if(hashWert < 0)
hashWert += tabellenGroesse;
return hashWert;
15
Algorithmen und Datenstrukturen
}
/*
* Interne Methode zum Bestimm. einer Primzahl, mindestens so gross wie n.
* Parameter n gibt die Zahl an, mit der begonnen wird (muss pos. sein).
* Rueckgabe: eine Primzahl groesser oder gleich n.
*/
private static int naechstePrimzahl( int n )
{
if( n % 2 == 0 ) n++;
for( ; ! istPrimzahl(n); n += 2)
;
return n;
}
/*
* Interne Methode fuer den test, ob eine Zahl eine Primzahl ist.
* Nicht besonders effizient.
* Parameter n ist die zu ueberpruefende Zahl.
* Rueckgabe: das Testrgebnis.
*/
private static boolean istPrimzahl( int n )
{
if (n == 2 || n == 3) return true;
if (n == 1 || n % 2 == 0) return false;
for( int i = 3; i * i <= n; i += 2 )
if (n % i == 0) return false;
return true;
}
}
import java.io.*;
import java.util.*;
public class SepChainHashTabTest
{
public static void main(String [ ] args)
{
SepChainHashTabelle h = new SepChainHashTabelle();
String eingabeZeile
= null;
BufferedReader eingabe = null;
try {
eingabe = new BufferedReader(
new FileReader("eing.txt"));
}
catch (FileNotFoundException io)
{
System.out.println("Fehler beim Einlesen!");
}
try {
while ( (eingabeZeile = eingabe.readLine() ) != null)
{
StringTokenizer str = new StringTokenizer(eingabeZeile);
if (eingabeZeile.equals("")) break;
String key
= str.nextToken();
String daten = str.nextToken();
System.out.println(key);
h.insert(new HashTabEintr(key,daten));
}
}
catch (IOException ioe)
{
System.out.println("Eingefangen in main()");
}
try {
16
Algorithmen und Datenstrukturen
eingabe.close();
}
catch(IOException e)
{
System.out.println(e);
}
h.printHashTabelle();
// Entfernen von Eintraegen
// System.out.println("Drei Eintraege werden entfernt");
// h.remove(new HashTabEintr("Fritz"));
// h.remove(new HashTabEintr("Josef"));
// h.remove(new HashTabEintr("Herbert"));
// h.printHashTabelle();
// Wiederauffinden
String eingabeKey = null;
BufferedReader ein = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Wiederauffinden von Elementen");
while (true)
{
try {
System.out.print("Bitte Schluessel eingeben, 0 bedeutet Ende: ");
eingabeKey = ein.readLine();
if (eingabeKey.equals("0")) break;
h.finde(new HashTabEintr(eingabeKey));
}
catch(IOException ioe)
{
System.out.println(eingabeKey +
" konnte nicht korrekt eingelesen werden!");
}
}
h.printHashTabelle();
System.out.println("Entfernen von Elementen");
while (true)
{
try {
System.out.print("Bitte Schluessel eingeben, 0 bedeutet Ende: ");
eingabeKey = ein.readLine();
if (eingabeKey.equals("0")) break;
if (h.finde(new HashTabEintr(eingabeKey)) != null)
{
System.out.println("Loeschen? (J/N)");
String eing = ein.readLine();
if (eing.equals("J")) h.remove(new HashTabEintr(eingabeKey));
}
}
catch(IOException ioe)
{
System.out.println(eingabeKey +
" konnte nicht korrekt eingelesen werden!");
}
}
h.printHashTabelle();
// Einfuegen
System.out.println("Einfuegen von Elementen");
while (true)
{
try {
System.out.print("Bitte Schluessel eingeben, 0 bedeutet Ende: ");
eingabeKey = ein.readLine();
if (eingabeKey.equals("0")) break;
String key = eingabeKey;
17
Algorithmen und Datenstrukturen
System.out.println("Bitte Daten eingeben");
eingabeKey = ein.readLine();
String daten = eingabeKey;
h.insert(new HashTabEintr(key,daten));
}
catch(IOException ioe)
{
System.out.println(eingabeKey +
" konnte nicht korrekt eingelesen werden!");
}
}
h.printHashTabelle();
}
}
18
Algorithmen und Datenstrukturen
Quadratisches Sondieren8
// Das Interface Hashable
public interface Hashable
{
int hash(int tabellenGroesse);
}
import java.util.*;
import java.io.*;
/* Das Interface Hashable
interface Hashable
{
int hash(int tabellenGroesse);
}
*/
// Hash-Tabellen-Eintrag
public class HashEintrag
{
Hashable element;
boolean istAktiv;
// False, falls geloescht
// Konstruktoren
public HashEintrag(Hashable e)
{
this(e,true);
}
public HashEintrag(Hashable e, boolean b)
{
element = e;
istAktiv = true;
}
}
import java.util.*;
// Die Klasse QuadProbingHashTabelle
//
// KONSTRUKTION: mit einer geeigneten Groessenangabe oder dem Defaultwert
//
// ****************** PUBLIC OPERATIONEN *********************
// void insert( x )
--> Einfuegen x
// void remove( x )
--> Entfernen x
// Hashable find( x )
--> Rueckgabewert: Element das zu x passt
// void makeEmpty( )
--> Entfernen aller Elemente
// int hash( String str, int tabellenGroesse )
//
--> Static Methode fuer Strings
// ****************** FEHLER ********************************
// insert ueberschreibt nicht den vorliegenden Wert, falls dieser ein
Duplikat ist;
// kein Fehler
/*
* Hash-Tabelle fuer quadratisches Probieren".
* Hinweis: Alles, was passt beruht auf der Methode "equals".
*/
public class QuadProbingHashTabelle
{
// Instanzvariable
private static final int DEFAULT_TABELLEN_GROESSE = 19;
/* Die Hash-Tabelle */
8
pr22142
19
Algorithmen und Datenstrukturen
protected HashEintrag [] hashArray;
/* Anzahl der belegten Zellen */
private int aktGroesse;
// Methoden
/*
* Erzeuge die Hash-Tabelle.
*/
public QuadProbingHashTabelle( )
{
this(DEFAULT_TABELLEN_GROESSE);
}
/*
* Erzeuge die Hash-Tabelle.
* Parameter groesse bestimmt die Tabellengroesse.
*/
public QuadProbingHashTabelle(int groesse)
{
belegeArray(groesse);
makeEmpty();
}
/*
* Logisches Leermachen der Hash-Tabelle
*/
public void makeEmpty( )
{
aktGroesse = 0;
for (int i = 0; i < hashArray.length; i++)
hashArray[i] = null;
}
/*
*
*/
public void belegeArray(int arrayGroesse)
{
hashArray = new HashEintrag[arrayGroesse];
}
/*
* Einfuegen in die Hash-Tabelle. Falls das einzufuegende
* Element schon da ist, dann tue nichts.
* Parameter x enthaelt das einzufuegende Element.
*/
public void insert(Hashable x)
{
// Einfuegen von x als aktives Element
int aktPos = findePos(x);
if (istAktiv(aktPos))
return;
hashArray[aktPos] = new HashEintrag(x,true);
// Rehash
if (++aktGroesse > hashArray.length / 2)
rehash();
}
/*
* Entfernen aus der Hash-Tabelle.
* Parameter x ist das zu entfernende Element.
*/
public void remove(Hashable x)
{
int aktPos = findePos(x);
// System.out.println(aktPos);
if (istAktiv(aktPos))
hashArray[aktPos].istAktiv = false;
}
20
Algorithmen und Datenstrukturen
/*
* Bestimme ein Element in der hash-Tabelle.
* Parameter x ist das zu suchende Element.
* RueckgabeWert: das passende Element, oder null, falls nicht gefunden.
*/
public Hashable finde(Hashable x )
{
int aktPos = findePos(x);
return istAktiv(aktPos) ? hashArray[aktPos].element : null;
}
private int findePos(Hashable x)
{
int kollisionsNr = 0;
int aktPos = x.hash(hashArray.length);
while ((hashArray[aktPos] != null) &&
! hashArray[aktPos].element.equals(x))
{
aktPos += 2 * ++kollisionsNr - 1;
if (aktPos >= hashArray.length)
aktPos -= hashArray.length;
}
return aktPos;
}
/*
* Rueckgabe true, falls aktPos aktiv ist
* Parameter aktPos ist Resultat des Aufrufs von findePos
*/
private boolean istAktiv(int aktPos)
{
return hashArray[aktPos] != null &&
hashArray[aktPos].istAktiv;
}
/*
*
*/
public void printHashTabelle()
{
for (int i = 0; i < hashArray.length; i++)
{
if ((hashArray[i] == null) || !istAktiv(i)) System.out.println();
else
System.out.println(hashArray[i].element.toString());
}
}
/*
* Eine Hash-Routine fuer String-Objekte.
* Parameter key ist der String fuer die Hash-Funktion.
* Parameter tabellenGroesse ist die Groesse der Hash-Tabelle.
* Rueckgabewert: der Hash-Wert.
*/
public static int hash(String key, int tabellenGroesse)
{
int hashWert = 0;
for( int i = 0; i < key.length( ); i++ )
hashWert = 37 * hashWert + key.charAt(i);
hashWert %= tabellenGroesse;
if(hashWert < 0)
hashWert += tabellenGroesse;
return hashWert;
}
/*
* Interne Methode zum Bestimmen einer Primzahl, mindesetns so gross wie
* n.
21
Algorithmen und Datenstrukturen
* Parameter n gibt die Zahl an, mit der begonnen wird
* (muss positiv sein).
* Rueckgabe: eine Primzahl groesser oder gleich n.
*/
private static int naechstePrimzahl( int n )
{
if( n % 2 == 0 ) n++;
for( ; ! istPrimzahl(n); n += 2)
;
return n;
}
/*
* Interne Methode fuer den Test, ob eine Zahl eine Primzahl ist.
* Nicht besonders effizient.
* Parameter n ist die zu ueberpruefende Zahl.
* Rueckgabe: das Testrgebnis.
*/
private static boolean istPrimzahl(int n)
{
if (n == 2 || n == 3) return true;
if (n == 1 || n % 2 == 0) return false;
for( int i = 3; i * i <= n; i += 2 )
if (n % i == 0) return false;
return true;
}
/*
* Expansion der Hash-Tabelle
*/
private void rehash()
{
HashEintrag [] alterArray = hashArray;
// Erzeue eine doppelt so grosse, leere Tabelle
belegeArray(naechstePrimzahl(2 * alterArray.length));
aktGroesse = 0;
// Kopiere die Tabelle
for (int i = 0; i < alterArray.length; i++)
if (alterArray[i] != null && alterArray[i].istAktiv)
insert(alterArray[i].element);
}
}
import java.io.*;
import java.util.*;
public class QuadProbHashTabTest
{
public static void main(String [ ] args)
{
QuadProbingHashTabelle h = new QuadProbingHashTabelle();
String eingabeZeile
= null;
BufferedReader eingabe = null;
try {
eingabe = new BufferedReader(
new FileReader("eing.txt"));
}
catch (FileNotFoundException io)
{
System.out.println("Fehler beim Einlesen!");
}
try {
while ( (eingabeZeile = eingabe.readLine() ) != null)
{
StringTokenizer str = new StringTokenizer(eingabeZeile);
22
Algorithmen und Datenstrukturen
if (eingabeZeile.equals("")) break;
String key
= str.nextToken();
String daten = str.nextToken();
System.out.println(key);
h.insert(new Eintrag(key,daten));
}
}
catch (IOException ioe)
{
System.out.println("Eingefangen in main()");
}
try {
eingabe.close();
}
catch(IOException e)
{
System.out.println(e);
}
System.out.println("Uebersicht zur Hash-Tabelle");
h.printHashTabelle();
System.out.println("Abfragen bzw. Modifikationen");
// Entfernen von Eintraegen
// System.out.println("Drei Eintraege werden entfernt");
// h.remove(new HashTabEintr("Fritz"));
// h.remove(new HashTabEintr("Josef"));
// h.remove(new HashTabEintr("Herbert"));
// h.printHashTabelle();
// Wiederauffinden
String eingabeKey = null;
BufferedReader ein = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Wiederauffinden von Elementen");
while (true)
{
try {
System.out.print("Bitte Schluessel eingeben, ! bedeutet Ende: ");
eingabeKey = ein.readLine();
// System.out.println(eingabeKey);
if (eingabeKey.equals("!")) break;
Hashable hEintr = h.finde(new Eintrag(eingabeKey));
if (hEintr == null)
System.out.println("Kein Eintrag!");
else
{
System.out.println(hEintr.toString());
System.out.println("Soll dieser Eintrag geloescht werden? ");
String antwort = ein.readLine();
// System.out.println(antwort);
if ((antwort.equals("j")) || (antwort.equals("J")))
{
// System.out.println("Eintrag wird entfernt!");
h.remove(new Eintrag(eingabeKey));
}
}
}
catch(IOException ioe)
{
System.out.println(eingabeKey +
" konnte nicht korrekt eingelesen werden!");
}
}
h.printHashTabelle();
23
Algorithmen und Datenstrukturen
/*
System.out.println("Entfernen von Elementen");
while (true)
{
try {
System.out.print("Bitte Schluessel eingeben, 0 bedeutet Ende: ");
eingabeKey = ein.readLine();
if (eingabeKey.equals("0")) break;
if (h.finde(new HashTabEintr(eingabeKey)) != null)
{
System.out.println("Loeschen? (J/N)");
String eing = ein.readLine();
if (eing.equals("J")) h.remove(new HashTabEintr(eingabeKey));
}
}
catch(IOException ioe)
{
System.out.println(eingabeKey +
" konnte nicht korrekt eingelesen werden!");
}
}
h.printHashTabelle();
// Einfuegen
System.out.println("Einfuegen von Elementen");
while (true)
{
try {
System.out.print("Bitte Schluessel eingeben, 0 bedeutet Ende: ");
eingabeKey = ein.readLine();
if (eingabeKey.equals("0")) break;
String key = eingabeKey;
System.out.println("Bitte Daten eingeben");
eingabeKey = ein.readLine();
String daten = eingabeKey;
h.insert(new HashTabEintr(key,daten));
}
catch(IOException ioe)
{
System.out.println(eingabeKey + " konnte nicht korrekt eingelesen
werden!");
}
}
h.printHashTabelle();
*/
}
}
24
Herunterladen