Deadlines: first deadline 3rd May, final deadline 14th May.
(This lab is taken directly from last year’s course, hence the Swedish :) )
Inledning
På en aktiebörs handlar man med aktier i stora bolag. Man kan göra detta via internet om man har en banktjänst för aktiehandel. Man kan där ange om man vill köpa eller sälja ett visst aktieslag och ange det pris man är villig att betala.
Antag att du vill köpa aktier i Ericsson för 70 kr styck. Ditt bud kommer då att jämföras med säljkursen, dvs det lägsta pris någon säljare är villig att acceptera för sina Ericssonaktier. Om säljkursen är 70 kr eller lägre accepteras ditt bud och du får köpa aktierna för 70 kr. Man kallar en sådan genomförd affär ett "avslut". (Det är förstås oklokt att bjuda över lägsta säljkurs, men även sådana bud måste hanteras.) Om säljkursen är högre än 70 kr kommer ditt bud att läggas i Ericssons "orderbok" där alla ej genomförda bud registreras. Om det senare kommer in en ny säljare som är villig att sälja för 70 kr kommer ditt köp genomföras då i stället.
För att underlätta lösningen av uppgiften så ges här en beskrivning för hur ni bör gå till väga för att lösa den:
Översikt av Uppgiften
Er uppgift är att implementera ett Javaprogram för aktiehandel enligt ovan. För enkelhets skull behöver ni bara kunna handla med ett aktieslag och alla bud avser bara en aktie i taget. Det finns däremot flera olika säljare och köpare. Buden ska anges i hela kronor.
Orderboken består av två prioritetsköer: en för säljare och en för köpare. I prioritetskön för säljare innebär lägre pris högre prioritet. I prioritetskön för köpare innebär däremot högre pris högre prioritet. Om aktiehandeln är stor är det viktigt att implementera dessa prioritetsköer effektivt. Du ska därför implementera dem med (binära) heapar.
Lägga bud
En användare kan göra tre saker: köpa, sälja, samt ändra ett redan lagt bud. Exempelvis skriver vi
Ada K 70
om Ada vill köpa aktien för 70 kr. På motsvarande sätt skriver vi
Bengt S 71
om Bengt vill sälja aktien för 71 kr,
Bengt NS 71 70
om Bengt ändrar sitt säljbud från 71 till 70 kr (NS = ny säljkurs), och
Ada NK 70 72
om Ada ändrar sitt köpbud från 70 till 72 kr (NK = ny köpkurs).
Registrera avslut
Ni får anta att indata är sådant att varje person har som mest ett enda bud i prioritetskön.
I det fall att ett bud accepteras ska ert program registrera köpet på kommandoraden. I ovanstående fall ändrade Bengt sitt säljbud till 70 kr vilket matchar Adas köpbud. Då ska programmet skriva:
Ada köper från Bengt för 70 kr
Skriva ut orderböcker
Efter det att ditt program skrivit ut alla genomförda köp, ska det till slut skriva ut orderboken. Säljare och köpare ska listas i prioritetsordning enligt följande exempel:
Orderbok: Säljare: Cecilia 70, Bengt 71, David 73, Erika 77 Köpare: Ada 69, Fredrik 68, Gustaf 68
Ändring av prioriteten
I en binär heap har båda operationerna insättning och borttagning av högst prioriterat element värstafallskomplexiteten O(log n) (där n är köns storlek, jämförelser tar O(1) tid, och arrayen inte ändrar storlek). Att ändra en prioritet tar O(n) eftersom man i värsta fall måste söka genom hela heapen efter det element vars prioritet ska ändras.
Ni ska dock åstadkomma O(log n) även för ändringsoperationen genom att använda en uppdaterbar heap. I en uppdaterbar heap har men en hjälpdatastruktur som håller reda på var i heapen ett visst element är lagrat. Ni ska använda en hashtabell som lagrar elementens positioner i heapen. På det sättet kan man hitta ett givet element i heapen med komplexiteten O(1) (om man har en perfekt O(1) hashfunktion).
Format för indata och utskrift
Ert huvudprogram ska heta Lab2.java. Det har en parameter Budlista som är en fil med en lista av bud enligt ovanstående format med ett bud per rad:
Ada K 70 Bengt S 71 Cecilia S 70 Bengt NS 71 72 David K 72 Erik S 73 Frida S 71 Gustaf K 69 Hanna S 72
När ni skriver java Lab2 Budlista ska ert program skriva ut en lista av avslut följt av aktuell orderbok enligt följande format:
Ada köper från Cecilia för 70 kr David köper från Bengt för 72 kr Orderbok: Säljare: Frida 71, Hanna 72, Erik 73 Köpare: Gustaf 69
Det ska också gå att skriva:
java Lab2 < Budlista
Filens innehåll finns då i System.in.
Testning
Det finns ett testprogram som genererar indata (listor med kommandon av ökande storlek) för ert program. Var nog med att använd detta för att testa ert program efter varje steg innan ni går vidare!
För att vara säkra på att ert program fungerar så ska ni klara minst 2 miljoner testfall. Anledning för att köra så många test är att testen som skapas bygger listor med kommandon av ökande storlek där antalet testfall växer exponentiellt med längden av listan. Så ju fler testfall som testas, desto längre prioritetsköer skapas av ert huvudprogram. Om ni kör få testfall, så kan det hända att ni missar fel som endast uppstår för längre prioritetsköer.
Tänk på att testprogrammet endast gör det troligt att det inte finns något fel i er kod. Eftersom ni inte kan köra oändligt många testfall så bör ni tänka igenom er kod noga och inte gissa er till tills testprogrammet inte hittar några fel.
Spara testprogrammet (testing.jar) i samma katalog som ni har er kompilerade java-kod (dvs .class-filerna). Starta sedan en terminal och i samma katalog kör:
java -ea -jar testing.jar lab2test.Lab2GenTest Lab2
Om testprogrammet verkar hänga sig (ger inga utskrifter) så har ni antagligen en oändlig loop i ert program. Kör då istället:
java -ea -jar testing.jar -v 5 lab2test.Lab2GenTest Lab2
Det sista testfallet är då det fall som programmet hänger sig för.
Andra sätt att använda programmet
Det finns ett flertal flaggor som kan användas. För att se dem, kör:
java -ea -jar testing.jar --help java -ea -jar testing.jar lab2test.Lab2GenTest "Lab2 --help"
Ni kan t.ex hoppa över tidiga testfall med
java -ea -jar testing.jar -s 2000000 lab2test.Lab2GenTest Lab2
Det går också att köra:
java -ea -jar testing.jar -t -1 lab2test.Lab2GenTest "Lab2 -p"
för parallell testning. Detta testar eventuellt snabbare genom att köra flera testfall parallellt. Detta använder metoden pureMain(String[]) som tar en array med alla kommandon och returnerar en sträng med all utdata istället för att skriva till System.out. Användning av System.out/System.err bör i detta undvikas helt eftersom det då blandas mellan olika test och kan vara missvisande.
Ledning: Steg 1: komma igång
Ert program ska bestå av följande klasser:
Lab2
Detta är ert huvudprogram som det har beskrivits ovan. Utgå ifrån Lab2.java. Förstå ungefär vad den existerande koden gör (och vad de bitar som saknar ska göra) innan ni fortsätter. Ni kan vänta med att fylla i de bitar som saknas tills ni har börjat på er prioritetskö. Ni får ändra på den existerande koden hur mycket ni vill.
(en klass för bud)
Denna klass beskriver ett bud. Ni kan utgå ifrån Bid.java.
Se till så att inga instanser av klassen går att ändra (alla instansvariabler ska vara "final"). t.ex strängar går inte att ändra. se boken sidan 61. Implementera inte Comparable, använd separata komparatorer istället för att jämföra bud. Se nedan.
(två komparatorer)
Dessa är till för att beskriva prioriteten av elementen i era två prioritetsköer. Ni behöver en för säljare och en för köpare.
compare är vanligtvis kompatibel (se compare) med equals, alltså att compare ger 0 om och endast om equals ger true.
(en prioritetskö)
Ni måste använda en ArrayList med generiska element. Använd alltså inte en array. Konstruktorerna måste ta en komparator som parameter. Komparatorn använder ni för att bestämma prioritetsordningen av elementen i heapen.
Ni bör läst igenom kapitel 21.2 innan ni implementerar er prioritetskö. Ni kan också läsa om heapar på wikipedia. Utgå gärna från koden i kursboken på sidan 812-818. Ni behöver inte skriva av koden, utan hittar den på Companion Website, men kom ihåg att ändra paketnamn, klassnamn, författare, osv..
Ni måste inte implementera något specifikt gränssnitt, men ni måste på valfritt sätt minst implementera:
-
uttagning av minsta element
-
insättning
-
en oeffektiv uppdatering (tar gammalt och nytt element som parametrar)
…eftersom detta behövs i ert huvudprogram.
För att implementera en oeffektiv uppdatering så söker ni igenom hela heapen efter det element (använd equals eller comparatorn) vars prioritet ska ändras. Välj vad ni vill göra då ni inte hittar det. Om ni hittar det, så byt ut elementet mot det nya och jämför sedan med parent/left/right för att återställa heapegenskapen. Skapa 2 hjälpmetoder (t.ex percolateDown och percolateUp) som flyttar ett element neråt (eller uppåt) tills elementet är på en lämplig plats. percolateDown finns redan om ni utgår från koden i boken. Använd sedan dessa två metoder i koden för insättning, borttagning, och uppdatering.
Ledning: Steg 2: klassinvariant, testning, och debugging
klassinvariant
En bra idé är att beskriva den klassinvariant som måste uppfyllas och testa att den håller i början och slutet av varje anrop till publika metoder (vilket är exakt då den måste hålla). Använd assertions för att testa klassinvarianten. För att assertions ska testas när ni kör er kod så måste ni ange flaggan -ea, annars testas de inte.
Exempel på hur ni kan testa er invariant:
invariantCheck beskriver den invariant som ni har för er prioritetskö.
I koden ovan kommer showHeap att anropas när invarianten inte håller. Ni bör implementera denna metod så att den visar hur er heap ser ut. Detta gör att ni kan lättare komma på vad som gick fel.
testning
Skriv en fil med indata till ert program och se om det verkar fungera. Använd sedan testprogrammet för att se om ert program fungerar. Om testprogrammet ger indata som ert program ger fel utdata för, ger ett oväntat exception, eller då er klassinvariant inte håller, så måste ni analysera vad som gått fel.
debugging
Ni kan göra utskrifter för debugging var som helst i er kod, men använd då System.err (inte System.out). Testprogrammet skriver ut allt efter varje test. Hittar ni inte lätt ett fel som testprogrammet ger så spara den indata som testprogrammet antav i en fil och kör ert program själva med filen som argument. Använder ni eclipse så kan ni ange filens namn som argument, och använda debuggern som finns i eclipse för att stega igenom programmet.
Ledning: Steg 3: effektiv uppdatering
Ni ska nu implementera en effektiv uppdatering. Använd en Map<E,Integer> (se Map) för att hålla reda på var varje element ligger i heapen. Ni ska använda HashMap för detta.
För att använda HashMap måste ni implementera hashCode i Bid så att hashkoden är samma när equals säger att två bud är samma (se hashCode). Olika bud får ha samma hashkod, men detta är lämpligen osannolikt.
Om detta inte stämmer så får ni konstiga fel när ni använder HashMap. Tänk på hur hashCode används där. Ni bör använda hashCode-metoden på strängar.
Varje gång ni flyttar ett element i heapen så måste ni uppdatera er Map. Då kommer den alltid innehålla rätt index för varje element. När ni uppdaterar behöver ni nu bara fråga er Map för att ta reda på var de gamla elementet ligger istället för att leta igenom hela heapen på linjär tid.
Er invariant har nu ändrats, så ni måste ändra er kod som kollar om invarianten är korrekt. Tänk på hur er Map måste förhålla sig till heapen: Alla nycklar i er Map måste finnas med i heapen, och alla element i heapen måste finnas med i er Map. Heapen måste ha rätt värde för varje index som ni sparat i er Map.
Inlämning
Innan ni laddar upp era filer till fire och skickar in, se till att er kod uppfyller följande krav:
-
Ert program klarar minst 2 miljoner testfall. (Obs! Ni behöver inte skriva några egna tester – det medföljande testprogrammet sköter det åt er).
-
Ert program består av: huvudprogram (klass med main), prioritetskö (generisk över elementen som sätts in), 2 komparatorer, och en klass som beskriver bud.
-
Varje klass är i var sin fil. Koden är korrekt indenterad. Instansvariablerna är dokumenterade (inkludera gärna också en beskrivning av er klassinvariant).
-
Använd inte paket! Det blir bara mer komplicerat för oss att rätta då.
-
Det får inte finnas varningar vid kompilering av er kod. Använd inte SuppressWarnings eller liknande.
-
Ni måste använda hjälpmetoder percolateDown/percolateUp eller liknande vid insättning, borttagning, och uppdatering för att flytta element i heapen.
-
Methoden för att uppdatera måste ta två parametrar: det gamla elementet och det nya.
-
Tänk på lämplig användning av private/public för varje metod. Varje metod ska vara dokumenterad (med javadoc) så att de tydligt framgår vad implementationen av metoden ska göra. Tänk på alla möjliga värden för parametrar. Om ni t.ex antar att en parameter inte är null, så bör detta skrivas. Ange alltid tidskomplexiteten. Anger ni den amorterade (i genomsnitt över många anrop) tidskomplexiteten så var noga med att säga att ni gör det. Ni får gärna ange både den amorterade och icke-amorterade tidskomplexiteten. Tänk på den amorterade och icke-amorterade tidskomplexiteten för er ArrayList och hur den påverkar komplexiteten av era metoder. Ni får anta att alla jämförelser med komparatorer och equals tar O(1) tid, och att ni alltid har en perfekt hashfunktion som tar O(1) tid.
-
Inga anrop till System.out/System.err i någon annan klass än ert huvudprogram. Använd inte System.exit.
Extrauppgifter
Här är några extra uppgifter för de som har tid och vill lära sig mer:
-
Beskriv tidskomplexiteten för era metoder i prioritetskön utan att anta att alla jämförelser med komparatorer och equals tar O(1) tid eller att hashfunktionen tar O(1). Ni kan anta att hashfunktionen är perfekt. Ni kan införa en variabel för varje okänd (komparatorer, equals, hashCode) tid. Tänk noga på var och hur många gånger de olika metoderna anropas.
-
Om ni sätter in muterbara element i prioritetskön, vad för problem kan uppstå om ett element ändras efter att det har satts in?
För att lösa problemet så kan man separera prioriteten ifrån elementen som man sätter in. Man anger då elementet och prioriteten separat när man sätter in, och anger elementet och den nya prioriteten när man uppdaterar. Implementera en prioritetskö på detta sätt.
Detta kräver en hel del ändringar. Lämpligen håller ni reda på elementen och prioriteten ihop genom att skapa en inre klass som ni i sin tur använder som element i heapen. Prioriteten kan vara heltal. Alternativt gör ni en mer generell lösning genom att lägga till en generisk variabel, t.ex PriorityQueue<P,E>.