Iteration versus Rekursion: Difference between revisions

From Alda
Jump to navigationJump to search
mNo edit summary
(Removing all content from page)
Line 1: Line 1:
== Hashtabelle mit linearer Verkettung ==
Pessimistischer Ansatz: Kollisionen treten häufig auf, deshalb wird unter jedem
Hashindex gleich eine Liste angelegt, in der Einträge aufgenommen werden können.


===Implementation in Python===
''HashNode'' ist eine Hilfsdatenstruktur, die Schlüssel und Wert speichert
und mit hilfe von ''next'' eine verkettete Liste darstellt
  class HashNode:
      def __init__(self,key,data,next):
          self.key = key
          self.data = data
          self.next = next #Verkettung!
Die eigentliche Hashtabelle wir in der Klasse ''HashTable'' implementier:
  class HashTable:
      def __init__(self):
        self.capacity = ... #Geigneter Wert
        self.size = 0
        self.array = [None]*self.capacity
In Python wird der Zugriffsoperator ''[ ]'' für eine Datenstruktur wie folgt (innerhalb einer Klasse) implementiert:
  def __setitem__(self, key, value)
So dass im Programmtext dann folgender Syntax möglich ist: <tt>a[key] = value</tt>
Genauso wir die Zuweisung <tt>value=a[key]</tt> wie folgt umgesetzt
  def __getitem__(self, key, value)
Implementierung der <tt>__setitem__</tt> Funktionen in der <tt>HashTable</tt> Klasse:
  def __setitme__(self, key, value):
      index = hash(key) % self.capacity
      node = self.array[index]
      while node is not None:
          if node.key == key:
              #Element key ist schon in der Tabelle
              #Überschreibe die Daten mit dem neuen Wert
              node.data = value
              return
          #Kollision des Hashwerts, probiere nächsten Key aus
          node = node.next
      #Kein Element hatte den richtigen Schlüssel.
      #==>Es gibt diesen Schlüssel noch nicht
      #Füge also ein neues Element in die Hashtabelle ein
     
      self.array[index] = HashNode(key, value, self.array[index])
      #Der alte Anfang der List wurde der Nachfolger vom neue eingefügten
      #ersten Element
     
      size+=1
Und die Implementierung der <tt>__getitem__</tt> Funktionen:
  def __getItem__(self, key):
      index = hash(key) % self.capacity
      node = self.array[index]
      while node is not None:
          if node.key == key #Gefunden!
              return Node.data
          node = node.next
      raise KeyError(key)
==== Wahl der Kapazität ====
In der C++ Standardbibliothek wird typischerweise ein Hash mit Hilfe der linearen Kette
imlementiert. Dabei wird <tt>capcity</tt> immer als ''Primzahl'' gewählt, wobei sich aufeinanderfolgende Kapazitäten immer ungefähr verdoppeln:
  53, 97, 193, 398, 769, ...
Das hat zur Folge, dass <tt>h % self.capacity</tt> ''alle'' Bits von h benutzt (Eigenschaft aus der Zahlentheorie)
== Hashtabelle mit offener Adressierung (offenes Hashing) ==
Optimistischer Ansatz: Kollisionen werden nicht so häufig auftreten.
=== Idee ===
Wenn <tt>array[index]</tt> durch Kollision bereits vergeben ist, probiere einen
anderen Index aus.
* Das Array enthält pro Element höchstens ein (key,value)-Paar
* Das Array muss stets mindestens ''einen'' freien Platz haben (sonst gäbe es eine  Endlosschleife). Es gilt immer <tt>self.size < self.capacity</tt>. Dies galt für die vorige Hash Implementation nicht.
=== Vorgehen bei Kollisionen ===
==== Sequentielles Suchen ====
Probiere den nächsten Index: <tt>index = index+1 % capacity</tt>
* Vorteil: einfach
* Nachteil: Clusterbildung
Clusterbildung heißt, dass sich größere zusammenhängende Bereiche bilden die belegt sind, unterbrochen von Bereichen die komplett frei sind. Beim Versuche des Einfügens eines Elements an einen Platz, der schon belegt ist, muss jetzt das ganze Cluster sequentiell durchlaufen werden, bis ein freier Platz gefunden wird. Damit entspricht die Komplexität der Suche der mittleren Länge der belegten Bereiche, was sich entsprechend in einer langsamen Suche widerspiegelt.
==== Doppeltes Hashing ====
Bestimme einen neuen Index (bei Kollisionen) durch eine ''2. Hashfunktion''.
Das doppelte Hashing wird typischerweise in der Praxis angewendet und liegt auch der Python Implementierung des Datentyps ''Dictionary'' (Syntax <tt>{'a':1, 'b':2, 'c':3}</tt> zugrunde.
Eine effiziente Implementierung dieses Datentyps ist für die Performance der Skriptsprache Python extrem wichtig, da z.B. beim Aufruf einer Funktion der auszuführunde Code in einem Dictionary unter dem Schlüssel ''Funktionsname'' nachgeschlagen wird oder die Werte lokaler Variablen innerhalb einer Funktion ebenfalls in einem Dictionary zu finden sind.
Für die Implementierung in Python werden wieder die obigen Klassen <tt>HashNode</tt> und <tt>HashTable</tt> benötigt, es folgen die angepassten Implementationen von <tt>__setitem</tt> und <tt>__getitem__</tt>:
  def __setitem__(self, key, value):
      h = hash(key)
      index = h % self.capacity
      while True:
          if self.array[index] is None or self.array[index].key is None
              #das Feld ist frei (1. Abfrage)
              #oder das feld ist als frei markiert (2. Abfrage)
              self.array[index] = HashNode(key, value)
              self.size +=1
              return
          if self.array[index].key == key:
              #Es gibt diesen Schlüssel schon,
              #überschreibe die Daten
              self.array[index].data = value
          #Letzter Fall: Kollision
          index = (5*index+1+h) % self.capacity
          h = h >> 5
Die vorgestellte Implementierung orientiert sich an Pythons interner Dictionary Implementierung, der zugehörige Quelltext mit ausführlichem Kommentar) findet sich unter [http://svn.python.org/view/*checkout*/python/trunk/Objects/dictobject.c dictobject.c Python Implementation (SVN)]
===== Komplexität des offenen Hashings =====
* Annahme: uniformes Hashing, das heißt alle Indizes haben gleiche Wahrscheinlichkeit
* Füllstand <math>\alpha = \frac{\text{size}}{\text{capacity}}</math>
* '''Erfolglose Suche''' (d.h. es wird entweder ein neues Element eingefügt oder ein <tt>keyError</tt> geworfen): Untere Schranke für die Komplexität ist <math>\Omega\left(\frac{1}{1-\alpha}\right)</math> Schritte bzw. Anzahl von neuen index Berechnungen.
* '''Erfolgreiche Suche''' <math>\Omega\left(\frac{1}{\alpha}\ln\left(\frac{1}{1-\alpha}\right)\right)</math>
{| border="1" cellspacing="0" cellpadding="5" align="center"
! <math>\alpha</math>
! 0.5
! 0.9
|-
| erfolglos
| 2.0
| 10
|-
| erfolgreich
| 1.4
| 2.6
|}
===== Wahl der Kapazität =====
In Python wird <tt>capacity</tt> Aufgrund der obigen Beobachtung so gewählt, dass <math>\alpha \leq 2/3</math>. Falls <math>\alpha</math> größer werden sollte, verdopple die Kapazität und kopiere das alte array in das neue Array (analog zum dynamischen Array)
In Python werden die Kapazitätsgrößen als Zweierpotenzen gewählt, also 4,8,16,32,...,
so dass <tt>h % self.capacity</tt> nur die unteren Bits von <tt>h</tt> benutzt.
== Anwendung von Hashing ==
* Hashtabelle, assoziatives Aray
* Sortieren in linearer Zeit (Übungsaufgabe 6.2)
* Suchen von Strings in Texten
* ...
=== Rabin Karp Algorithmus ===
In Textverarbeitungsanwendungen ist eine häufig benutzte Funktion die ''Search & Replace'' Funktionalität. Die Suche sollte in O(len(text)) möglich sein, aber ein naiver Algorithmus braucht O(len(text)*len(searchstring))
==== Naive Implementierung der Suche ====
  def search(text, s):
      M, N = len(text), len(s)
      for k in range(M-N):
          if s==text[k:k+N]
              return k
      return -1 #nicht gefunden
==== Idee des Rabin Karp Algorithmus ====
Statt Vergleichen <tt>s==text[k:k+N], die O(N) benötigen da N Vergleiche der Buchstaben durchgeführt werden müssen, Vergleiche die Hashs von Suchstring und dem zu untersuchenden Textabschnitt: <tt>hash(s) == hash(text[k:k+N])</tt>. Dabei muss natürlich <tt>hash(s)</tt> nur einmal berechnet werden, wohingegen <tt>hash(text[k:k+N])</tt> immer wieder neu berechnet werden muss. Damit der Vergleich O(1) sein kann, ist es deswegen erforderlich, eine solche Hashfunktion zu haben, die nicht alle Zeichen (das wäre O(N) ) einlesen muss, sondern die vorhergehende Hashfunktion mit einbezieht.
Eine solche Hashfunktion heißt ''Running Hash'' und funktioniert analog zum ''Sliding Mean''.
Die Running Hash Funktion berechnet in O(1) den hash von <tt>text[k+1:k+1+N]</tt> ausgehend vom hash für <tt>text[k:k+N]</tt>.
Idee: Interpretiere den Text als Ziffern in einer base d Darstellung:
<math>h_k = \text{text}[k]\cdot d^{N-1} + \text{text}[k]\cdot d^{N-2} + \cdots + \text{text}[k+N-1]</math>
Für die Basis 10 (Dezimalsystem) ergibt sich also
<math>h_k = \text{text}[k]\cdot {10}^{N-1} + \text{text}[k]\cdot {10}^{N-2} + \cdots + \text{text}[k+N-1]</math>
Daraus folgt
<math>h_{k+1} = 10\cdot h_k - \text{text}[k]\cdot {10}^{N} + \text{text}[k+N]</math>
Und die Komplexität ist O(1).
In der Realität wählt man dann d=32 und benutzt noch an einigen Stellen modulo Operationen, um die Zahlen nicht zu groß werden zu lassen.
==== Implementation ====
  def searchRabinKarp(text, s):
      ht, hs, dN = 0, 0, 1
      M, N = len(text), len(s)
      d, q = 32, <sehr große Primzahl>
     
      #Initialisierung     
      for k in range(N):
          ht = ht*d + ord(text[k]) % q
          #ord() gibt die ASCIInummer des übergebenen Zeichens zurück
          ds = hs*d + ord( s[k] )
          dN = (dN*a) % q
      #Die Variable sind jetzt wie folgt initialisiert:
      #ht = hash(text[0:N])
      #hs = hash(s)
      #dN = (d**N) % q
     
      #Hauptschleife
      k = 0
      while k < M-N:
          if hs == ht and s==text[k:k+N]:
              return k
          ht = (d*ht - dN * ord(text[k]) + dN*q + ord(text[k+M]) ) % q
          k +=1
          return -1

Revision as of 19:28, 4 June 2008