830 likes | 1.1k Views
Hashing. Plan. Hashing Hashfunktioner Kollisionsstrategier Effektivitet Hashing i Javas biblioteker Prioritetskøer Binær hob Anvendelser: heapsort, ekstern sortering. Hashing søgning ved nøgletransformation.
E N D
Plan • Hashing Hashfunktioner Kollisionsstrategier Effektivitet Hashing i Javas biblioteker • Prioritetskøer Binær hob Anvendelser: heapsort, ekstern sortering
Hashingsøgning ved nøgletransformation • Med balancerede træer foretages O(log2N) sammenligninger af nøgler. • Men er O(log2N) den bedst opnåelige kompleksitet? • Nej. • Hvordan opnås lavere kompleksitet? • Med hashing, en teknik, der benytter transformationer af nøgler til direkte at kunne referere til poster i en tabel. Med hashing opnås under gunstige omstændigheder kompleksitet O(1).
h: K I Grundlæggende ide Gem hver post i en tabel på en plads, der er bestemt af postens nøgle. En hashfunktion er en metode til beregning af et tabelindeks ud fra en nøgle. Matematisk udtrykt: En hashfunktion er en afbildning af en mængde af nøgler på et indeksinterval. Ideelt set burde to forskellige nøgler afbildes på to forskellige indices. At to eller flere nøgler afbildes på samme indeks kaldes en kollision. En kollisionsstrategi er en algoritme til håndtering af kollisioner.
Tid/plads-afvejning(trade off) • Ingen pladsbegrænsninger: benyt nøglen som indeks (triviel hashfunktion) • Ingen tidsbegrænsninger: benyt sekventiel søgning • Hvis der er begrænsninger på både plads og tid: benyt hashing
Hashingteknikken • Lad h betegne hashfunktionen. • Indsættelse: En post med nøgle K placeres på indeks h(K), med mindre der i forvejen er en post på dette indeks. Så må posten placeres på anden måde (hvordan - afhænger af kollisionsstrategien). • Søgning: Ved søgning efter en post med nøgle K, undersøges først posten på indeks h(K). Hvis denne indeholder K, afsluttes søgningen med succes. Ellers fortsætter søgningen (hvordan - afhænger af kollisionsstrategien).
“Gode” hashfunktioner • Kollisioner bør så vidt muligt undgås. Hashfunktionen bør sprede funktionsværdierne jævnt på hele indeksintervallet. • Hashfunktionen bør være beregningsmæssigt billig.
Konstruktion af hashfunktioner(Korte nøgler) • Korte nøgler(nøgler, der kan være i et maskinord): • Betragt nøglen som et heltal og beregn • h(K) = K mod M (i Java: K % M) • hvor M er tabelstørrelsen. • h(K) [0;M-1]
Eksempel med korte nøgler • Nøgler bestående af 4 ascii-tegn, tabelstørrelse 101. • ascii ab c d • hex 6 16 2 6 3 6 4 • bin 01100001011000100110001101100100 0x61626364 = 1633831724 16338831724 % 101 = 11 Nøglen "abcd" hasher til 11. 0x64636261 = 1684234849 1684234849 % 101 = 57 Nøglen "dcba" hasher til 57. 0x61626263 = 1633837667 1633837667 % 101 = 57Nøglen "abbc" hasher også til 57. Kollision!
Konstruktion af hashfunktioner(Lange nøgler) • Lange nøgler(nøgler, der ikke kan være i et maskinord): • Betragt nøglen som et langt heltal og beregn • h(K) = K mod M • hvor M er tabelstørrelsen. • Altså i princippet som for korte nøgler.
Eksempel med lange nøgler • Eksempel med 4 tegn. Men metoden virker også for vilkårligt lange nøgler. • Benyt Horners regel: • 0x61626364 = • 97*2563 + 98*2562 + 99*2561 + 100= • ((97*256 + 98)*256 + 99)*256 + 100 • Tag modulo efter hver addition for at undgå aritmetisk overløb: • (97*256 + 98 = 24930) % 101 = 84 • (84*256 + 99 = 21603) % 101 = 90 • (90*256 + 100 = 23140) % 101 = 11
Tabelstørrelsen • Vælg tabelstørrelsen som et primtal. Hvorfor? • I eksemplet før havde vi "abcd" = 0x61626364 = • 97*2563 + 98*2562 + 99*2561 + 100 • Hvis tabelstørrelsen vælges til 256, vil kun det sidste tegn have betydning ved beregning af h. • En simpel måde at sikre sig, at alle tegn bidrager, er at vælge tabelstørrelsen som et primtal.
int hash(String key, int tableSize) { • int h = 0; • for (int i = 0; i < key.length(); i++) • h = h*37 + key.charAt(i); • h %= tableSize; • return h < 0 ? h + tableSize : h; • } Eksempel på hashfunktion • int hash(String key, int tableSize) { • int h = 0; • for (int i = 0; i < key.length(); i++) • h = (h*37 + key.charAt(i)) % tableSize; • return h; • } For at sprede værdierne bedre er 256 erstattet med 37. Modulo-beregningerne undervejs kan undværes:
Javas implementering af hashCode i String(Java 1.1) public int hashCode() { int h = 0; int off = offset; char val[] = value; int len = count; if (len < 16) { for (int i = len; i > 0; i--) h = (h * 37) + val[off++]; } else { // only sample some characters int skip = len / 8; for (int i = len; i > 0; i -= skip, off += skip) h = (h * 39) + val[off]; } return h; }
Javas implementering af hashCode i String(Java 1.2) • publicint hashCode() { • int h = 0; • int off = offset; • char val[] = value; • int len = count; • for (int i = 0; i < len; i++) • h = 31*h + val[off++]; • return h; • }
Hvis M være tabelstørrelsen, hvor mange indsættelser kan da i gennemsnit foretages, før der opstår en kollision? M 100 12 365 24 1000 40 10000 125 100000 396 1000000 2353 Hyppigheden af kollisioner • Fødselsdagsparadokset: Hvor mange personer skal være forsamlet i et selskab, for at der er mere en 50% sandsynlighed for at mindst to personer har fødselsdag på samme dato? • Svar: 24.
Mulighed 2 (åben adressering): Sørg for at N < M: Læg kolliderende nøgler i tabellen. Kollisionstrategier • Antal poster: N • Tabelstørrelse: M • Mulighed 1 (separat kædning): • Tillad N > M: • Læg nøgler, der hasher til samme indeks, ind i en liste (med cirka N/M nøgler per liste).
Separat kædning • Simpel, praktisk og meget udbredt metode. • Metode: M hægtede lister - en for hver tabelindgang. • 0: * • 1: L A W * • 2: M X * • 3: N C * • 4: * • 5: E P * (M = 11) • 6: * (N = 14) • 7: G R * • 8: H S * • 9: I * • 10: * Nedbringer den gennemsnitlige søgetid med en faktor M i forhold til sekventiel søgning.
public interface HashTable { void put(Object key, Object value); Object get(Object key); void remove(Object key); } public class SeparateChainingHashTable implements HashTable { private HashEntry[] array; ... } Implementering af separat kædning
class HashEntry { HashEntry(Object k, Object v, HashEntry n) { key = k; value = v; next = n; } Object key, value; HashEntry next; }
void put(Object key, Object value) { int pos = Math.abs(key.hashCode()) % array.length; for (HashEntry e = array[pos]; e != null; e = e.next) if (key.equals(e.key)) return; array[pos] = new HashEntry(key, value, array[pos]); } Object get(Object key) { int pos = Math.abs(key.hashCode()) % array.length; for (HashEntry e = array[pos]; e != null; e = e.next) if (key.equals(e.key)) return e.value; return null; }
void remove(Object key) { int pos = Math.abs(key.hashCode()) % array.length; for (HashEntry e = array[pos], prev = null; e != null; prev = e, e = e.next) if (key.equals(e.key)) { if (prev != null) prev.next = e.next; else array[pos] = e.next; return; } } prev e e.next
Effektivitet af separat kædning • Indsættelse: N/M (i gennemsnit) • Mislykket søgning: N/M (i gennemsnit) • Succesfuld søgning: 1 + N/M/2 (i gennemsnit) • Værste tilfælde: N • Hvis listerne holdes sorteret: Tid for mislykket søgning mindskes til N/M/2 • Tid for indsættelse øges til N/M/2
Åben adresseringLineær prøvning • Åben adressering: Ingen hægter. Alle poster opbevares i tabellen. • Lineær prøvning: Start lineær søgning fra hashpositionen, og stands ved den søgte post eller en tom position. • Stadig konstant søgetid, hvis M er tilstrækkelig stor.
0 1 2 3 4 5 6 Et simpelt eksempel • Mængden af nøgler er alfabetets store bogstaver. Der er ingen information tilknyttet nøglerne. Tabelstørrelsen er 7. • h(K) = (K’s nummer i alfabetet) mod 7 = (K - ‘A’ + 1) % 7
N 0 1 2 C 3 K 4 S 5 6 • Tabel efter indsættelse af nøglerne • C, K, N, S h(N) = 14 % 7 = 0 h(C) = 3 % 7 = 3 h(K) = 11 % 7 = 4 h(S) = 19 % 7 = 5
N 0 1 2 C 3 h(Y) = 25 % 7 = 4 K 4 S 5 6 N 0 1 2 C 3 K 4 S 5 Y 6 Placering efter 3 forsøg Indsættelse af Y giver kollision
N 0 1 2 C 3 h(D) = 4 % 7 = 4 K 4 S 5 Y 6 N 0 D 1 2 C 3 K 4 S 5 Y 6 Placering efter 5 forsøg (med “wrap around”) Indsættelse af D Bemærk: tabellen må ikke blive fuld!
Implementering af åben adressering abstract class ProbingHashTable implements HashTable { ProbingHashTable() { array = new HashEntry[size]; } void put(Object key, Object value) { ... } Object get(Object key) { ... } void remove(Object key) { ... } abstract protected int findPos(Object key); private HashEntry[] array; private int currentSize; }
class HashEntry { HashEntry(Object k, Object v) { key = k; value = v; } Object key, value; boolean isActive = true; } Object get(Object key) { int pos = findPos(key); if (array[pos] == null || !array[pos].isActive) return null; return array[pos].value; } void remove(Object key) { int pos = findPos(key); if (array[pos] == null) return; array[pos].isActive = false; }
void put(Object key, Object value) { int pos = findPos(key); array[pos] = new HashEntry(key, value); if (++currentSize < array.length / 2) return; // rehash HashEntry[] oldArray = array; array = new HashEntry[nextPrime(2 * oldArray.length)]; currentSize = 0; for (int i = 0; i < oldArray.length; i++) if (oldArray[i] != null && oldArray[i].IsActive) put(oldArray[i].key, oldArray[i].value); } Tidsforbruget for nextPrime er O(√N * logN). Ved rehash er simpel kopiering utilstrækkelig.
class LinearProbingHashTable extends ProbingHashTable { protected int findPos(Hashable key) { int pos = Math.abs(key.hashCode()) % array.length; while (array[pos] != null && !array[pos].key.equals(key)) pos = (pos + 1) % array.length; return pos; } } Klassen LinearProbingHashTable
De præcise udtryk er: forsøg ved mislykket søgning, og forsøg ved succesfuld søgning, hvor = N/M betegner fyldningsgraden. Effektivitet af lineær prøvning • Tyndt besat tabel: ligesom separat kædning. • Lineær prøvning bruger gennemsnitligt færre end 5 forsøg for en hashtabel, der er mindre end 2/3 fuld.
a a Mislykket søgning Succesfuld søgning Effektivitetskurver for lineær prøvning
Antag at alle positioner [i:j] indeholder poster, mens i-1, j+1 og j+2 er tomme. i-1 j+1 j+2 Argumentation for tendens til klyngedannelse Så vil chancen for, at en ny post placeres på position j+1 være lig med chancen for, at en ny post skal placeres i intervallet [i:j+1]. For at den nye post placeres på j+2, skal dens hashværdi derimod være præcis j+2.
Klyngedannelse • Uheldigt fænomen. • Lange klynger har en tendens til at blive længere. • Søgelængen vokser drastisk, efterhånden som tabellen fyldes. • Lineær prøvning er for langsom, når tabellen bliver 70-80% fuld.
Kvadratisk prøvning(reducerer risikoen for klyngedannelse) Prøvningssekvens: Lineær prøvning: pos, pos + 1, pos + 2, pos + 3, ... Kvadratisk prøvning: pos, pos + 12, pos + 22, pos + 32, ... Kvadrering kan undgås: Lad Hi betegne den i’te position (H0 er startpositionen). Idet Hi-1 = pos + (i - 1)2 = pos + i2 - 2i + 1 = Hi - 2i + 1 fås Hi = Hi-1 + 2i - 1
class QuadraticProbingTable extends ProbingHashTable { protected int findPos(Object key) { int pos = key.hashCode() % array.length; int i = 0; while (array[pos] != null && !array[pos].element.equals(key)) pos = (pos + 2 * ++i - 1) % array.length; return pos; } } Implementering af kvadratisk prøvning Det kan bevises, at Hvis fyldningsgraden er mindre end 0.5, er indsættelse altid mulig, og under indsættelsen vil ingen indgang blive prøvet mere end én gang.
Dobbelt hashing Undgå klyngedannelse ved at bruge en ekstra hashfunktion. I stedet for som i lineær prøvning at prøve successive indgange, foretages prøvningen med en fast afstand bestemt af den anden hashfunktion. Derved øges sandsynligheden for at finde tomme indgange ved indsættelse.
class DoubleHashTable extends ProbingHashTable { protected int findPos(Object key) { int pos = Math.abs(key.hashCode()) % array.length; while (array[pos] != null && !array[pos].element.equals(key)) pos = (pos + key.hash2()) % array.length; return pos; } } Implementering af dobbelt hashing
Krav til den andenhashfunktion • Den bør ikke returne 0. • Den skal altid returnere værdier, der er primiske med M. Kan opnås ved at vælge M som et primtal og lade h2(k) < M for ethvert k. • Den skal være forskellig fra den første. • En simpel og hurtig metode er h2(k) = 8 - k % 8 (k % 8 er de sidste 3 bit af k)
De præcise udtryk er: forsøg ved mislykket søgning, og forsøg ved succesfuld søgning, hvor er fyldningsgraden. Effektivitet af dobbelt hashing • Dobbelt hashing bruger gennemsnitligt færre forsøg end lineær prøvning. Færre end 5 forsøg ved en søgning, når tabellen højst er 80% fuld, og færre end 5 forsøg ved en succesfuld søgning, når tabellen højst er 99% fuld.
dobbelt hashing Succesfuld søgning Mislykket søgning lineær prøvning a a Mislykket søgning Succesfuld søgning a a Dobbelt hashing contra lineær prøvning
Fordele ved separat kædning • Idiotsikker metode (bryder ikke sammen) • Antallet af poster behøver ikke at være kendt på forhånd • Sletning er simpel • Tillader ens nøgler
Hashing i Javaclass HashMap public class HashMap { public HashMap(int initialCapacity, float loadFactor); public HashMap(int initialCapacity); public HashMap() { this(101, 0.75); } public Object put(Object key, Object value); public Object get(Object key); public Object remove(Object key); public int size(); public boolean isEmpty(); public boolean containsKey(Object key); public boolean contains(Object value); public void clear(); public Set keySet(); public Collection values(); public Set EntrySet(); }
class Entry { Object key; Object value; Entry next; int hash; } public Object get(Object key) { int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % table.length; for (Entry e = table[index]; e != null; e = e.next) if (e.hash == hash && e.key.equals(key)) return e.value; return null; } Metoden get(benytter separat kædning) private Entry[] table; private int count; private int threshold; private float loadFactor;
public Object put(Object key, Object value) { int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % table.length; for (Entry e = tab[index]; e != null; e = e.next) if (e.hash == hash && e.key.equals(key)) { Object old = e.value; e.value = value; return old; } if (count >= threshold) { rehash(); return put(key, value); } Entry e = new Entry(); e.hash = hash; e.key = key; e.value = value; e.next = table[index]; table[index] = e; count++; return null; } Metoden put
protected void rehash() { int oldCapacity = table.length; Entry[] oldTable = table; int newCapacity = oldCapacity * 2 + 1; Entry newTable[] = new Entry[newCapacity]; threshold = (int) (newCapacity * loadFactor); table = newTable; for (int i = oldCapacity; i-- > 0; ) { for (Entry old = oldTable[i]; old != null; ) { HashtableEntry e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = newTable[index]; newTable[index] = e; } } } Metoden rehash
public Object remove(Object key) { int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % table.length; for (Entry e = tab[index], prev = null; e != null; prev = e, e = e.next) { if (e.hash == hash && e.key.equals(key)) { if (prev != null) prev.next = e.next; else table[index] = e.next; count--; return e.value; } } return null; } Metoden remove
Grunde til ikke at bruge hashing • Hvorfor bruge andre metoder? • Der er ingen effektivitetsgaranti • Hvis nøglerne er lange, kan hashfunktionen være for kostbar at beregne • Bruger ekstra plads • Understøtter ikke sortering