1 (7) Algoritmer och datastrukturer Frivilliga bonusuppgifter Syfte Att ge ytterligare programmeringsträning i allmänhet, och i problemlösning med rekursion i synnerhet. Uppgifterna ger också några inblickar i syntax för programmeringsspråk. Mål Ett bra tentamensresultat. Genomförande och redovisning Uppgifterna löses och redovisas individuellt. Samarbete är ej tillåtet! Lösningar som förefaller vara kopierade beaktas ej. Vid oklarheter kan kontrollfrågor ställas. Kursledaren och assistenterna får givetvis tillfrågas om något är oklart. Du får utnyttja dina bonuspoäng vid ett av de tre tentamenstillfällena för årets kurs i juni, augusti eller oktober. Lösningarna lämnas in i form av pappersutskrift vid tentamenstillfället. Häfta ihop bladen, inga plastpärmar! Märk lösningarna med din tentamenskod (ej namn) och lämna dem till skrivningsvakten. Skicka inga lösningar med e-post! Lösningar rättas endast om bonuspoäng bedöms kunna bidra till högre skrivningsbetyg. Bonuspoäng adderas till skrivningspoängsumman innan skrivningsbetyget sätts. Bonuspoäng kan aldrig överstiga 1/6-del av poänggränsen för ett betyg, d.v.s. max 4 poäng för betyget 3 (gräns 24p), max 6 poäng för betyget 4 (gräns 36p) och max 8 poäng för betyget 5 (gräns 48p). Poängtalen som står efter uppgifterna avser maximala poäng för lösningar av god kvalitet. Litteratur För del B: Weiss kap. 11.2. Programkod Givna kodavsnitt finns på kursens hemsida under laborationsfliken i filen bonus.zip. Allmänt Alla funktioner skall vara rekursiva! (med några triviala undantag)
2 (7) Uppgift 1 Växla pengar Skriv en rekursiv funktion som returnerar hur många olika sätt det finns att växla ett belopp i hela kronor givet en uppsättning mynt- och sedelvalörer. int[] sekvalues = { 1, 5, 10, 20, 50, 100, 500, 1000 ; // parameters // amount the amount to be changed // val the different values arranged in ascending order // n the number of different values public static int change( int amount, int[] val, int n ); Exempel: Beloppet 12 kronor kan växlas på fyra olika sätt: 12x1kr, 10kr+2x1kr, 2x5kr+2x1kr, 5kr+7x1kr. Så change(12,sekvalues,sekvalues.length) skall returnera 4. (2 p) Uppgift 2 Permutationer a) Konstruera en funktion som skriver ut alla permutationer av en sträng. T.ex. skall anropet permutations( abc ); ge utskriften abc acb bac bca cab cba Ledning: Permutationerna av strängen S fås genom att för varje tecken c i S addera c till varje permutation av S där S fås genom att ta bort c ur S. Ex. addera a till varje permutation av bc, b till varje permutation av ac, och c till varje permutation av ab. Skriv en rekursiv hjälpfunktion som bygger upp en permutation i en extra ackumulerande parameter (för exempel på tekniken se fibiter i den första föreläsningen om rekursion). Funktionen får innehålla en loop. Anropa hjälpfunktionen från permutations. (2 p) b) Ange en lämplig tidsfunktion för permutations(s), T(n), där n är strängen s:s längd. Sätt upp rekurensekvationer för T(n) och lös dem. Redovisa härledningen och lösningen på formen T(n) = O(f(n)). (1 p)
3 (7) Syntax och grammatiker Inledning Övningen ger förhoppningsvis lite insikt i rekursiv parsning och evaluering av aritmetiska uttryck - hur enkelt det kan bli om man utgår från en formell grammatisk definition av språket som skall analyseras och bygger lämpliga datastrukturer för att representera uttrycken. Nedan följer en kort orientering om beskrivning av syntax för programmeringsspråk med formell grammatik. Grammatik för programmeringsspråk Formell syntax För alla språk finns syntaxregler som definierar vilka textsträngar som är grammatiskt korrekta fraser i språket. För programmeringsspråk, som är en typ av formella språk, brukar man definiera grammatiken i s.k. BNF, eller Backus-Naur Form, efter upphovsmännen John Backus och Peter Naur. En BNF-grammatik består av en samling produktionsregler. Några operatorer som används i BNF är ::=,,, <symbol>. Här följer ett exempel på en BNF-grammatik som beskriver en meny på ett sätt som du kanske inte sett förut. 1 En förklaring följer efter exemplet. <middag> ::= <förrätt> <huvudrätt> <dessert> <förrätt> ::= <soppa> <sallad> <soppa> ::= <kycklingsoppa> <rödbetssoppa> <sallad> ::= <Waldorfsallad> <västkustsallad> <huvudrätt> ::= <vegetariskt> <kött> <marint> <fågel> <vegetariskt> ::= <böngryta> <linsbiffar> <marint> ::= <fisk> <skaldjur> <fisk> ::= <havsfisk> <gös> <abborre> <havsfisk> ::= <lax> <hälleflundra> <pigghaj> <skaldjur> ::= <hummer> <krabba> <havskräftor> <räkor> <kött > ::= <oxfilé> <lammstek> <mungo> <fågel> ::= <fasan> <gås> <kalkon> <dessert> ::= <ostbricka> <äpplekaka> <frukt> <glass> <frukt> ::= <päronpåhallspegel> <mango> <glass> ::= <hallonsorbet> <storstrut> Produktionsreglerna i BNF har formen <symbol> ::= produktion. Sekvens och val kan användas i reglernas högerled när man vill uttrycka att det som produceras består av en följd av något, eller ett val mellan flera alternativ. Den första regeln ovan kan utläsas en middag består av en förrätt, följt av en huvudrätt, följt av en dessert. Den andra: en förrätt består antingen av en soppa eller av en sallad. Ordningen mellan reglerna spelar ingen roll, men man brukar sätta upp de övergripande reglerna först. En av symbolerna utgör grammatikens startsymbol (middag ovan). Reglerna i en BNF-grammatik definierar två huvudtyper av syntaktiska kategorier: 1 Oklart om restaurangen har en eller två stjärnor i Guiden.
4 (7) Terminaler o Terminaler är grammatikens grundsymboler - dess basfall. Terminaler är sig själva och refererar inte vidare till andra begrepp i grammatiken. De förekommer därför inte till vänster om ::=. o Ex. <abborre>, <fasan>. Icketerminaler o Icketerminaler är symboler till vänster om ::= som definieras genom att referera till terminaler, andra icketerminaler, eller rekursivt till sig själva. o Ex. <huvudrätt> ::=, <fisk> ::= Val Val mellan olika alternativ i produktionerna uttrycks med operatorn. Ett av alternativen måste väljas. Iteration Iteration uttrycks genom rekursiva regler, direkt eller indirekt. Vill man t.ex. formulera begreppet en sekvens av ett eller flera x skriver man <x-sekvens> ::= x x <x-sekvens> vilket utläses: En x-sekvens består av ett x eller ett x följt av en x-sekvens. Ex. x, xx, xxx, osv. För begreppet en sekvens av noll eller flera y behövs symbolen (epsilon) och vi skriver <y-sekvens> ::= y <y-sekvens> vilket utläses: En y-sekvens är antingen tom eller så är den ett y följt av en y-sekvens. Ex.,y, yy, yyy, osv. I rekursiva regler av typen ovan är det som står till vänster om rekursionens basfall. Ex. Vad är det för fel på denna regel? <dessert> ::= <glass> <dessert> 2 Syntes och analys En grammatik kan användas på två sätt: o För att producera korrekta fraser i språket. (Att komponera en måltid från menyn.) o För att analysera om en sträng är en korrekt fras i språket. Detta kallas parsning. (Att undersöka om något som serveras av restaurangen finns på menyn, eller om man råkat ut för här är vårt kylskåp.) Exempel: Aritmetiska uttryck Syntaxen för aritmetiska uttryck med konstanter och parenteser kan definieras med en enkel BNF-grammatik. I detta exempel skriver vi av praktiska skäl terminalerna inom. <expression> ::= <constant> <binary_expression> ( <expression> ) <binary_expression> ::= ( <expression> <operator> <expression> ) <operator> ::= + - * / % ^ <constant> ::= <digit> <digit> <constant> <digit> ::= 0 1 2 3 4 5 6 7 8 9 Exempel på korrekta uttryck: 0, 123, (123), (1+2), ((3-4)^7), ((1+(2))), men inte 1+2, (3-4^7) eller (-(1/2)). Operatorn / är heltalsdivision, % rest vid heltalsdivision och ^ upphöjt till. 2 Den är svår att implementera tyvärr.
5 (7) Parsning av uttryck I Weiss kap. 11.2 beskrivs tabellstyrd parsning med stack. En annan vanlig metod är rekursiv nedstigning (eng. recursive descent). Metoden bygger på att man definierar ömsesidigt rekursiva parsningsfunktioner med utgångspunkt från grammatiken. Vanligen definierar man en sådan funktion för varje icketerminal möjligen med undantag för enklare icketerminaler som kan hanteras på annat sätt (som de tre sista ovan). I vårt exempel skall vi låta parsningsfunktionerna bygga syntaxträd från uttrycken som matas in på tangentbordet. Uttryckets värde kan senare beräknas genom att analysera trädet med en rekursiv evalueringsfunktion. Eftersom detaljer som t.ex. parenteser inte skulle fylla någon funktion i trädet, och således inte finns där, brukar man säga att trädet är uttryckets abstrakta syntax, till skillnad från den konkreta syntaxen som definierades med BNF ovan. Exempel: Konkret syntax (((1+2)^3)-(4*5)) motsvaras av det abstrakta syntaxträdet - ^ * + 3 4 5 1 2 När uttrycket är översatt till abstrakt syntax är det mycket enklare för datorn att räkna ut dess värde än det skulle vara att försöka tolka dess konkreta syntax tecken för tecken. T.ex. får vi fram värdet hos syntaxträdet ovan genom att först rekursivt beräkna delträdens värden. Om dessa blir v 1 resp. v 2 så är har hela trädet värdet v 1 v 2, etc. Ett löv, d.v.s. en konstant har naturligtvis sig självt som värde. Datarepresentation för abstrakta uttrycksträd Följande mer eller mindre fullständiga klassdefinitioner är givna: public interface Expression { int getvalue(); void prettyinfix(); void prettypostfix(); void prettyprefix(); public class ConstantExpression implements Expression { private int value; public ConstantExpression (int value) { this.value = value;
6 (7) public class BinaryExpression implements Expression { private SymbolTypes op; private Expression leftoperand; private Expression rightoperand; public BinaryExpression(Expression left, SymbolTypes op, Expression right) { this.leftoperand = left; this.op = op; this.rightoperand = right; Uppgift 3 Överskugga de tre metoderna som skriver ut uttrycket i infix-, postfix-, resp. prefixform i subklasserna till Expression. Den konkreta syntaxen för de två sista formerna fås genom att byta ut produktionsregeln för binary_expression i grammatiken mot en av: <postfix_binary> ::= ( <expression> <expression> <operator> ) <prefix_binary> ::= ( <operator> <expression> <expression> ) samt motsvarande i högerledet för expression. Exempel: Infixuttrycket (((1+2)^3)-(4*5)) blir som postfix (((1 2 +) 3 ^) (4 5 *) -) och som prefix (- (^ (+ 1 2) 3) (* 4 5)). Prefixformen är alltså inte postfixformens omvändning, vilket man kanske kan tro. Testa på det abstrakta syntaxträdet som finns i huvudprogrammet: Expression testexpr = new BinaryExpression( new BinaryExpression ( new BinaryExpression ( new ConstantExpression(1), SymbolTypes.PLUS, new ConstantExpression (2)), SymbolTypes.EXP, new ConstantExpression (3)), SymbolTypes.MINUS, new BinaryExpression ( new ConstantExpression (4), SymbolTypes.MULT, new ConstantExpression (5))); (1 p) Uppgift 4 Överskugga metoden getvalue i ConstantExpression och i BinaryExpression. Metoden skall returnera uttryckets värde (evaluera uttrycket) om det är definierat. Givetvis är nolldivision odefinierat (gäller både / och %). Testa! (2 p)
7 (7) Uppgift 5 Skriv färdigt klassen Parser. import symbols.*; public class Parser { private SymbolReader symbolreader; public Parser( Reader instream ) { symbolreader = new SymbolReader(inStream); public Expression parse() throws SyntaxError { Symbol s = symbolreader.readnextsymbol(); Funktionen skall läsa symboler från tangentbordet med readnextsymbol och bygga syntaxträd av lämplig typ. En referens till trädet returneras. För inläsning av symboler (språkets ord ) finns klassen SymbolReader som är en anpassad version av Weiss:s Tokenizer-klass från kalkylatorprogrammet i kap. 12.2. Syntaxfel hanteras med public class SyntaxError extends Exception { public SyntaxError(String msg) { super(msg); Följande fel skall rapporteras: SyntaxError: illegal operator encountered SyntaxError: number or '(' expected SyntaxError: ')' expected Skriv ett program som testar parsern och uttrycksevalueringen i B.2. Tips: Definiera ömsesidigt rekursiva parsningsfunktioner för icketerminalerna <expression> och <binary_expression> i grammatiken. Anropa en av dessa från parse. När en parsningsfunktion anropas skall alltid den första symbolen i deluttrycket som skall analyseras vara inläst. Skicka denna som inparameter till funktionen. (3 p)