Frivillig reflektionsuppgift TDDC76 2016
Eric Elfving
Detta moment i kursen har som uppgift att knyta samman projektet och
era nyvunna kunskaper om vanliga datastrukturer och algoritmer. Förväntat
omfång är 500-1000 ord (exklusive kodexempel) och redovisas genom att skicka
er individuella lösning till er projektassistent via e-post i PDF-format.
1 Inbyggda datastrukturer i C++
I C++ har vi flera så kallade containrar. Varje container är en abstraktion
av ett visst sätt att lagra data. Även om standarden inte anger exakt vilken
datastruktur som ska ligga bakom respektive container ger de krav som ställs på
implementationen att de flesta väljer en viss datastruktur för en given container.
Nedan följer en lista över den vanligaste datastruktur som används för en specifik
container.
array Ett fält (array) med fast storlek.
vector En ren linjär fältstruktur likt array, men det interna fältet är dynamiskt
allokerat. Fältet har en fast storlek vilket innebär en omallokering och flytt
av samtliga element vid insättning när fältet är fullt.
deque En sekvens av flera fält som inte nödvändigtvis är i sekventiell ordning
i minnet. Detta gör att borttagning och instättning i början och i slutet
kan göras effektivt. Står för double-ended queue.
forward_list En enkellänkad lista.
list En dubbellänkad lista.
map, set Ett (röd-svart) binärt sökträd. Dessa kräver att elementtypen är jämförbar med operator<.
unordered_map, unordered_set Använder sig av en hashtabell där elementen sparas i en viss bucket baserat på dess hash. Detta kräver att vi
anger en hash-funktion som ska användas för just denna container.
stack, queue och priority_queue Dessa är så kallade contineradaptrar. Det
betyder att de har normalt sett ingen egen datastruktur utan bygger på
någon annan container. Anledningen att de finns är att göra användningen
mer abstrakt. Vill jag exempelvis ha flera värden men endast är intresserad
1
av det senast instoppade värdet är en stack tydligare att använda sig av
jämfört med exempelvis vector.
Din uppgift är att analysera koden i ert projekt och hitta en användning
av någon av ovanstående containrar. Du ska sedan visa på exempel på hur ni
använt denna container och resonera kring valet av container. Frågor som kan
vara bra att fundera kring kan vara “har det varit enkelt att använda sig av
denna container”, “är det (teoretiskt) rätt val av container eller bör vi använt
någon annan container (datastruktur) för ökad (algoritmisk) effektivitet” och
“finns det någon annan datastruktur (utöver de inbyggda containrarna) som
skulle passat bra för vårt användningsfall”.
Utifrån din analys ovan ska du ge ett kort kodexempel på hur du skulle
använt en annan container istället för den ni använt. Det räcker att visa något
specifikt användningsfall.
2 Algoritmer
Förutom containrar har standardbiblioteket implementationer av ett stort antal
standardalgoritmer. Gemensamt för dessa är att de tar minst ett intervall av
element som anges med så kallade iteratorer. En iterator är ett objekt som
refererar till ett specifikt element i någon container och kan stega igenom den
specifika container som användes för att skapa iteratorn. Koden i kodexempel
1 visar hur man skapar en iterator från en viss vector och sedan hur man kan
använda iteratorn för att ta fram värdet på en viss position och stega till andra
element.
Kodexempel 1: Exempel på iteratorer
v e c t o r <int> v a l s { 2 , 3 , 1 , 5 4 , 6 } ;
auto i t = v a l s . b e g i n ( ) ; // b e g i n g e r en i t e r a t o r som
// r e f e r e r a r t i l l f ö r s t a e l e m e n t e t
c o u t << * i t << e n d l ; // s k r i v e r u t 2
++i t ; // i t r e f e r e r a r nu t i l l andra e l e m e n t e t
auto s t o p = v a l s . end ( ) ; // end g e r en i t e r a t o r som r e f e r e r a r t i l l
// e t t t ä n k t e l e m e n t e f t e r d e t s i s t a ( 6 )
// värdena 3 t i l l 6 s k r i v s u t
while ( i t != s t o p )
{
c o u t << * i t << e n d l ;
++i t ;
}
Ett enkelt exempel på hur man använder algoritmerna är sort. För att sortera hela vectorn vals behöver vi endast skriva sort(vals.begin(), vals.end())
2
Alla algoritmer går inte att använda till samtliga containrar. Detta kan låta som
ett konstigt val, men om man tänker på den bakomliggande datastrukturen blir
det ganska självklart - hur sorterar vi ett binärt sökträd eller en hashtabell?
I standardbiblioteket har de valt att uttrycka kraven en algoritm ställer i
form av iteratorkategorier där random access går att använda till allt (ges exempelvis från vector) och forward är svårast att använda sig av. Namnen kommer
från att forward-iteratorer endast går att stega framlänges ett element i taget
genom vald datastruktur medan random access stödjer hopp i datastrukturen det går alltså alltid att beräkna var i minnet ett specifikt element finns.
Kodexempel 2: Exempel på random access-iteratorer
v e c t o r <int> v a l s { 2 , 3 , 4 , 5 , 6 } ;
auto i t { v a l s . b e g i n ( ) } ;
c o u t << * ( i t + 4 ) << e n d l ; // s k r i v e r u t 6
f o r w a r d _ l i s t <int> f l { 1 , 2 , 3 , 4 } ;
c o u t << * ( f l . b e g i n ( ) + 2 ) << e n d l ; // k o m p i l e r a r i n t e
Med algoritmerna kan man lösa väldigt många problem på ett effektivt och
lättläst sätt. I kodexempel 3 visas hur algoritmerna rotate och upper_bound
kan användas för att implementera insertion sort.
Kodexempel 3: Insertion sort med rotate
s t d : : v e c t o r <int> v { 2 , 4 , 2 , 0 , 5 , 1 0 , 7 , 3 , 7 , 1 } ;
fo r ( auto i = v . b e g i n ( ) ; i != v . end ( ) ; ++i ) {
s t d : : r o t a t e ( s t d : : upper_bound ( v . b e g i n ( ) , i , * i ) , i , i +1);
}
Gå tillbaka till er projektkod och hitta minst två kodstycken som skulle gå
att ersätta med ett anrop till en av standardalgoritmerna. För varje kodstycke
ska du noggrant beskriva vad koden utför och reflektera kring läsbarheten av
koden, både före och efter ändringen. Åtminstone en av algoritmerna bör vara
under en av följande kategorier på cppreference1 : Partitioning, Binary search,
Set, Heap.
1 http://en.cppreference.com/w/cpp/algorithm
3