Sid 1
Kapitel 7: Sökning (Hashning)
Hashning
7-1
• Hashning är en teknik för att göra insättningar,
borttag och sökningar i en tabell, på en konstant tid.
• Hashning stödjer ej operationer av ”ordnande
karaktär som t ex:
» finn minsta/största.
» skriv ut posterna i nyckel-ordning.
• I detta avsnitt skall vi diskutera
– Några metoder för att implementera Hash-tabeller.
– Jämföra dessa metoder.
– Påvisa applikationer vid vilka hashning används.
– Jämföra Hash-tabeller med binära sökträd.
Grundläggande idé
7-2
• Den ideala hash-tabellen är som datastruktur bara ett
fält av fix storlek innehållande nycklarna.
• Tabellstorleken är Hsize.
– Konventionen är att låta tabellen gå från 0 till Hsize-1.
• Typiskt är nyckeln en sträng med ett associerat värde.
• Varje nyckel mappas till ett tal i intervallet [0,Hsize-1]
och posten placeras i motsvarande cell. Denna mappning
kallas för en hash-funktion.
• Frågeställningar:
– Hur väljer vi funktion?
– Hur hanteras situationen när två nycklar hashar till
samma värde (kollision)?
– Hur bestäms tabellstorleken?
Sid 2
Kapitel 7: Sökning (Hashning)
Hashfunktionen
7-3
A
B
• Hashfunktionen ”krymper” datatypen A till
datatypen B.
Hash-funktionen.
7-4
• Huvudkrav på hash-funktionen
– snabb att beräkna.
– ge en god spridning, d v s en jämn fördelning av
indexvärden.
• hash(key)= key mod Hsize. (Divisionsmetoden)
– är normalt en godtagbar lösning om
» nycklarna är heltal.
» om inte key har någon speciell oönskad egenskap.
• Oftast önskvärt att tabellstorleken är ett primtal.
– ger funktion enkel att beräkna samt en jämn spridning.
• Vanligen är nycklarna strängar och hash-funktionen
måste väljas med större omsorg.
Sid 3
Kapitel 7: Sökning (Hashning)
Exempel 1.
Nycklarna är strängar.
7-5
• En möjlighet är att helt enkelt addera ihop de olika
tecknens ASCII-värden. Funktionen blir :
unsigned int hash( const char * Key,
const int hSize)
{
const char * Keyptr = Key;
unsigned int hashVal=0;
while (*Keyptr )
hashVal += *Keyptr++;
return hashVal % hSize;
}
• Den hash-funktionen är enkel att beräkna, men om
tabellstorleken är stor distribuerar den dock inte
nycklarna på ett bra sätt.
Exempel 2.
Nycklarna är strängar.
7-6
unsigned int hash( const char * Key,
const int hSize)
{
return (Key[0]+27*Key[1]+729*Key[2]) % hSize;
}
– Antar att key åtminstone har längden två.
– 27 representerar antal bokstäver i alfabetet plus
blanktecknet och 729 är 272.
• Fungerar ändå inte bra eftersom det engelska språket
inte ger en slumpmässigt likformig fördelning.
Sid 4
Kapitel 7: Sökning (Hashning)
Exempel 3.
Nycklarna är strängar.
7-7
unsigned int hash( const char * Key,
const int hSize)
{
const char * Keyptr = Key;
unsigned int hashVal=0;
while (* Keyptr )
hashVal = (hashVal << 5) + *Keyptr++;
return hashVal % hSize;
}
– Använder alla tecken i nyckeln.
• Ger en relativt bra distribution.
• Har fördelen att vara extremt enkel och snabb.
– Om inte nycklarna är mycket långa.
Kollisionshanteringen
7-8
• Det finns flera metoder. Vi kommer att presentera
två av de enklaste:
– öppen hashning.
» (open hashing, separate chaining)
– sluten hashning.
»
(closed hashing, open addressing, rehashing)
Sid 5
Kapitel 7: Sökning (Hashning)
Öppen hashning (separate chaining).
7-9
• Vid öppen hashning håller man alla element som hashar till
samma värde i en separat lista.
• fördelar:
– cellerna behöver inte ligga kontinuerligt i minnet.
– tillåter traversering i hash-key-ordning, dock ej i sekventiell
key-ordning.
• nackdelar:
– allokeringen av nya celler tenderar att göra denna metod
mindre tidseffektiv.
– kräver att ytterligare en datastruktur implementeras.
– extra utrymme behövs för hashtabellen och pekarna.
Fyllnadsgraden
7 - 10
• Fyllnadsgraden (load factor) lf definieras som
– antal element i hashtabellen dividerat med
tabellstorleken.
• De intressantaste måtten är den genomsnittlig tiden
för:
– insättning.
– (lyckad) sökning.
– misslyckad sökning.
– borttag.
Sid 6
Kapitel 7: Sökning (Hashning)
Öppen hashning, fig 1.
7 - 11
typedef struct list_node * node_ptr;
struct list_node
{
element_type element;
node_ptr next;
};
typedef node_ptr LIST;
typedef node_ptr position;
struct hash_tbl
{
unsigned int table_size;
LIST *the_lists; // fält av listor, allokeras
// senare
};//Listorna använder headers, allokeras senare
typedef struct hash_tbl * HASH_TABLE;
}
Öppen hashning, fig 2.
7 - 12
HASH_TABLE initialize_table( unsigned int table_size )
{
HASH_TABLE H;
int i;
H= new struct hash_tbl; /* Allokera tabell */
if( H == NULL ) fatal_error("Out of space!!!");
H->table_size=next_prime(table_size);
H->the_lists= new LIST[H->table_size]; //Allok list-pek
if( H->the_lists==NULL ) fatal_error("Out of space!");
for(i=0; i<H->table_size; i++ ) { // Allok list-headers
H->the_lists[i] = new struct list_node;
if( H->the_lists[i] == NULL )
fatal_error("Out of space!!!");
else
H->the_lists[i]->next = NULL;
}
return H;
}
Sid 7
Kapitel 7: Sökning (Hashning)
Öppen hashning, fig 3.
7 - 13
position
find( element_type key, HASH_TABLE H )
{
position p;
LIST L;
L = H->the_lists[ hash(key, H->table_size) ];
p = L->next;
while( (p != NULL) && (p->element != key) )
// strcmp behövs;
p = p->next;
return p;
}
Öppen hashning, fig 4.
7 - 14
void insert( element_type key, HASH_TABLE H )
{
position pos, new_cell;
LIST L;
pos = find( key, H );
if( pos == NULL ) // key ej funnen
{
new_cell = new sizeof (struct list_node);
if( new_cell == NULL )
fatal_error( "Out of space!!!");
else {
L= H->the_lists[ hash(key,H->table_size ) ];
new_cell->next = L->next;
new_cell->element = key; // strcpy behövs!!
L->next = new_cell;
}
}
}
Sid 8
Kapitel 7: Sökning (Hashning)
Sluten hashning (rehashing).
7 - 15
• Sluten hashning är ett alternativ för att lösa
kollisionsproblemet utan att använda länkade listor.
– kallas även öppen adressering.
• Vid sluten hashning prövas alternativa celler tills en
ledig hittas.
– Formell beskrivning:
» cellerna h0(x), h1(x), h2(x),.... prövas efter varandra
där
hi(x)= ( Hash(x) + f(i) ) mod hSize,
» med f(0)=0. Funktionen f representerar
kollisionshanterings-strategin.
Sluten hashning (rehashing), forts.
7 - 16
• Alla data ligger i tabellen varför sluten hashning
kräver en större tabell än öppen hashning.
• Generellt bör fyllnadsgraden (lf) vara mindre än 0.5
vid sluten hashning.
• Kräver ”lazy deletion”.
• Vi tittar nu närmare på tre olika strategier för
kollisionshanteringen:
– Linjär prövning (Linear probing).
– Kvadratisk prövning (Quadratic probing).
– Dubbel hashning (Double hashing).
Sid 9
Kapitel 7: Sökning (Hashning)
Linjär prövning.
7 - 17
• f(i) är en linjär funktion, vanligen f(i)=i. Denna
innebär att cellerna provas i sekvens.
– en ledig cell kan alltid hittas så länge som tabellen
är tillräckligt stor, men tiden för att göra det kan bli
ganska stor.
• Problem:
– primär klustring (primary clustering), d v s att block
av ockuperade börjar att formas, även om tabellen
är relativt tom.
» medför att varje nyckel som hashas in i klustret
kommer att kräva flera försök för att lösa
kollisionerna, och dessutom själv adderas till klustret.
• Förväntade antalet prövningar är
– omkring ½( 1 + 1/(1- lf)2 ) för insättningar och
misslyckade sökningar.
– omkring ½( 1 + 1/(1- lf) ) för lyckade sökningar.
Kvadratisk prövning.
7 - 18
• Eliminerar de problem med primär klustring som
linjär prövning uppvisar.
• Dock kvarstår att element som hashar till samma
position kommer att pröva samma alternativa celler.
Detta är känt som sekundär klustring.
– Sekundär klustring är mer av teoretiskt intresse.
Simuleringar visar att den i allmänhet orsakar en
extra halv prövning per sökning.
• Kollisionsfunktionen är kvadratisk, t ex f(i)= i2.
• Sats:
– Om kvadratisk prövning används och
tabellstorleken är ett primtal, kan ett nytt element
alltid sättas in om tabellen är åtminstone halvtom.
Sid 10
Kapitel 7: Sökning (Hashning)
Sluten hashning, fig 1.
7 - 19
enum kind_of_entry { legitimate, empty, deleted };
struct hash_entry
{
element_type element;
enum kind_of_entry info;
};
typedef INDEX position;
typedef struct hash_entry cell;
struct hash_tbl
{
unsigned int table_size;
cell * the_cells; // fält av hash_entry celler
// allokeras senare
};
typedef struct hash_tbl * HASH_TABLE;
Sluten hashning, fig 2.
7 - 20
HASH_TABLE initialize_table( unsigned int table_size )
{
HASH_TABLE H;
int i;
if( table_size < MIN_TABLE_SIZE ) {
error("Table size too small"); return NULL; }
H = new struct hash_tbl; // Allokera tabell
if( H == NULL ) fatal_error("Out of space!!!");
H->table_size= next_prime( table_size ); // Allok celler
H->the_cells = new cell [H->table_size];
if(H->the_cells == NULL) fatal_error("Out of space!!!");
for(i=0; i<H->table_size; i++ )
H->the_cells[i].info = empty;
return H;
}
Sid 11
Kapitel 7: Sökning (Hashning)
Sluten hashning, fig 3.
7 - 21
position find( element_type key, HASH_TABLE H )
{
position i, current_pos;
i=0;
current_pos = hash( key, H->table_size );
while( (H->the_cells[current_pos].element!= key)
&& (H->the_cells[current_pos].info!= empty ))
// strcpy behövs troligen!!
{
current_pos += 2*(++i) - 1;
if( current_pos > H->table_size )
current_pos -= H->table_size;
}
return current_pos;
}
Sluten hashning, fig 4.
7 - 22
void insert( element_type key, HASH_TABLE H )
{
position pos;
pos = find( key, H );
if( H->the_cells[pos].info != legitimate )
// ok att göra insert här
{
H->the_cells[pos].info = legitimate;
H->the_cells[pos].element = key;
// strcpy behövs troligen!!
}
}
Sid 12
Kapitel 7: Sökning (Hashning)
Dubbel hashning.
7 - 23
• Denna teknik eliminerar problemet med sekundär
klustring till priset av extra multiplikationer och
divisioner.
• Dubbel hashning innebär att vi applicerar en andra
hash-funktion på x och prövar på distansen
hash2(x), 2*hash2(x), ... o s v.
• Ett populärt val av funktion är f(i)= i * hash2(x).
– Notera att :
» funktionen får aldrig evaluera till 0.
» viktigt att försäkra sig om att alla celler kan prövas.
» ett dåligt val av hash2(x) är förödande.
Dubbel hashning, forts.
7 - 24
• En funktion som
– hash2(x)= R - (x mod R),
» med R som ett primtal mindre än hSize.
• Simuleringar visar att det förväntade antalet
prövningar är nästan detsamma som för en strategi
med slumpmässig kollisionshantering.
• Detta gör dubbelhashning teoretiskt intressant.
– Kvadratisk prövning kräver dock inte en andra
hashfunktion och är således troligen enklare och
snabbare i praktiken.
Sid 13
Kapitel 7: Sökning (Hashning)
Reorganisation av hashtabellen.
7 - 25
• Om tabellen blir för full kommer
– Körtiden för de olika operationerna att börja ta för
lång tid.
– Insättningar kanske misslyckas för sluten hashning
med kvadratisk lösning (resolution).
» Detta kan hända om det är för många borttagningar
blandade med insättningarna.
– En lösning är då att
» skapa en ny, dubbelt så stor, tabell (med en ny hashfunktion).
» scanna igenom hela den första tabellen under det att
nya hashvärden beräknas för varje kvarvarande
element och detta sätts in i den nya tabellen.
• Hela denna operation kallas ”reorganisation av
hashtabellen”. Även rehashing används.
– Detta är uppenbart en mycket dyr operation, som
lyckligtvis inträffar relativt sällan.
Reorganisation , forts.
7 - 26
• Reorganisation kan implementeras på flera sätt med
kvadratisk prövning.
– Några av alternativen är att göra en reorganisation,
1. så snart som tabellen är halvfull.
2. bara när en insättning misslyckas.
3. så snart som tabellen når en viss fyllnadsgrad.
Sid 14
Kapitel 7: Sökning (Hashning)
Reorganisation, fig 1.
7 - 27
HASH_TABLE rehash( HASH_TABLE H )
{
unsigned int i, old_size;
cell *old_cells;
old_cells = H->the_cells;
old_size = H->table_size;
// Skapa en ny tom tabell
H = initialize_table( 2*old_size );
//Scanna igenom den gamla tabellen, och sätt in i den nya.
for( i=0; i<old_size;i++ )
if( old_cells[i].info == legitimate )
insert( old_cells[i].element, H );
free( old_cells );
return H;
}
Applikationer - Hashtabell
7 - 28
• Kompilatorer använder hashtabeller för att hålla reda på
deklarerade variabler i källkoden (symboltabell).
• Används i grafteoretiska problem där noderna har namn
i stället för nummer.
• Används i program som spelar något slags spel.
– När programmet söker igenom olika spellinjer, håller det
reda på de ställningar det har sett genom att beräkna en
hashfunktion baserad på positionen. Om samma position
uppträder igen så kan programmet undvika en dyr ny
beräkning.
» Denna generella egenskap hos alla spel-program är känd
som en transpositionstabell.
• Ännu ett användningsområde är
”on-line”- stavningskontroll.
» Om felstavningsupptäckt är viktig, kan en hel ordbok vara
pre-hashad.
Sid 15
Kapitel 7: Sökning (Hashning)
Summering
7 - 29
• Hashtabeller kan användas för att implementera
insert- och findoperationerna på konstant tid i
genomsnitt.
• Fyllnadsgraden är speciellt viktig att beakta när
man använder hashtabeller, eftersom tidsgränserna
annars inte gäller.
• Val av hashfunktion är viktig,
» speciellt när nycklarna inte är en kort sträng eller ett
heltal.
• Vid öppen hashning bör fyllnadsgraden vara nära 1,
» även om performance inte signifikant degraderas
förrän fyllnadsgraden blir mycket hög.
• Vid sluten hashning bör fyllnadsgraden inte
överstiga 0.5 .
• Trots den uppenbara enkelheten hos hashtabellen är
mycket av analysen ganska komplicerad och
fortfarande återstår många obesvarade frågor.
Summering, forts.
7 - 30
• Reorganisation kan implementeras för att tillåta
tabellen att växa (och minska) och på så sätt
upprätthålla en rimlig fyllnadsgrad .
• En nackdel med hashtabellen är att den inte
understödjer ”ordnande” operationer.
» T ex ej möjligt att finna det minsta elementet.
» Ej heller möjligt att söka effektivt efter en sträng om
inte den exakta strängen är känd.
• Binära sökträd kan också användas för att
implementera insert- och findoperationerna.
– Binära sökträd stödjer också rutiner som kräver
ordning och är således mer kraftfulla .
• Å andra sidan är värsta fallet för hashning normalt
resultatet av ett implementationsfel, medan däremot
en redan sorterad indatamängd kan ge ett binärt
sökträdett sämre uppträdande.
Sid 16
Kapitel 7: Sökning (Hashning)
Slutord.
7 - 31
• Balanserade sökträd är relativt dyra att
implementera.. .
• Så om ingen ordningsinformation efterfrågas och
det finns en antydan till misstanke om att indata
redan kan vara sorterat, är hashtabellen den
datastruktur som bör väljas.