reach 4 explizit = jeder Zustand/Übergang wird einzeln repräsentiert symbolisch = Mengen-Repräsentation von Zuständen/Übergängen durch Formeln

Ähnliche Dokumente
Programmiertechnik II

Datenstrukturen. einfach verkettete Liste

Bäume, Suchbäume und Hash-Tabellen

Informatik II, SS 2014

Klausur C-Programmierung / / Klingebiel / 60 Minuten / 60 Punkte

Probeklausur Programmieren in C Sommersemester 2007 Dipl. Biol. Franz Schenk 12. April 2007, Uhr Bearbeitungszeit: 105 Minuten

Informatik II, SS 2014

Schwerpunkte. Verkettete Listen. Verkettete Listen: 7. Verkettete Strukturen: Listen. Überblick und Grundprinzip. Vergleich: Arrays verkettete Listen

Datenstrukturen. Mariano Zelke. Sommersemester 2012

12. Hashing. Hashing einfache Methode um Wörtebücher zu implementieren, d.h. Hashing unterstützt die Operationen Search, Insert, Delete.

Grundlagen der Informatik. Prof. Dr. Stefan Enderle NTA Isny

Einführung in die Informatik I

Programmieren in C. Rekursive Strukturen. Prof. Dr. Nikolaus Wulff

Advanced Programming in C

Grundlagen der Programmiersprache C für Studierende der Naturwissenschaften

Nicht-Kanonizität von AIGs Systemtheorie 1 Formale Systeme 1 # WS 2006/2007 Armin Biere JKU Linz Revision: 1.6

Grundlagen der Künstlichen Intelligenz

Einführung in die Programmierung Wintersemester 2008/09

Algorithmen und Datenstrukturen (für ET/IT) Wiederholung: Ziele der Vorlesung. Wintersemester 2012/13. Dr. Tobias Lasser

Algorithmen und Datenstrukturen

Vorlesung Informatik 2 Algorithmen und Datenstrukturen

Programmiertechnik II

11. Rekursion, Komplexität von Algorithmen

Die Programmiersprache C Eine Einführung

Einleitung Implementierung Effizienz Zeit-Buffer Zusammenfassung Quellenverzeichnis. Hashing. Paulus Böhme

Dynamische Speicherverwaltung

Grundlagen der Künstlichen Intelligenz

Der Dreyfus-Wagner Algorithmus für das Steiner Baum Problem

8. Hashing Lernziele. 8. Hashing

Speicher und Adressraum

II.3.1 Rekursive Algorithmen - 1 -

GI Vektoren

Vorlesung Datenstrukturen

Organisatorisches. Algorithmen und Datenstrukturen (für ET/IT) Programm heute. Definition Datenstruktur. Nächste Woche keine Vorlesung!

Klausur Programmieren in C Sommersemester 2007 Dipl. Biol. Franz Schenk 13. April 2007, Uhr Bearbeitungszeit: 105 Minuten

Programmieren in C. Speicher anfordern, Unions und Bitfelder. Prof. Dr. Nikolaus Wulff

1.4. Funktionen. Objektorientierte Programmierung mit C++

Nachname: Vorname: Matr.-Nr.: Punkte: 1. Aufgabe: ( / 25 Pkt.) Gegeben ist das folgende Struktogramm zur Berechnung von sin(x) mit Hilfe einer Reihe.

7. Sortieren Lernziele. 7. Sortieren

Willkommen zur Vorlesung. Algorithmen und Datenstrukturen

Welche Informatik-Kenntnisse bringen Sie mit?

Name: Seite 2. Beantworten Sie die Fragen in den Aufgaben 1 und 2 mit einer kurzen, prägnanten Antwort.

Algorithmen zur Datenanalyse in C++

6. Verkettete Strukturen: Listen

Lösungsvorschlag Serie 2 Rekursion

Dynamische Programmierung. Problemlösungsstrategie der Informatik

Algorithmen & Programmierung. Steuerstrukturen im Detail Selektion und Iteration

4.4.1 Implementierung vollständiger Bäume mit Feldern. Reguläre Struktur: Nachfolger des Knoten i sind die Knoten 2*i und 2*i+1.

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 16/17. Kapitel 14. Bäume. Bäume 1

15. Elementare Graphalgorithmen

Komplexität von Algorithmen

Graphen: Datenstrukturen und Algorithmen

4. Hashverfahren. geg.: Wertebereich D, Schlüsselmenge S = {s 1,..., s n } D. Menge A von Speicheradressen; oft: A = {0,..., m 1}

Graphalgorithmen I. Simon Regnet. May 16, Universität Erlangen. Simon Regnet (Universität Erlangen) Graphalgorithmen I May 16, / 56

Datenstrukturen. Mariano Zelke. Sommersemester 2012

C- Kurs 09 Dynamische Datenstrukturen

13. Bäume: effektives Suchen und Sortieren

5.8.2 Erweiterungen Dynamische Hash-Funktionen (mit variabler Tabellengröße)?

Algorithmen und Datenstrukturen SoSe 2008 in Trier. Henning Fernau Universität Trier

Deklarationen in C. Prof. Dr. Margarita Esponda

Programmieren in C. C Syntax Datentypen, Operatoren und Kontrollstrukturen. Prof. Dr. Nikolaus Wulff

C++ Notnagel. Ziel, Inhalt. Programmieren in C++

Gliederung. Tutorium zur Vorlesung. Gliederung. Gliederung. 1. Gliederung der Informatik. 1. Gliederung der Informatik. 1. Gliederung der Informatik

Einstieg in die Informatik mit Java

Kapitel 9. Komplexität von Algorithmen und Sortieralgorithmen

FH München, FB 03 FA WS 06/07. Ingenieurinformatik. Name Vorname Matrikelnummer Sem.Gr.: Hörsaal Platz

Haskell, Typen, und Typberechnung. Grundlagen der Programmierung 3 A. Einige andere Programmiersprachen. Typisierung in Haskell

Kapitel 5: Dynamisches Programmieren Gliederung

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 11/12 1. Kapitel 11. Listen. Listen

Einführung in die Informatik 2

C-Kurs 2010 Pointer. 16. September v2.7.3

Vorkurs Informatik WiSe 16/17

Themen. Statische Methoden inline Methoden const Methoden this Zeiger Destruktor Kopierkonstruktor Überladen von Operatoren

Integer Integer Integer (Voreinstellung) Integer Gleitkomma Gleitkomma leer/unbestimmt Integer ohne Vorzeichen Integer (explizit) mit Vorzeichen

Kapitel 9. Komplexität von Algorithmen und Sortieralgorithmen

Algorithmen und Datenstrukturen (für ET/IT) Programm heute. Sommersemester Dr. Tobias Lasser

Stack stack = new Stack(); stack.push ("Würstchen"); string s = (string) stack.pop(); Console.WriteLine (s);

M. Graefenhan Übungen zu C. Blatt 3. Musterlösung

Algorithmen & Datenstrukturen 1. Klausur

Branch-and-Bound. Wir betrachten allgemein Probleme, deren Suchraum durch Bäume dargestellt werden kann. Innerhalb des Suchraums suchen wir

DAP2 Praktikum Blatt 1

Student: Alexander Carls Matrikelnummer: Aufgabe: Beschreibung des euklidischen Algorithmus Datum:

Übungsklausur Algorithmen I

Dynamisches Huffman-Verfahren

Binärbäume: Beispiel

Übungspaket 29 Dynamische Speicherverwaltung: malloc() und free()

Programmieren was ist das genau?

8 Elementare Datenstrukturen

Dr. Monika Meiler. Inhalt

Nachklausur Bitte in Druckschrift leserlich ausfüllen!

Gebundene Typparameter

Klausur: Grundlagen der Informatik I, am 05. Februar 2008 Dirk Seeber, h_da, Fb Informatik. Nachname: Vorname: Matr.-Nr.: Punkte:

Algorithmen und Programmierung II

Herzlich willkommen!!!

INSTITUT FÜR THEORETISCHE INFORMATIK, PROF. SANDERS

J.5 Die Java Virtual Machine

Übungspaket 23 Mehrdimensionale Arrays

Variablen. Deklaration: «Datentyp» «Variablenname» Datentyp bestimmt Größe in Bytes: sizeof Beispiel: long int v; Größe: 4 Bytes

Einführung in die C-Programmierung

Es sei a 2 und b 2a 1. Definition Ein (a, b)-baum ist ein Baum mit folgenden Eigenschaften:

Transkript:

Erreichbarkeitsanalyse reach 1 Symbolisch vs Explizit reach 2 Definition Unter Erreichbarkeitsanalyse versteht man explizit = jeder Zustand/Übergang wird einzeln repräsentiert symbolisch = Mengen-Repräsentation von Zuständen/Übergängen durch Formeln Berechnung aller erreichbaren Zustände Resultat kann explizit oder symbolisch repräsentiert werden Explizites Modell Symbolisches Modell wird für weitere Algorithmen oder Optimierung der Systeme benötigt Explizite Analyse Graphensuche Explicit Model Checking Symbolische Analyse Symbolic Model Checking Überprüfung, ob gewisse Zustände erreichbar sind entspricht Model Checking von Sicherheitseigenschaften (Safety) klassisch: Symbolic MC für HW, explicit MC für SW (Praxis heute: symbolic und explicit für SW+HW) Symbolisches Modell reach 3 Symbolische Erreichbarkeitsanalyse reach 4 für die Theorie reicht explizite Matrix-Darstellung von T Anfangszustand: 0 Übergangsmatrix 0 1 2 3 4 5 6 7 0 0 0 1 0 1 0 0 0 1 0 0 0 1 0 1 0 0 2 0 0 0 0 1 0 1 0 3 0 0 0 0 0 1 0 1 4 1 0 0 0 0 0 1 0 5 0 1 0 0 0 0 0 1 6 1 0 0 0 0 0 0 0 7 0 1 0 1 0 0 0 0 Symbolische Travesierung des Zustandraumes von einer Menge von Zuständen werden in einem Schritt die Nachfolger bestimmt Nachfolger werden symbolisch berechnet und repräsentiert legt Breitensuche nahe im allgemeinen schon nicht berrechenbar in der Praxis ist T in einer Modellierungs- oder Programmiersprache beschrieben initial state = 0; next state = (current state + 2 * (bit + 1)) % 8; Resultat ist eine symbolische Repräsentation aller erreichbaren Zustände z.b. 0 <= state && state < 8 && even (state) symbolische Repräsentation kann exponentiell kompakter sein

Parallele Komposition reach 5 Zustandsexplosion reach 6 Multiplaktives Eingehen der Anzahl Komponentenzustände in Systemzustandszahl: symbolische Repräsentation hat oft einen Paralleloperator S G = S K1 S K2 z.b. Produkt von Automaten, der einzelne Automat wird explizit repräsentiert G = K 1 K 2 Komponenten des Systems werden seperat programmiert process A begin P1 end process B begin P2 end; offensichtlich bei sequentieller Hardware: n-bit Zähler läßt sich mit O(n) Gattern implementieren, hat aber 2 n Zustände Hierarchische Beschreibungen führen zu einem weiteren exponentiellen Faktor Additives Eingehen der Größe der symbolischen Komponenten Programmgrösse des Systems: K 1 + K 2, bzw. P1 + P2 bei Software noch komplexer: in der Theorie nicht primitiv rekursiv (siehe Ackerman-Funktion) in der Praxis ist der Heap (dynamisch allozierte Objekte) das Problem Explizite vs Symbolische Modellierung reach 7 Explizite Analyse durch Tiefensuche reach 8 explizite Repräsentation reduziert sich auf Graphensuche suche erreichbare Zustände von den Anfangszuständen linear in der Grösse aller Zustände symbolische Modellierung Berechnung der Nachfolger eines Zustandes ist das Hauptproblem on-the-fly Expansion von T für einen konkreten erreichten Zustand damit Vermeidung der Berechnung von T für unerreichbare Zustände alternativ symbolische Berechnung/Repräsentation der Nachfolgezustände Markiere besuchte Zustände Implementierung abhängig davon, ob Zustände On-the-fly generiert werden Unmarkierte Nachfolger besuchter Zustände kommen auf den Suchstack Nächster zu bearbeitender Zustand wird oben vom Stack genommen Falls Fehlerzustand erreicht wird hat man einen Weg dorthin der Pfad bzw. Fehlertrace findet sich auf dem Stack der Einfachheit wegen breche die Suche ab

Tiefensuche Depth First Search (DFS) reach 9 recursive_dfs_aux (Stack stack, State current) if (marked (current)) return; mark (current); stack.push (current); if (is_target (current)) stack.dump_and_exit (); /* target reachable */ forall successors next of current recursive_dfs_aux (stack, next); stack.pop (); Erreichbarkeits-Analyse mit DFS recursive_dfs () Stack stack; reach 10 forall initial states state recursive_dfs_aux (stack, state); /* target not reachable */ Abbruch beim Erreichen des Zieles ist schlechter Programmierstil! Effizienz verlangt nicht-rekursive Variante Markieren sollte durch Hashen ersetzt werden damit Zustände on-the-fly erzeugt werden können Stackoverflow bei Rekursion reach 11 Nicht-Rekursive DFS Korrekte Version reach 12 #include <stdio.h> void f (int i) printf ("%d\n", i); f (i + 1); int main (void) f (0); dieses C-Programm stürzt nach wenigen Rekursionsschritten ab non_recursive_dfs_aux (Stack stack) while (!stack.empty ()) current = stack.pop (); if (is_target (current)) dump_family_line_and_exit (current); forall successors next of current if (cached (next)) continue; cache (next); stack.push (next); next.set_parent (current);

Vorbereitung Nicht-Rekursive DFS reach 13 DFS 1. Fehlerhafte Version reach 14 non_recursive_dfs () Stack stack; forall initial states state stack.push (state); non_recursive_dfs_aux (stack); /* target not reachable */ non_recursive_buggy_dfs_aux (Stack stack) while (!stack.empty ()) current = stack.pop (); if (is_target (current)) dump_family_line_and_exit (current); if (cached (current)) continue; cache (current); forall successors next of current stack.push (next); next.set_parent (current); DFS 2. Fehlerhafte Version reach 15 Breitensuche Breadth First Search (BFS) reach 16 non_recursive_but_also_buggy_aux (Stack stack) while (!stack.empty ()) current = stack.pop (); forall successors next of current if (cached (next)) continue; cache (next); stack.push (next); next.set_parent (current); if (is_target (next)) dump_family_line_and_exit (next); bfs_aux (Queue queue) while (!queue.empty ()) current = queue.dequeue (); if (is_target (current)) dump_family_line_and_exit (current); forall successors next of current if (cached (next)) continue; cache (next); queue.enqueue (next); next.set_parent (current);

Vorbereitung Nicht-Rekursive BFS reach 17 DFS versus BFS reach 18 bfs () Queue queue; forall initial states state cache (state); queue.enqueue (state); bfs_aux (queue); Tiefensuche einfach rekursiv zu implementieren (was man aber sowieso nicht sollte) erlaubt Erweiterung auf Zyklenerkennung Liveness Breitensuche /* target not reachable */ erfordert nicht-rekursive Formulierung kürzeste Gegenbeispiele findet immer kürzesten Pfad zum Ziel Vorwärts- versus Rückwärtstraversierung reach 19 Globale versus Lokale/On-The-Fly Analyse reach 20 Vorwärtstraversierung bzw. Vorwärtsanalyse Nachfolger sind die als nächstes zu betrachtenden Zustände analysierte Zustände sind alle erreichbar Rückwärtstraversierung bzw. Rückwärtsanalyse Vorgänger sind die als nächstes zu betrachtenden Zustände manche untersuchten Zustände können unerreichbar sein meist nur in Kombination mit symbolischen Repräsentationen von Zuständen symbolische Darstellungen bei Rückwärtsanalyse meist zufällig und komplex manchmal terminiert Rückwärtsanalyse in wenigen Schritten Globale Analyse anwendbar auf Rückwärts- oder Vorwärtstraversierung arbeitet mit allen globalen Zuständen (inklusive nicht erreichbaren) Kombination mit expliziter Analyse skaliert sehr schlecht Lokale oder On-The-Fly Analyse arbeitet nur auf den erreichbaren Zuständen generiert und analysiert diese on-the-fly ausgehend von Anfangszuständen gemischte Vorgehensweise möglich: bestimme vorwärts erreichbare Zustände, darauf dann globale Analysen

Beispiel zu Global versus Lokal reach 21 Speicherverbrauch reach 22 process A int count; run() while (true) if (b.count!= this.count) count = (count + 1) % (1 << 20); process B int count; run() while (true) if (a.count == this.count) count = (count + 1) % (1 << 20); A a; B b; bei globaler Analyse wird jedem Zustand ein Markierungsbit hinzugefügt bool cached (State state) return state.mark; void cache (State state) state.mark = true; bei globaler Analyse ohne On-the-fly Zustandsgenerierung 2 64 = 2 32 2 32 mögliche Zustände können nicht vorab alloziert werden bei lokaler Analyse mit On-the-fly Zustandsgenerierung 2 2 20 = 2097152 erreichbare Zustände ergeben 8-fache Menge an Bytes 16 MB (plus Overhead diese zu Speichern, < Faktor 2) Zustandsspeicherung bei On-The-Fly Expliziter Analyse reach 23 Hashtabellen zur Zustandsspeicherung reach 24 Anforderungen an Datenstruktur zur Speicherung von Zuständen: besuchte Zustände müssen markiert/gespeichert werden (cache) pro neuem Zustand eine Einfügeoperation neue Nachfolger werden auf schon besucht getestest (cached) pro Nachfolger eine Suche auf Enhaltensein Laufzeit kann als konstant angesehen werden in der Theorie weit komplexere Analyse notwendig gute Hashfunktion ist das Wesentliche Randomisierung bzw. Verteilung der Eingabebits auf Hashindex Alternative Implementierungen: Bit-Set: pro möglichem Zustand ein Bit (im Prinzip wie bei globaler Analyse) Suchbäume: Suche/Einfügen logarithmisch in Zustandsanzahl Anpassung der Grösse der Hashtabelle: entweder durch dynamische (exponentielle) Vergrösserung oder Maximierung und Anpassung an den verfügbaren Speicher Hashtabelle: Suche/Einfügen konstant in Zustandsanzahl

Hashtabelle mit Collision-Chains reach 25 table 8 count_table == 5 0 Schlechte Hashfunktion für Strings in Compilern reach 26 1 2 3 17 7 9 3 unsigned bad_string_hash (const char * str) const char * p; unsigned res; size_table == (1 << 2) res = 0; unsigned hash (unsigned data) return data; struct Bucket * find (unsigned data) unsigned h = hash (data); h &= (size_table - 1);... for (p = str; *p; p++) res += *p; Schlechte Hashfunktion für Strings in Compilern reach 27 unsigned very_bad_string_hash (const char * str) const char * p; unsigned res; res = 0; for (p = str; *p; p++) res = (res << 4) + *p; Klassische Hashfunktion für Strings in Compilern [Dragonbook] reach 28 unsigned classic_string_hash (const char *str) unsigned res, tmp; const char *p; res = 0; for (p = str; *p; p++) tmp = res & 0xf0000000; /* unsigned 32-bit */ res <<= 4; res += *p; if (tmp) res ^= tmp >> 28;

Analyse der Klassischen Hashfunktion für Strings reach 29 empirisch sehr gute Randomisierung bei Bezeichnern von Programmiersprachen relative Anzahl Kollisionen als Maßzahl für die Qualität schnell: maximal 4 logisch/arithmetische Operationen pro Zeichen bei längeren Strings von 8 Zeichen und mehr gute Bit-Verteilung Überlagerung der 8-Bit Kodierungen der einzelnen Zeichen (man beachte aber die ASCII Kodierung) Klustering bei vielen kurzen Strings (z.b. in automatisch generiertem Code) n1,..., n99, n100,..., n1000 Bessere Hashfunktion für Strings reach 30 static unsigned primes [] = 2000000011, 2000000033,... ; #define NUM_PRIMES (sizeof (primes) / sizeof (primes[0])) unsigned primes_string_hash (const char * str) unsigned res, i; const char * p; i = 0; res = 0; for (p = str; *p; p++) res += *p * primes[i++]; if (i >= NUM_PRIMES) i = 0; Gute Hashfunktion für Zustände reach 31 CRC basierte Hashfunktion für Strings reach 32 static unsigned primes [] = 2000000011, 2000000033,... ; #define NUM_PRIMES (sizeof(primes)/sizeof(primes[0])) unsigned hash_state (unsigned * state, unsigned words_per_state) unsigned res, i, j; res = 0; i = 0; for (j = 0; j < words_per_state; j++) res += state[j] * primes [i++]; if (i >= NUM_PRIMES) i = 0; #define CRC_POLYNOMIAL 0x82F63B78 static unsigned crc_hash_bit_by_bit (const char * str) unsigned const char * p; unsigned res = 0; int i, bit; for (p = str; *p; p++) res ^= *p; for (i = 0; i < 8; i++) bit = res & 1; res >>= 1; if (bit) res ^= CRC_POLYNOMIAL;

Analyse der Hashfunktion für Zustände reach 33 Super-Trace Algorithmus reach 34 lässt sich leicht parametrisieren durch verschiedene Primzahlen oder Anfangspositionen heutzutage ist Integer-Multiplikation sehr schnell Probleme mit expliziter vollständiger Zustandsraum-Exploration: 1. wegen Zustandsexplosion oft zu viele Zustände 2. ein einziger Zustand braucht oft schon viel Platz (dutzende Bytes) durch mehr Platz für die Multiplizierer auf der ALU und durch Vorhandensein mehrerer Integer-Functional Units was noch fehlt ist Anpassung an Tabellengröße Ideen des Super-Trace Algorithmus: 1. behandle Zustände mit demselben Hash-Wert als identisch 2. speichere nicht die Zustände, sondern Hash-Werte erreichter Zustände bei Zweierpotenzgröße: einfache Maskierung Super-Trace wird auch Bit-State-Hashing genannt sonst Primzahlgröße und Anpassung durch Modulo-Bildung Super-Trace Hashtabelle als Bit-Set reach 35 Analyse des Super-Trace Algorithmus reach 36 table 0 1 8 Vorteile: 1 2 3 1 0 1 17 7 9 3 drastische Reduktion des Speicherplatzes auf ein Bit pro Zustand Reduktion um mindestens 8 mal Größe des Zustandes in Bytes Anpassung der Größe der Hash-Tabelle/Bit-Set an vorhandenen Speicher size_table == (1 << 2) Ausgabe einer unteren Schranke der Anzahl besuchten Zustände Parametrisierung der Hash-Funktion erlaubt unterschiedliche Exploration Falls Zustand mit gleichem Hash-Wert früher schon besucht wurde, dann gilt momentaner Zustand als besucht! Nachteile: bei nicht-kollisionsfreier Hash-Funktion (der Normalfall) Verlust der Vollständigkeit nur vage Aussage über Abdeckung möglich

Zwei Hashfunktionen beim Supertrace reach 37 Hash-Compact reach 38 berechne zwei Hashwerte mit unterschiedlichen Hashfunktionen Speichern von Zuständen durch Eintrag jeweils eines Bits in zwei Hashtabellen Zustand wird als schon erreicht angenommen gdw. beide Bits gesetzt z.b. h 1,h 2 :Keys 0,...,2 32 1 statt n Bits zu setzen kann man auch einfach den n-bit Hash-Wert speichern bei 256 MB = 2 28 Bytes Platz für die Hashtabelle bis zu 2 26 Hashwerte/Zustände (32-Bit Hashfunktion h:keys 0,...,2 32 1, 4 Bytes Hashwert pro Zustand) beliebige Varianten ergeben ungefähr folgendes Bild: zwei Hashtabellen mit jeweils 2 32 Bits = 2 29 Bytes = 512 MB 100% Coverage lässt sich auch leicht auf n Hashfunktionen ausbauen n = 4 mit 2 GB Speicher für Hashtabellen ist durchaus realistisch 50% n = 2 Hash Compact vollständig vergl. Parameteriesierung der Hashfunktion basierend auf Primzahlmultiplikation 0% Speicher 2^10 2^15 2^20 2^25 2^30 2^35 (schematisch nach [Holzmann 1998], 427567 erreichbare Zustände, 1376 Bits) Best-Case-Analyse des Speicher-Verbrauchs reach 39 Idealisierte Best-Case Annahmen: verwendete Hash-Funktion ist möglichst kollisionsfrei Hashtabelle wird solange wie möglich ohne Kollisionen gefüllt m Speicherplatz in Bits, s Zustandsgrösse in Bits, r erreichbare Zustände coverage n=2 (m) = m r 2 coverage hashcompact (m) = m r w coverage complete (m) = m r s w = Wordgröße in Bits,z.B. 32 Bit,w log 2 r (im vorigen Schaubild ist die x-achse logarithmisch dargestellt) in der Praxis gibt es Kollisionen und die unvollständigen brauchen mehr Speicher