F5: Debriefing OU2, repetition av listor, träd och hashtabeller Carl Nettelblad 2017-04-24
Frågor Kommer nog inte att täcka 2 timmar Har ni frågor på OU3, något annat vi har tagit hittills på kursen, listor och träd specifikt? Fundera, så tar vi dem i helgrupp eller framme vid tavlan mot slutet
Recursive descent Gå från vänster till höger Titta bara på nästa token Rekursera ned olika djupt Var upptäcks de flesta fel? Längst ut Metoderna returnerar innan hela uttrycket är tolkat Längst in Primary fastnar på något den inte förstår
Hur hantera fel? (T.ex.) primary vet precis vad som gått fel Vet inte hur man kom dit Rapportera precis vad felet Med egna undantagsklasser Syntaxfel Ett viss uttryck är alltid felaktigt Evalueringsfel Att faktiskt beräkna uttrycket, just nu, går inte
Fånga felen Fånga undantag har två syften Rapportera felet I vårt fall, ta med info om aktuellt token i rapporten Hantera felet Vad gör vi nu? Hela programmet ska inte avslutas, ska fortsätta fungera normalt Exempel: Läsa till slutet av raden» Kunde vara stänga filer, starta om en bana, nollställa en beräkning, parkera en styrd utrustning i ett säkert läge, logga ut användaren
Kompilatorfel eller oväntade resultat vid körning Sätt att hantera fel Oväntat resultat/krasch Rapporterat undantag Hitta fel i förberedda tester Hitta fel i kompilering
Primary public double primary() { double result = 99999; if (tokenizer.isnumber()) { result = tokenizer.getnumber(); tokenizer.nexttoken(); } else return result; } Vad händer om vi missar att sätta result i ett fall? Får 99999 vid körning
Kompilatorfel eller oväntade resultat vid körning Sätt att hantera fel Oväntat resultat/krasch Rapporterat undantag Hitta fel i förberedda tester Hitta fel i kompilering
Primary public double primary() { double result = 99999; if (tokenizer.isnumber()) { result = tokenizer.getnumber(); tokenizer.nexttoken(); } else } else { throw new SyntaxException("unhandled case in primary"); } return result; } Vad händer om vi missar ett fall?
Kompilatorfel eller oväntade resultat vid körning Sätt att hantera fel Oväntat resultat/krasch Rapporterat undantag Hitta fel i förberedda tester Hitta fel i kompilering
Primary public double primary() { double result; if (tokenizer.isnumber()) { result = tokenizer.getnumber(); tokenizer.nexttoken(); } else return result; } Vad händer om vi missar att sätta result i ett fall? Kompileringsfel! Otilldelad variabel
Kompilatorfel eller oväntade resultat vid körning Sätt att hantera fel Oväntat resultat/krasch Rapporterat undantag Hitta fel i förberedda tester Hitta fel i kompilering
functionhelper private double functionhelper(string name, double val) { if (name.equals("cos")) { return Math.cos(val); } else if (name.equals("sin")) { return Math.sin(val); } } else { return Math.log(val); } } Vad händer om name inte är något av de godkända namnen?
Kompilatorfel eller oväntade resultat vid körning Sätt att hantera fel Oväntat resultat/krasch Rapporterat undantag Hitta fel i förberedda tester Hitta fel i kompilering
functionhelper private double functionhelper(string name, double val) { if (name.equals("cos")) { return Math.cos(val); } else if (name.equals("sin")) { } return Math.sin(val); } else if (name.equals("log")) { return Math.log(val); } else { throw new SyntaxException("Function " + name + " not supported."); } }
Kompilatorfel eller oväntade resultat vid körning Sätt att hantera fel Oväntat resultat/krasch Rapporterat undantag Hitta fel i förberedda tester Hitta fel i kompilering
Länkade listor Låt varje element peka på sin efterföljare Precis som ArrayList finns LinkedList i Java I många fall är ArrayList bäst Men inte om man vill lägga till/ta bort element någon annanstans än nära slutet
Fler varianter Dubbellänkad, cirkulär lista Lättare att nå sista elementet, lättare att röra sig bakåt i listan Men alla algoritmer krångligare
Klasser för länkade listor Precis som ArrayList finns LinkedList i Java Men nu ska vi inte använda den! Vi ska skriva vår egen klass Övning för att själv kunna skapa mer avanceade grafer och datastrukturer Övning i rekursivt tänkande En listan kan vara Tom Eller ett element, som i sig pekar på en lista
Exempel på enkellänkad lista Varje element pekar på sin efterföljare Inte föregångare! Hitta föregångare lika dyrt som att hitta visst index Varje element representeras med klassen Node Rekursiv definition, en Node har en referens till en (annan?) Node class Node { int data; Node next; }
Klassen List public class List { private Node first; private Node last; // Optional
Klassen List public class List { private static class Node { int data; Node next; Inre klass. Vilken skillnad gör static? } Node(int d, Node n) { data = d; next = n; } private Node first; private Node last; // Optional...
tostring public String tostring() { } return "[" + tostring(first) + "]"; private static String tostring(node n) { if (n==null) { return ""; } else { return n.data + " " + tostring(n.next); } } Varför static? Vanligt mönster med hjälpmetod som vandrar rekursivt genom elementen Skulle kunna skrivas iterativt också, ibland svårare
Dubbellänkad cirkulär Många operationer möjliga, men alla algoritmer blir mer komplicerade och lagringen kräver mer minne Enkellänkad kan ofta vara enklast och effektivast Ibland med tillägget att listobjektet också håller reda på direktpekare till sistaelementet, och/eller totala antalet element
Hjälpmetoder Publika metoder finns för att ge ett tydligt kontrakt till omvärlden Så här använder du den här klassen Hjälpmetoderna finns för att ge dig en tydlig struktur inne i klassen Kan göra saker som är förbjudna utifrån Kan ta andra parametrar och returnera andra värden än de publika metoderna Klassmetoder (static) om all information finns i parametrarna och returvärdet
Träd Ett (rotat) träd är en mängd noder där En speciell nod kallas rot Övriga noder delas in i m disjunkta delmängder som i sin tur är rotade träd Rekursiv definition! Inga cykler Tydlig riktning på relation mellan noder
Binära träd Ett binärt träd är en mängd noder som antingen är tom eller som består av tre olika delmängder Exakt en nod: roten Vänster delträd Höger delträd Observera att vänster delträd kan vara tomt Höger delträd något annat än bara andra barnet
Operationer på träd Orientering, vandring mellan noderna Traversering, besök alla noder Preorder, postorder, inorder Sökning efter nodinnehåll eller position Inlägg/uttag av noder Sammanfogning av flera träd Mätning av höjd, bredd, antal noder, väglängd, balans
Representera binära träd i kod Precis som med listor skapar vi nodobjekt Som refererar till andra nodobjekt class Node { } int data; Node parent; Node left; Node right;... Förälderpekaren är ofta onödig, jämför enkellänkad/dubbellänkad lista.
Exempel: BinarySearchTree En klass som lagrar textsträngar som data i ett binärt träd Trädet är ett binärt sökträd, med följande regel för alla noder Varje nyckel (unik sträng) finns alltså endast en gång Praktisk struktur för sökning, inläggning och urtag Men inte oproblematisk
Nodrepresentation private static class Node { private String value; private Node left, right; } private Node(String value, Node left, Node right) { this.value = value; this.left = left; this.right = right; }
Placering av klassen public class BinarySearchTree { private static class Node {... } private Node root; public BinarySearchTree() { root = null; }
tostring public String tostring() { return tostring(root); } private static String tostring(node r) { if (r==null) { return ""; } else { return tostring(r.left) + " " + r.value + " " + tostring(r.right); } } Vilken typ av traversering är detta? Preorder, postorder, inorder?
Lämna till hjälpmetod public void add(string value) { root = add(root, value); } Lägga till element
Lägga till element private static Node add(node r, String value) { } if (r==null) { } return new Node(value,null,null); int compare = value.compareto(r.value); if (compare<0) { r.left = add(r.left, value); } else if (compare>0) { } r.right = add(r.right, value); return r; Fortsätt tills underträd saknas och lägg in ny nod där med värdet Låt jämförelse styra önskat värde Eller stanna om befintligt element har samma värde Jämför med likheterna i List
Egenskaper hos binära träd Ett binärt träd med k fyllda nivåer innehåller n noder n = 1 + 2 + 4 + + 2 k 1 = 2 k -1 k = log 2 (n + 1) log 2 n I ett sådant träd blir arbetet för Sökning O log n Inlägg O log n Uttag O log n
Sämsta möjliga träd I ett sådant träd blir arbetet för Sökning O n Inlägg O n Uttag O n Trädet en linjär lista Algoritmerna blir i praktiken som algoritmerna för enkellänkad
Vad händer för mitt träd? Vad gäller i genomsnitt? Vad är genomsnitt? I genomsnitt i ett genomsnittligt träd? Tre mått som påverkar trädets prestanda: Höjd ger rekursionsdjup i värsta möjliga fall Värsta möjliga höjd lika med antalet element Intern väglängd: för lyckad sökning Extern väglängd: för misslyckad sökning
Intern väglängd Intern väglängd (ipl, eller bara i) kan definieras som summan av nivån för alla noder Roten har nivå 1, rotens barn nivå 2, Exempel: i = 1 + 2 * 2 + 4 * 3 + 4 * 4 = 33 Att hitta en lagrad nyckel tar då i genomsnitt i n = 33 11 = 3 steg
Extern väglängd Extern väglängd (EPL, eller bara E) tar med externa noder dessutom Den externa väglängden definieras som summan av de externa nodernas nivåer (4 * 4 + 8 * 5 = 56 för exemplets 12 externa noder) Varje sökning efter en nyckel som inte finns slutar i en extern nod. Eftersom antalet externa noder är n + 1 blir det genomsnittliga antalet försök för misslyckad sökning E/(n + 1) Extra: Bevisa att antalet externa noder är n + 1 och att E = i + 2n + 1
Iläggningsordning Hur ett binärt träd växer beror på i vilken ordning noderna läggs i Praktiska implementationer kan ibland också balansera om trädet AVL-träd, rödsvarta träd En definition på hur bra trädet är blir hur välordnat det blir i medeltal över alla möjliga iläggningsordningar Den interna väglängden för alla träd som bildas om man lägger in alla n! permutationer av n nycklar blir 1,38 n log 2 n + O n I ett slumpmässigt träd tar då sökning O(log n) operationer
Binära sökträd i Java TreeSet<E>, TreeMap<K,V> är sådana ombalanserande binära träd. De garanterar O(log n) för inlägg, sökning och urtag för varje specifikt träd.
Alla binära träd är inte sökträd
Alla binära träd är inte sökträd Aritmetiska uttrycket 1 * (2 3) + (4 + 5)/6 Trädet styr evalueringsordning, inga parenteser Värdet beräknas genom att rekursivt: Beräkna värdet av vänster träd Beräkna värdet av höger träd Utföra operationen på de två beräknade värdena
OU3 Utgå från beskrivningen av enkellänkade listor och binära träd Fundera över vilka rekursiva hjälpmetoder som blir nyttiga Fundera över tidskomplexitet Se till att skriva egna testfall för BST, lita inte bara på automattesterna
Hashtabeller Vi har sett att man kan göra sökträd Oftast i en struktur är det viktiga att kunna lägga in, söka och ta bort Inte traversera i viss ordning Tidskomplexitet Lägg in Sök Ta bort Sorterad array Θ(n) Θ(log n) Θ(n) Länkad lista Θ(n) Θ(n) Θ(n) Binärt sökträd Θ(log n) Θ(log n) Θ(log n) Går det att göra bättre? Inte om vi bara kan jämföra enskilda element
Hashtabeller Vi kanske kan använda mer information än bara en sorterings-/jämförelseregel. Arrayer är snabba. Lagra alla element i en array, välj index utifrån en hashfunktion. Samma objekt leder alltid till samma hashvärde. Blir värdena alltid unika? Oftast inte. Hur gör man då? Leta efter en annan plats i arrayen. eller Använd en länkad lista för alla element som kolliderar. Vi tittar på alternativet med en länkad lista.
Heltal som element Tänk att vi har en liten tabell med 7 platser och hashfunktionen h(k) = k mod 7 (rest vid heltalsdivision) som hashfunktion. k 27 4 7 42 3 14 81 26 13 8 h(k) 6 4 0 0 3 0 4 5 6 1 Väljer att hålla listorna sorterade eftersom man vid inlägg ändå måste kontrollera att elementet inte redan är inlagt.
Tidskomplexitet Antag att vi vill lagra n nycklar i en array med m platser Den genomsnittliga listlängden blir då α = n m fyllnadsgraden som kallas Om α 1 och om hashfunktionen sprider elementen någorlunda jämnt över arrayen blir strukturen effektiv för inlägg, sökning och urtag. Misslyckad sökning: 1 + α försök Lyckad sökning: 1 + α 2 försök Fördjupad analys i separat dokument
Tidskomplexitet Om tabellstorleken ungefär motsvara antalet element och hashfunktionen ger bra spridning tar sökning, inlägg och urtag O(1) tid Nära på lika snabbt som vanlig array Trädstrukturer är O(log n) Dessutom är varje operation ofta dyrare
Skapa hashfunktioner Enormt viktiga att hashvärdet är någorlunda unikt Även när man tar modulo något mindre tal För heltal: h(k) = k Blir k mod m i praktiken för hashtabell storlek m Teckensträngar Konstruera ett heltal utifrån teckenkoderna, se mer i länkat dokument Ofta viktigt att samma teckenföljd i annan ordning inte får samma kod
Hashstrukturer i Java HashSet<E> HashMap<K, V> Ofta bättre (snabbare) än TreeSet, TreeMap Samma metoder Olika implementationer av gränssnitten (interface) Set och Map Går att skriva kod som ska ta en Map och sedan ge den antingen en TreeMap eller en HashMap Malin pratar mer om det nästa gång
Hashfunktioner i Java Java har inbyggda hashfunktioner Metoden int hashcode() finns i klassen Object Precis som tostring och equals Standardversionen beräknar hash utifrån referenslikhet D.v.s. om a == b har de också samma hashvärde I standardklasser som ersätter equals ersätts också hashcode Samma objekt bör inte byta hashvärde under sin livstid Tänk om du stoppar in ett objekt i en HashSet och sedan förändrar det så att det byter hashvärde
Sammanfattning Listor och träd är flexibla datastrukturer Grund för att komma fram till egna representationer Oerhört kraftfullt att ha objekt som refererar till objekt av samma typ Som Node Om du bara behöver en lista, använd ändå ArrayList Om du bara behöver en länkad lista, använd LinkedList HashMap och HashSet är bra grundval för att skapa avbildningar och lagra mängder Kräver korrekt implementation av hashcode och equals om referenslikhet inte räcker
Frågor?