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.