Föreläsning 0 Innehåll Hashtabeller implementering, effektivitet Interfacen Set och Map ijava Interfacet Comparator Undervisningsmoment: föreläsning 0, övningsuppgifter 0-, lab 5 och 6 Avsnitt i läroboken: 7. 7.5 I gamla upplagan: 9. 9.5 PFK (Föreläsning 0) HT 205 / 52 Interfacen Set och Map, hashtabeller Exempel på vad du ska kunna Förklara begreppen hashtabell och hashfunktion. Definiera vad som menas med sluten och öppen hashtabell och hur kollisioner hanteras i sådana tabeller. Förklara vad som menas med fyllnadsgraden (eng: load factor) för en hashtabell. Förklara hur sökning, insättning och borttagning går till i slutna respektive öppna hashtabeller. Implementera öppna hashtabeller (görs på laboration 5). Ange tidskomplexiteten för operationer på hashtabell. Använda interfacen Map och Set och deras implementerande klasser i Java Collections Framework. Känna till och kunna implementera interfacen Comparable respektive Comparator. PFK (Föreläsning 0) HT 205 2 / 52 Diskutera Hashtabeller Tidigare har vi sett att man kan använda binära sökträd för att lagra data som man snabbt ska kunna söka i. Tidskomplexiteten för att sätta in, söka och ta bort element i ett balanserat binärt sökträd är O(logn). Antag att vi har fått i uppdrag att skriva ett program som hanterar medlemmar i en förening. Antalet medlemmar är max 000 st. Varje medlem har ett unikt medlemsnummer mellan 0 och 999. Detta nummer används som nyckel för att identifiera och söka efter en medlem. Finns det något bättre (snabbare, enklare) sätt än binärt sökträd för att lagra medlemmarna i just detta specialfall? Antag att de nycklar som ska användas vid sökningen är heltal i intervallet 0..n. En vektor med n + platser kan användas. Elementet med nyckel k placeras på plats k i vektorn. Sökning, insättning och borttagning av k blir en direkt access till plats k. Alla dessa operationer har tidskomplexitet O(). Idéen kan generaliseras till alla slags objekt: Nycklarna översätts till ett heltal i intervallet 0..n. PFK (Föreläsning 0) HT 205 3 / 52 PFK (Föreläsning 0) HT 205 4 / 52
Hashfunktion Hashfunktion för heltal Idé: översätt nycklar till heltal som kan användas som index i en vektor. hashfunktion h nyckel hashkod, tal i intervallet 0..table.length Hashfunktionen h avbildar en stor mängd nycklar på en liten mängd tal. Ett sådant tal (hashkod) kan användas som index i en vektor. Kollisioner (olika nycklar får samma hashkod) är oundvikliga och måste hanteras. En bra hashfunktion bör påverkas av alla delar av nyckeln. ger litet förväntat antal kollisioner, sprider elementen över hela tabellen. Om nyckeln är ett heltal k kan man räkna ut hashkoden så här int index = k % table.length; if (index < 0 ) { index : index + table.length; Math.abs kan ge ett negativt resultat och används därför inte här. PFK (Föreläsning 0) HT 205 5 / 52 PFK (Föreläsning 0) HT 205 6 / 52 Hashfunktion för strängar Metoden hashcode För en sträng s 0 s s 2 s n är en lämplig hashfunktion ascii(s 0 ) 3 n + ascii(s ) 3 n 2 + + ascii(s n ) Ger ett stort heltal (som får anpassas till tabellens storlek genom % table.length). Tecken i olika positioner multipliceras med olika potenser av 3. Permutationer av samma tecken ger därför olika hashkod. 3 är ett primtal och det kan visas att man därför får relativt få kollisioner. I klassen Object finns en metod hashcode som översätter ett objekt till ett heltal. Den är implementerad så att olika objekt om möjligt avbildas på olika heltal. Metoden hashcode är skuggad i Javas klasser (String, Integer ) så att lika objekt avbildas på samma heltal. Heltalet som returneras från hashcode får sedan anpassas till tabellens storlek med % table.length. Man måste skugga hashcode (och equals) i den klass vars objekt ska fungera som nyckel i en hashtabell. Objekt för vilka equals ger true ska få samma hashkod. PFK (Föreläsning 0) HT 205 7 / 52 PFK (Föreläsning 0) HT 205 8 / 52
Diskutera Hashtabeller olika alternativ Element med nycklarna, 8, 27, 64, 6 ska sättas in i en tabell med 7 platser. Använd hashfunktionen h(x) = x % 7. 0 2 3 4 5 6 Hur hanterar man kollisioner (dvs. att olika nycklar får samma hashkod)? Det finns olika sätt att implementera hashtabeller. Sluten hashtabell (eng: open addressing) en vektor används för att lagra elementen Det finns sedan olika sätt att hantera kollisioner t ex linjär teknik kvadratisk teknik Öppen hashtabell (eng: separate chaining) en vektor av listor Kolliderande objekt placeras i samma lista. PFK (Föreläsning 0) HT 205 9 / 52 PFK (Föreläsning 0) HT 205 0 / 52 Sluten hashtabell med linjär kollisionsteknik Problem med linjär teknik Vid linjär teknik sätter man in ett element som kolliderar med ett annat på första lediga plats efter den där det skulle ha hamnat om ingen kollision inräffat. Tabellen betraktas som cirkulär, d.v.s. plats 0 anses komma efter tablesize-. 6 8 64 27 0 2 3 4 5 6 Linjär teknik ger upphov till primär klustring i tabellen. Om flera objekt har samma hashkod hval kommer de att ligga i ett kluster kring platsen hval i tabellen. Även objekt vars hashkoder är nära hval kommer att drabbas av kollisioner och bygga ut klustret. Stora kluster gör sökning långsam. Ex: hashfunktion h(x) = x % 0. Sätt in element med nycklarna 3, 3, 23, 33, 5, 5. Sökning efter visst element börjar på den plats elementets hashkod anger och fortsätter eventuellt framåt. Om det inte påträffas före en ledig plats finns det inte i tabellen. 3 3 23 33 0 2 3 4 5 6 5 5 7 8 9 PFK (Föreläsning 0) HT 205 / 52 PFK (Föreläsning 0) HT 205 2 / 52
Borttagning i sluten hashtabell med linjär kollisionsteknik Borttagning i sluten hashtabell med linjär kollisionsteknik Forts Om vi vid borttagning bara gör platsen tom, leder det till fel vid sökning. Ex: Tag bort 23 ur tabellen på föregående bild: Om vi i stället markerar platsen icke-aktiv vid borttagning (i fig. nedan markerat med ett d): 3 3 33 5 5 3 3 d 33 5 5 0 2 3 4 5 6 7 8 9 0 2 3 4 5 6 7 8 9 Om vi nu söker efter 5 vars hashkod är 5 börjar vi pröva plats 5. Eftersom denna plats är tom sluter vi oss felaktigt till att det sökta elementet inte finns i tabellen. så kan vi utföra sökningen med början på den plats hashkoden anger och framåt över alla upptagna och icke-aktiva platser. Först när vi stöter på en riktigt tom plats är det misslyckad sökning. PFK (Föreläsning 0) HT 205 3 / 52 PFK (Föreläsning 0) HT 205 4 / 52 Tidskomplexitet, linjär teknik Sluten hashtabell, kvadratisk kollisionsteknik Alternativ, bättre teknik för hantering av kollisioner. Värstafallet för operationerna sökning, insättning och borttagning är O(n), där n är antalet element som finns insatta i tabellen. Inträffar om alla element hamnar i en följd och vi t ex vid sökning måste pröva alla platserna i denna följd. Är dock ytterst osannolikt. Under förutsättning att tabellen inte fylls till mer än hälften får man O()-komplexitet i medeltal. Först prövas nästa plats, sedan platsen 4 steg fram, sedan 9 steg fram, alltså hval, hval +, hval + 2 2, hval + 3 2,,hVal + i 2, där hval är elementets hashkod. Tabellen används fortfarande cirkulärt. Undviker primär klustring av element. Kan modifieras till andra sekvenser av steg. PFK (Föreläsning 0) HT 205 5 / 52 PFK (Föreläsning 0) HT 205 6 / 52
Sluten hashtabell, kvadratisk kollisionsteknik Exempel Sluten hashtabell, kvadratisk kollisionsteknik Sätt in element med nycklarna 89, 8, 49, 58, 9 i en tabell med 0 platser. Hashfunktion: x % 0 89 % 0 = 9 8 % 0 = 8 49 % 0 = 9 58 % 0 = 8 9%0=9 49 58 9 0 2 3 4 5 6 7 8 89 8 9 Problem: Inte alltid säkert att man hittar ledig plats även om det finns. Om t ex tabellens storlek är 6 och man använder hashfunktionen x % 6 och sätter in element med nycklarna 0, 6, 32 och 64 så kan man inte därefter hitta någon ledig plats för element som hashas till plats 0. De enda platser som kommer att prövas i serien H + i 2 när H = 0blir de upptagna platserna 0,, 4 och 9. Om tabellens storlek är ett primtal kan ett nytt element alltid sättas in om tabellens fyllnadsgrad är mindre än 0.5. Tidskomplexitet: Ännu ej fullständigt utredd. Värsta fallet är O(n). I praktiken mindre klustring än den linjära tekniken. PFK (Föreläsning 0) HT 205 7 / 52 PFK (Föreläsning 0) HT 205 8 / 52 Öppen hashtabell (separate chaining) Elementen i tabellen är listor. I lista nummer k ligger alla element vars nyckel har hashkoden k. Öppen hashtabell (separate chaining) Exempel Sätt in element med nycklarna, 8, 27, 64, 6 i en öppen tabell med 7 listor. Använd hashfunktionen h(x) = x % 7 0 2 tablesize-2 tablesize- 0 2 3 4 5 6 64 8 6 27 PFK (Föreläsning 0) HT 205 9 / 52 PFK (Föreläsning 0) HT 205 20 / 52
Nycklar och hashkod är inte samma sak Öppen hashtabell Blanda inte ihop begreppen nyckel och hashkod! Nycklarna är unika. Hashkoden beräknas med en hashfunktion som avbildar nycklarna på heltal. Olika nycklar kan få samma hashkod (kollision). Obeservera att borttagning i öppen tabell är enklare än i sluten. Elementet tas helt enkelt bort ur den lista där det befinner sig. Vi får inga problem med luckor som i den slutna tabellen. 0 2 3 4 5 6 64 8 6 27 OBS! Unika nycklar: 64, 8, samma hashkod: Unika nycklar: 6, 27 samma hashkod: 6 I Javas klassbibliotek används öppna tabeller i klasserna HashSet och HashMap. På laboration 5 får ni själva implementera en öppen hashtabell med enkellänkade listor. PFK (Föreläsning 0) HT 205 2 / 52 PFK (Föreläsning 0) HT 205 22 / 52 Tidskomplexitet, öppen tabell Rehashing Om fyllnadsgraden blir för stor måste man bygga om tabellen: Värstafallet för operationerna sökning, insättning och borttagning är O(n), där n är antalet element som finns insatta i tabellen. Inträffar om alla element hamnat i samma lista. Bra hashfunktion gör detta osannolikt. Medelfall Om hashfunktionen är sådan att alla platser är lika sannolika och om man inte har för hög fyllnadsgrad är tidskomplexiteten O() i medelfall. Fyllnadsgrad (eng. load factor) = antal insatta element/antal platser itabellen. Valet av fyllnadsgrad är en kompromiss mellan minnesåtgång och tidsåtgång. Ett lämpligt val av fyllnadsgrad är 0.75. Skapa en dubbelt så stor tabell. Sätt in alla element i den nya tabellen. 0 2 3 4 5 6 64 8 25 6 27 6 6 0 2 3 4 5 7 8 9 0 2 3 8 25 27 64 PFK (Föreläsning 0) HT 205 23 / 52 PFK (Föreläsning 0) HT 205 24 / 52
Använda hashtabell för att implementera ADT mängd Använda hashtabell för att implementera ADT lexikon En hashtabell passar bra att använda för att representera en mängd: Man kan deklarera en inre klass som representerar nyckel-värde-par och sätta in sådana objekt i hashtabellen. Hashkoden beräknas på nyckeln. Ett hashtabell innehåller inte dubbletter. Det är effektivt att sätta in ett element. ta bort ett element. undersöka om ett element finns i hashtabellen. Tidskomplexiteten för dessa operationer är O() imedelfall. 0 2 3 4 null null null null key value next key value next null 5 6 null key value next null PFK (Föreläsning 0) HT 205 25 / 52 PFK (Föreläsning 0) HT 205 26 / 52 Interfacen Set och Map i java.util ADT mängd (Set) Iterable Collection En mängd (Set) är en samling element som inte innehåller dubbletter. Metoderna i interfacet Set finns även i interfacet Collection. De har dock olika kontrakt genom att Set inför restriktionen att inga dubbletter får förekomma. Queue List Set Map Enligt specifikationen i Java får en mängd (Set) innehålla null-element. Men bara ett null-element, p.g.a. dubblettförbudet. Vissa konkreta implementeringar av interfacet Set i java.util förbjuder dock insättning av null. SortedSet SortedMap PFK (Föreläsning 0) HT 205 27 / 52 PFK (Föreläsning 0) HT 205 28 / 52
Interfacet SortedSet Klasser som implementerar Set Förutsätter att elementen som sätts in går att jämföra med varandra. Elementen ska antingen implementera interfacet Comparable eller genom att man (via konstruktorn) anger ett Comparator-objekt som kan användas för jämförelser. Vi återkommer till detta. TreeSet implementerar det utvidgade interfacet SortedSet. Använder ett slags balanserat träd, inte AVL-träd utan röd-svarta träd (eng. Red-Black trees), som också garanterar att höjden är O( 2 log n). HashSet Använder hashtabell. Set Garanterar att operationen iterator() returnerar en iterator som går igenom mängden i växande ordning. Utvidgar Set-interfacet med några operationer som återspeglar att elementen går att ordna. Exempel: returnera minsta element, returnera största... HashSet SortedSet TreeSet PFK (Föreläsning 0) HT 205 29 / 52 PFK (Föreläsning 0) HT 205 30 / 52 Interfacet Set Abstrakta klasser AbstractCollection AbstractSet Collection Set SortedSet betyder ärver från ("extends") betyder implementerar ("implements") Interfacet Set Abstrakta klasser Kommentarer till hierarkin på föregående bild: Interface fick t.o.m. Java 7 inte innehålla implementeringar. Ibland kan man implementera vissa operationer med hjälp av andra operationer i samma interface. Ex: isempty () size() == 0 För att underlätta för den som ska implementera ett (stort) interface kan man implementera en abstrakt klass som innehåller implementeringar av vissa metoder enligt detta mönster. Ex: klasserna AbstractCollection och AbstractSet Implementatören av en konkret klass kan då ärva den abstrakta klassen och behöver sedan bara implementera återstående operationer iinterfacet. Ex: klasserna TreeSet och HashSet. HashSet TreeSet PFK (Föreläsning 0) HT 205 3 / 52 Fr.o.m Java 8 får man ha default-metoder i interface. Det innebär att man nu hade kunnat lägga de metoder som implementeras i Abstractklasserna direkt i interfacen istället. PFK (Föreläsning 0) HT 205 32 / 52
Klassen TreeSet Klassen TreeSet Implementerar interfacet SortedSet. Klassrubrik: public class TreeSet<E> extends AbstractSet<E> implements SortedSet<E> E förutsätts alltså inte implementera Comparable Men elementen som sätts in måste dock gå att jämföra med varandra. Se nästa bild. Det finns flera konstruktorer i klassen, bl.a: public TreeSet(); 2 public TreeSet(Comparator<? super E> c); Används den första konstruktorn, förutsätts elementen implementera Comparable annars genereras ClassCastException. Den andra konstruktorn har en parameter som är ett objekt av en klass som implementerar interfacet Comparator. Används denna kommer jämförelser att utföras med hjälp av komparatorn. PFK (Föreläsning 0) HT 205 33 / 52 PFK (Föreläsning 0) HT 205 34 / 52 Exempel på användning av klassen TreeSet Comparable<E> Diskutera // Skapa en mängd och lägg till personer i mängden Set<Person> set = new TreeSet<Person>(); set.add(new Person("Kalle", "340609-234")); set.add(new Person("Kajsa", "37009-222")); // undersök om personen med personnummer 37009-222 // finns i mängden boolean found = set.contains(new Person(null, "37009-222")); Klassen Person måste implementera Comparable<Person>. I metoden compareto jämförs personernas personnummer. En klass som implementerar interfacet Comparable implementerar en metod compareto för jämförelse. Alla jämförelser mellan objekt av klassen sker så som beskrivs i denna metod. Vår exempelklass Person implementerar Comparable<Person>. I metoden compareto är det personnumren som jämförs. Antag att vi vill kunna jämföra personer på olika sätt, både efter personnummer och efter namn. Vi vill t.ex. skapa en mängd där personerna ordnas efter personnummer och en mängd där personerna ordnas efter namn. Hur löser vi detta? PFK (Föreläsning 0) HT 205 35 / 52 PFK (Föreläsning 0) HT 205 36 / 52
Interfacet Comparator Implementering av interfacen Comparable och Comparator Klassen Person public interface Comparator<T> { /** * Compares its two arguments for order. * Returns a negative integer, zero, or a positive * integer as the first argument is less than, * equal to, or greater than the second. */ int compare(t e, T e2); Interfacet Comparator ger oss möjlighet att jämföra objekt av en klass på flera olika sätt. PFK (Föreläsning 0) HT 205 37 / 52 Exempel på användning av klassen TreeSet Komparator public class Person implements Comparable<Person> { private String name; private String pnbr; public int compareto(person other) { return pnbr.compareto(other.pnbr); public class NameComparator implements Comparator<Person> { public int compare(person p, Person p2) { return p.getname().compareto(p2.getname()); PFK (Föreläsning 0) HT 205 38 / 52 Exempel på användning av klassen HashSet // Denna mängd kommer att ordnas efter personnummer TreeSet<Person> nbrset = new TreeSet<Person>(); // Denna mängd kommer att ordnas efter namn Set<Person> nameset = new TreeSet<Person>(new NameComparator()); set.add(new Person("Kalle", "340609-234")); set.add(new Person("Kajsa", "37009-222")); Person p = new Person("Kalle", "340609-234"); Person p2 = new Person("Kajsa", "37009-222"); nbrset.add(p); nbrset.add(p2); nameset.add(p); nameset.add(p2); Antag vi vill vill sätta in Person-objekt i en mängd av typen HashSet. HashSet<Person> set = new HashSet<Person>(); Person p = new Person("Kajsa", "37009-222"); set.add(p); boolean found = set.contains(new Person(null, "37009-222")); Nu måste equals och hashcode skuggas i klassen Person. I equals ska personnumren jämföras. I hashcode ska en hashkod för personnumret beräknas. PFK (Föreläsning 0) HT 205 39 / 52 PFK (Föreläsning 0) HT 205 40 / 52
Skugga metoderna equals och hashcode Om man glömmer skugga hashcode Inuti klasserna HashSet och HashMap används metoderna hashcode() och equals(object) för att hitta ett element: Med x.hashcode() % tablesize beräknas först platsen för elementet med nyckel x. Sedan söks x i listan på denna plats. I samband med denna sökning används equals. Om vi glömmer att skugga hashcode i Person hittar vi troligen inte personen: När personen p sätts in beräknas hashkoden för objektet som p refererar till. När vi söker efter personen baseras sökningen på hashkoden av det objekt som är parameter till contains-metoden. Detta är ett annat objekt (men med samma personnummer). Sökningen utgår från den plats denna senare hashkod anger och med största sannolikhet är det i en helt annan del av tabellen än den där personen sattes in. PFK (Föreläsning 0) HT 205 4 / 52 PFK (Föreläsning 0) HT 205 42 / 52 Klass som skuggar equals och hashcode Klassen Person ADT lexikon (Map) public class Person { private String name; private String pnbr; // konstruktor och övriga metoder public boolean equals(object other) { if (other instanceof Person) { return pnbr.equals(((person) other).pnbr); else { return false; public int hashcode() { return pnbr.hashcode(); PFK (Föreläsning 0) HT 205 43 / 52 I ett lexikon (Map) betraktas element som tvådelade en nyckel och tillhörande värde. Nyckeln avbildas (eng. maps) på sitt värde. Nycklar är unika, men inte värden. Man använder nyckeln för att söka tillhörande värde. Exempel: nyckel = månad, värde = antal dagar i månaden. nyckel = personnummer, värde = Person-objekt med namn, adress. PFK (Föreläsning 0) HT 205 44 / 52
Interfacet Map i java.util Interfacet Map -ett urval av metoderna AbstractMap Map SortedMap public interface Map<K,V> { V get(object key); boolean isempty(); V put(k key, V value); V remove(object key); int size(); Set<K> keyset(); Collection<V> values(); Set<Map.Entry<K,V>> entryset(); HashMap TreeMap public interface Entry<K,V> { K getkey(); V getvalue(); V setvalue(v); PFK (Föreläsning 0) HT 205 45 / 52 PFK (Föreläsning 0) HT 205 46 / 52 Klasser som implementerar Map Exempel på användning av klassen TreeMap TreeMap implementerar interfacet SortedMap. Använder balanserat binärt sökträd Operationen keyset() garanterar att den returnerade mängden är ordnad. keyset().iterator() ger därför en iterator som går igenom nycklarna i växande ordning. Ytterligare operationer som bygger på ordning mellan nycklarna finns. HashMap Använder öppen hashtabell. Map Map<String, Integer> map = new TreeMap<String, Integer>(); map.put("januari", 3); map.put("februari", 28); map.put("mars", 3); map.put("april", 30); map.put("maj", 3); System.out.println("Antal dagar i mars: " + map.get("mars")); HashMap SortedMap TreeMap I en TreeMap är det nyckelklassen som måste implementera Comparable. I exemplet har nycklarna typen String. Klassen String implementerar Comparable<String>. PFK (Föreläsning 0) HT 205 47 / 52 PFK (Föreläsning 0) HT 205 48 / 52
Exempel på användning av klassen TreeMap, forts Interfacet Map.Entry En Map används normalt för att med nyckeln hitta motsvarande värde. Ibland behöver man göra tvärtom: System.out.println("Månader med 3 dagar:"); for (Map.Entry<String, Integer> e : map.entryset()) { if (e.getvalue() == 3) { System.out.println(e.getKey()); Metoden entryset returnerar en mängd med alla nyckel-värde-par. Genom att traversera denna mängd kan vi ta reda på vilka nyckel-värde-par som har värdet 3 och skriva ut motsvarande nyckel. Map.Entry är ett inre interface som är nästlat i interfacet Map. /* Representerar ett nyckel-värdepar */ public interface Entry<K,V> { K getkey(); V getvalue(); V setvalue(v); // ändrar värdet till V och // returnerar det gamla värdet Operationen entryset returnerar en mängd (Set) av Entry-objekt. d.v.s. objekt av en klass som implementerar interfacet Map.Entry PFK (Föreläsning 0) HT 205 49 / 52 PFK (Föreläsning 0) HT 205 50 / 52 Exempel på användning av klassen HashMap Exempel Exempel på användning av klassen HashMap Exempel 2 Map<String, Integer> map = new HashMap<String, Integer>(); map.put("januari", 3); map.put("februari", 28); map.put("mars", 3); map.put("april", 30); map.put("maj", 3); System.out.println("Antal dagar i mars: " + map.get("mars")); I en HashMap måste nyckelklassen skugga equals och hashcode. I exemplet har nycklarna typen String. Klassen String skuggar equals och hashcode. Antag vi vill vill sätta in Person-objekt i en hashtabell (HashMap). Personens personnummer ska vara nyckel. Map<String, Person> map = new HashMap<String, Person>(); map.put("37009-222", new Person("Kajsa", "37009-222")); Person p = map.get("37009-222"); if (p!= null) { Här har nycklarna typen String, och i denna klass är redan equals och hashcode skuggade. PFK (Föreläsning 0) HT 205 5 / 52 PFK (Föreläsning 0) HT 205 52 / 52