Datalogiövning 24/1 2007
Hashtabeller! (& kanske lite sortering)
Allmänt om hashtabeller: Snabb lösning för sökningar, O(1). Man lagrar par av nycklar och värden. En
hashfunktion beräknar ur nyckeln ett hashtabell-index där värdet ska lagras. När man sedan vill hitta
värdet för en viss nyckel så gör man ungefär samma sak; man beräknar hash-värdet för nyckeln och
letar på den plats i tabellen som man får ut. En perfekt hashfunktion genererar olika värden för alla
möjliga nycklar; en minimal perfekt hashfunktion använder dessutom alla index i tabellen. Sådana
hashfunktioner finns, men de behövs oftast inte, dessutom kräver de att man känner till nycklarna i
förväg och att storleken på dessa är begränsad. I praktiken räcker det att man får bra spridning på
värdena. Inget entydigt svar på vad som är en "bra" hashfunktion. Generellt sett vill man ha en
funktion som sprider ut värdena nästan slumpmässigt. Det är också bra om hashfunktionen går fort att
beräkna. Här använder vi oss av enkla hashfunktioner som försöker sätta ett unikt hash-värde på varje
möjligt input; sedan använder man modulo för att få det att rymmas i tabellen.
Om man bara ska sätta in heltal så behöver man ingen speciell hashfunktion; talen är ju i sig bra index
till hashtabellen.
En stor hashtabell ger färre krockar men tar upp mer minne. Man vill ha en kompromiss. En tumregel är
att göra hashtabellen dubbelt så stor som antalet element man vill sätta in ("50 % luft"). Man använder
ofta modulo-operatorn för att få fram ett index som ligger inom tabellens gränser. Det har visat sig att
spridningen blir bäst om man använder ett primtal som storlek på tabellen.
Krockar kan hanteras på olika sätt, t ex med krocklistor eller linjär/kvadratisk probning. (Linjär kan leda
till problem med klustring, klumpar av värden som gör det svårt att hitta en ledig plats.) Man kan
också använda omhashning, dvs använda en annan hashfunktion; antingen för att hitta antalet steg
som man ska hoppa i tabellen eller för att lägga in i ytterligare en hashtabell. Hashtabeller har alltid
O(1) i bästa fallet, men olika krockhanteringsmetoder ger olika värstafallskomplexitet. Krocklistor har t
ex i värsta fallet O(N) om man använder just listor
(varför?). Men om man har ett självbalanserande binärt träd istället för en krocklista får man O(log N).
Krocklistor är okänsliga för klustring, medan probing-baserade metoder är mer beroende av en bra
hashfunktion.
Bloomfilter: Man kan göra en typ av minnessnål multipel hashning som heter Bloom-filter. I t ex ett
stavningskontrollprogram där man måste kunna ha med massor av ord vill vi att det ska vara
minnessnålt och vill alltså inte lagra själva orden. Vi kan då ha en boolsk hashtabell som bara lagrar
ettor och nollor. Tabellen är initialiserad så att den är full av nollor. Sedan beräknar man
hashfunktionen för alla rättstavade ord och sätter ettor på motsvarande plats i tabellen. Då får vi
perfekt "recall" av rättstavade ord, dvs om vi har stavat rätt så kommer programmet alltid att veta
detta. Det finns dock ett problem med falska positiva: om ett felstavat ord råkar få samma hashkod
som ett rättstavat, så kommer ordet inte att markeras som felstavat. För att råda bot på detta kan man
använda flera olika hashfunktioner (t ex 14) och ordet måste få ettor efter att ha körts genom samtliga.
Då är det mycket liten chans att felstavade ord slinker igenom. Detta lägger bara till en konstant faktor
och påverkar inte komplexiteten, som fortfarande är O(1).
I Python finns en datatyp som heter dictionary och motsvarar en hashtabell. Den lagrar alltså par av
nycklar och värden utan någon särskild inbördes ordning.
Nycklar kan vara t ex tal eller strängar (men inte listor).
>>>d = {} # Skapar en tom dictionary
>>>d={'Sverige':'Stockholm','Finland':'Helsingfors'} # Skapar en dictionary med två nyckel/värde-par
Nu kan man skriva t ex
>>>d['Finland']
'Helsingfors'
Om man försöker lägga in ett nytt värde för en nyckel som redan finns, så skrivs det gamla värdet
över. Man kan alltså inte ha fler värden för en nyckel.
Dictionaries kan växa och krympa medan programmet körs. Man kan ta ut nycklar och värden genom
att använda del:
>>> del d['Finland']
Man kan skriva ut alla nycklar med keys(), kanske i kombination med sort().
>>> d.keys()
['Finland', 'Sverige']
Värdena kan på samma sätt skrivas ut med values().
>>> d.values()
['Helsingfors', 'Stockholm']
Om man gör en for-loop över en dictionary på det lättaste sättet (for i in d:) så loopar man över
nycklarna.
Det finns också en funktion items() som skriver ut nyckel/värde-paren som tupler, en annan
datastruktur i Python.
>>> d.items()
[('Finland', 'Helsingfors'), ('Sverige', 'Stockholm')]
Man kan kolla om nycklar eller värden finns:
>>> d.has_key('Norge')
False
>>> d.has_key('Finland')
True
Ett exempel ur boken: Om man jobbar med stora matriser där många element är noll så kan det vara
bra att använda en dictionary istället.
(OK, den här är inte så stor, men man skulle kunna tänka sig t ex en sökmotor som håller reda på vilka
ord som finns på vilka webbsidor; då skulle man få väldigt många nollor!)
Matrisen ovan kan man skriva som en dictionary där man bara tar med de nollskilda elementen:
matrix = {(0,3): 1, (2, 1): 2, (4, 3): 3}
Här är nycklarna alltså tupler. Man kan nu skriva t ex
>>> matrix[(4,3)]
3
Tyvärr så får man nu ett felmeddelande om man försöker komma åt ett nollelement:
>>> matrix[(4,4)]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
KeyError: (4, 4)
Det måste man gå runt genom att använda metoden get() för dictionaries; den hämtar värdet om
nyckeln finns, eller returnerar ett default-värde annars. Detta default-värde blir noll i vårt fall.
>>> matrix.get((4,4),0)
0
>>> matrix.get((4,3),0)
3
Hashtabeller/dictionaries kan med fördel användas istället för dumträd i BFS/DFS.
Allmänna uppgifter på hashtabeller
Under gulfkriget var det väldigt svårt för armestaben att hålla reda på alla TV-bolag som for omkring
och rapporterade i öknen. För att hålla reda på dem användes en hashvektor. Koden fungerade inte
som avsett och man har nu gett i uppdrag åt en f.d. datalogistudent att titta på en misstänkt del av
koden:
hashvector = [None]*100
def put(s, val):
hashcode = 0
for l in s:
hashcode += ord(l)
hashcode = hashcode % 100
hashvector[hashcode] = val
Vad är det för fel på koden? Beskriv hur man kan förbättra den. Namnen på TV-bolagen kan antas
bestå av högst tre bokstäver. Det kommer inte att förekomma mer än 75 TV-bolag.
4 förbättringar; ett direkt fel, två prestandasänkande saker och en ett exempel på dålig
programmeringsstil.
Det som gör att koden inte fungerar är att det inte finns någon krockhantering.
hashvektor[hashcode] = info;
För att lösa det kan man använda t.ex. krocklistor eller linjär sökning. Linjär sökning fungerar så att om
det är upptaget på den givna positionen så söker man tills man antingen hittar nyckeln (den var redan
instoppad sedan tidigare) eller tills man hittar ett tomt element i vektorn.
Möjlig kod för krocklista:
hashvector = [None]*100
def put(s, val):
hashcode = 0
for l in s:
hashcode += ord(l)
hashcode = hashcode % 100
if hashvector[hashcode]==None:
hashvector[hashcode]=[val]
else:
hashvector[hashcode].append(val)
Hashkoden är ganska korkat implementerad. Strängarna "AB" och "BA" får samma värde. För att lösa
det kan man vikta genom att multiplicera med t.ex. 1, 100 och 10000.
Längden på hashvektorn är lite för liten och dessutom inget primtal. Välj istället hashvektorns storlek
till exempelvis 151 (dubbel så stor som förväntade antalet element). Det är dessutom dålig
programmeringsstil att hårdkoda värdet på flera ställen, använd en variabel istället.
Databasprogram
Till en databas med c:a hundra tusen historiska händelser under dom senaste femtusen åren ska en
hashvektor skapas för snabb sökning efter ett givet datum. Föreslå en bra hashfunktion.
En generell hashfunktion är
h(key) = key % size
Då måste du hitta en lämplig tabellstorlek (size) och nyckel (key) för att hitta/lägga in information i
hashtabellen.
Det finns 100 000 händelser att hasha. Hashvektorn bör ha primtalsstorlek som ger 50% luft, d v s
runt 200 000. Anta att alla år innan 0 E.Kr. är negativa tal. En lämplig nyckel är till exempel (50000000
- YYYYMMDD). Man tar bort datumet från 50000000 för att undvika negativa tal. Då blir hashfunktionen
h(key) = (50000000 - YYYYMMDD) % 200003
Kollisioner
Redogör för kollisionshantering i hashtabeller. Måste man alltid hantera krockar? Vad händer om man
inte gör det? Vad innebär klustring?
Hur fungerar kollisionshantering i bloomfilter?
Arkens djurpar
I arken fanns två individer av varje art, alltså herr och fru marsvin, herr och fru gråsugga osv. Noa har
ordentligt fört in varje individs artnamn i en fil, dock huller om buller.
Det finns cirka två miljoner namn i filen och Noa behöver hjälp med att kolla att varje namn
förekommer exakt två gånger.
Uppskatta komplexiteten (antal jämförelser) för följande metoder.
•
Djuren in i en kö, upprepa sedan följande: Första djuret ut, snurra igenom kön och ta bort
maken.
•
Djuren in i vektor som quicksorteras och sedan kollas igenom (hur?).
•
Djuren in i ett binärträd som sedan kollas igenom (hur?).
•
Djuren in i en hashtabell med cirka tre miljoner platser som sedan kollas igenom (hur?).
•
Djuren in i trappa (heap) som sedan töms parvis.
•
Djuren hashas in i Viggos bloomfilter med fjorton hashfunktioner.
Om någon metod är helt oanvändbar ska du tala om det.
Uppfinn gärna någon egen, ännu bättre metod!
Kömetoden kräver en biljon jämförelser, bloomfiltret är oanvändbart.
Quicksortering följd av parkoll tar N log N + N/2 jämförelser.
Binärträd med inordergenomgång tar cirka 2NlogN, trappa likaså.
Hashningen går i linjär tid, t ex 5N och parkollen tar cirka 2N. Klart snabbast alltså eftersom i detta fall
log N blir ca 20.
Ännu något effektivare är att redan vid inhashningen observera om det finns fler än två av någon art.
-----------
041019:4 Fågelsökning
Lilla fågelboken, med hundra uppslagsord, och Stora fågelboken, med tiotusen uppslagsord finns
lagrade på datorsökbar form. Det finns två olika sätt att söka
på. Sökmetod A tar lika lång tid för bägge fågelböckerna, men sökmetod B tar dubbelt så lång tid när
man söker i Stora fågelboken. (4p) Vilka är sökmetoderna? Motivera ditt svar!
Sökmetod A, som tar lika lång tid oberoende av antal uppslagsord är hashning, som är O(1). Sökmetod
B tar dubbelt så lång tid när antalet uppslagsord ökar kvadratiskt (1002 = 10000) och är alltså O(logn),
vilket innebär binärsökning (i sorterad array eller binärt sökträd).
Unika Pokémonkort
Linda har n stycken pokémonkort hemma. Varje gång hon får nya kort skriver hon in namnen sist i en
fil.
Nu vill hon skapa en lista med unika kort, alltså utan eventuella dubbletter.
Uppskatta komplexiteten (antal jämförelser) för följande metoder. Ange också
vilka metoder som är oanvändbara, eller behöver modifieras. Uppfinn gärna någon
egen, ännu bättre metod!
1. Läs in korten till en array, upprepa sedan följande för varje kort: Skriv ut
kortet, sök sedan med linjärsökning efter dubbletter i resten av arrayen, och
sätt dessa till null.
2. Läs in korten till en array. Sortera korten i bokstavsordning med insättningssortering. Skriv sen ut alla kort som inte är identiska med nästa i vektorn.
3. Hasha in korten i en hashtabell utan krocklistor. Skriv sedan ut alla nullskilda element.
4. Hasha in korten i Viggos bloomfilter med fjorton hashfunktioner. Gå sedan
igenom filen igen och skriv ut alla kort som ger trä i bloomfiltret.
5. Sortera in korten i ett binärt sökträd, som i labb 3, och skriv sedan ut trädet
i inorder.
6. Läs in korten till en stack. Upprepa följande till stacken är tom: Poppa första
kortet och skriv ut det, poppa sedan resten av korten, utom dom som är
identiska med första kortet, till en andra stack och poppa tillbaka rubbet
till första stacken.
1. Läs in korten till en array, upprepa sedan följande för varje kort: Skriv ut kortet,
sök sedan med linjärsökning efter dubbletter i resten av arrayen, och sätt dessa till
null. Inläsning O(n), en linjärsökning för varje kort nO(n), utskrift O(n)=>O(n2 )
totalt. Fungerar.
2. Läs in korten till en array. Sortera korten i bokstavsordning med insättningssortering. Skriv sen ut alla kort som inte är identiska med nästa i vektorn. Inläsning
O(n), sortering n2 , sen går man igenom alla igen och jämför med nästa vilket tar
O(n) =>O(n2 ) totalt. Fungerar i princip, men vi måste lägga till utskrift av allra
sista elementet.
3. Hasha in korten i en hashtabell utan krocklistor. Skriv sedan ut alla nullskilda
element. Att hasha in alla korten tar O(n) och utskriften O(n) =>O(n) totalt.
Men det fungerar bara om man har en perfekt hashfunktion, annars kan man ju
få krockar även för olika kort.
4. Hasha in korten i Viggos bloomfilter med fjorton hashfunktioner. Gå sedan igenom
filen igen och skriv ut alla kort som ger trä i bloomfiltret. Komplexitet O(n) som
hashningen ovan, men fungerar inte. Eftersom vi går igenom filen när vi ska skriva
ut korten och inte sållar bort några kort så kommer alla kort att skrivas ut.
5. Sortera in korten i ett binärt sökträd, som i labb 3, och skriv sedan ut trädet i
inorder. Stoppa in n kort i trädet tar nO(logn), skriva ut tar O(n) => O(nlogn)
totalt. Fungerar bra om man i insättningmetoden låter bli att lägga in ett kort om
man upptäcker att det redan fanns med.
6. Läs in korten till en stack. Upprepa följande till stacken är tom: Poppa första
kortet och skriv ut det, poppa sedan resten av korten, utom dom som är identiska
med första kortet, till en andra stack och poppa tillbaka rubbet till första stacken.
Första kortet jämförs med resten (som vid urvalssortering) =>O(n2 ) totalt. Fungerar om man när man poppar till den andra stacken slänger bort alla kort som
är identiska med det första.