NITH PG00 Algoritmer og datastrukturer Løsningsforslag Eksamen.juni 0 Dette løsningsforslaget er til tider mer detaljert enn det man vil forvente av en eksamensbesvarelse. Det er altså ikke et eksempel på en perfekt besvarelse, men snarere en diskusjon av oppgavene. Oppgave a) f a (n) tilhører vekstklassen O(n ) b) f b (n) tilhører vekstklassen O(.0 n ) c) fua har kjøretid av orden O(n ) d) fub har kjøretid av orden O(n log n) Oppgave a) Når vi søker sekvensielt i en liste starter vi i den ene enden av listen og går gjennom alle elementene helt til vi eventuelt finner det vi søker etter. Når vi søker binært ser vi først på elementet som ligger på midten av listen, og vurderer om søkemålet ligger til høyre eller til venstre. Deretter søker vi videre etter søkemålet i den halvdelen det eventuelt ligger i. Vi kan bruke binært søk kun i sorterte lister. Man må søke sekvensielt i usorterte lister. Man kan hevde at det er meningsløst å bruke binært søk i sorterte lenkede lister, i og med at listen må traverseres sekvensielt. Hvis sammenligningsoperaasjonen (I java vanligvis compareto) er kostbar, så kan man dog forsvare å bruke binært søk i lenkede lister.
b) Slik kan man implementere sekvensielt søk i java: public static int search(int[] data, int n) { for (int i = 0; i < data.length; i++) if(n == data[i]) return i; return -; } Oppgave a) Vi lar variabelen n betegne antall elementer i linsten list. loop : Her vil list.get(i) ha konstant kjøretid, og løkken får derfor kjøretid av orden O(n). loop : Her vil iteratoren sørge for at iter.next() har konstant kjøretid, så løkken har kjøretid av orden O(n). En liten ulempe med denne løkken er at vi må lage et iterator-objekt. Når vi har med en ArrayList å gjøre er et slikt iterator-objekt meget enkelt: Det behøver kun å huske på indeksen til neste element. loop : list.remove(0) har kjøretid av orden O(n): Når vi fjerner elementet som ligger i indeks 0, blir alle de etterfølgende elementene kopiert ett steg til venstre. Denne løkken har altså kjøretid av orden O(n ). Som vi ser, så skiller loop seg ut her, med sin overlegent dårlige kjøretid. En fordel med loop er at vi ikke forholder oss direkte til hvordan iterasjonen foregår. En fordel med loop er at vi slipper overhead knyttet til iterator-objeketet. I indekserte lister som ArrayList er derfor loop et naturlig valg. b) loop : list.get(i) har kjøretid av orden O(n): For å komme fram til element i i listen, er vi nødt til å bevege oss sekvensielt gjennom listen. Hele løkken har kjøretid av orden O(n ) loop : Her vil iter.next() har konstant kjøretid, så hele løkken får kjøretid av orden O(n). loop : list.remove(0) innebærer kun flytting av et par pekere, så denne operasjonen har konstant kjøretid. Hele løkken har kjøretid av orden O(n).
Som vi ser, så skiller loop seg ut med sin overlegent dårlige kjøretid. Her bør vi kanskje holde oss til loop. Det er lite overhead knyttet til iteratoren: Den behøver kun å holde styr på hvor det neste objektet ligger. loop kan nok forventes å være raskest, men her må vi betale den kostnaden at vi ødelgegger list. I lenkede lister som LinkedList er loop et naturlig valg. Oppgave a) (iv) Selection sort. I linje k ligger de k minste elementene sortert først i listen. Man går videre til neste linje ved å finne fram til det minste av de n k gjenværende elementene og flytte det på plass. b) (ii) Insertion sort. I linje k er de k første elementene sortert. Man går videre til neste linje ved å plassere element k + på riktig plass blant de k + første elementene. c) Stoppbetingelsen er len < 7. Når stoppbetingelsen inntreffer sorteres listen med Insertion sort. d) Man har sannsynligvis tenkt at Insertion sort er mer effektiv enn Quick sort på lister av lengde mindre enn 7, til tross for at Quick sort vanligvis har lavere kjøretid på lengre lister. Det følgende er er langt hinsides det som forventes på eksamen: For stoppbetingelsen len < k kan vi gjøre følgende analyse: Quick-sort deler opp listen i m n/k segmenter med lengder n, n,..., n m der n i < k. Siden Insertion sort har kvadratisk kjørtid, finner vi en en konstant K slik at sorteringen av segment i har kjøretid mindre enn Kn i. Den totale kjøretiden for sortering i Insertion sort blir da Kn + Kn + + Kn m < Kk(n + n + + n m) < Knk. Oppbrytingen av listen i de m underlistene krever et logaritmisk antall nivå med sammenligning av n elementer. Dette gir en kjøretid omkring Ln log m = Ln log n k Dette gir følgende estimat for den totale kjøretiden for quick-sort: Knk + Ln log n k Nå kan vi estimere K og L og forsøke å finne den verden av k som gir best kjøretid når n er fastsatt. Dersom vi deler uttrykket for kjøretiden på n, ser vi at vi søker den k som gjør uttrykket Kk + L log nk minst mulig. Vi deriverer dette utttrykket som funksjon av k, og ser at vi har et minumumspunkt for k = L K. Det er flere usikkerhetsmomenter i denne beregningen: Vi er avhengige av gode estimater for L og K. Dessuten bør vi merke oss at alle kjøretidsestimater for Quick sort har stor usikkerhet, i og med at Quick sort er ganske sensitiv for detaljer i datamaterialet.
Oppgave 5 a) Et balansert søketre er karakterisert ved at de ulike undertrærne er omtrent like høye. For binære søketrær med en balanseringsmekanisme vil høyden være av orden O(log n). Ubalanserte søketrær kan degenerere til lenkede lister. I så fall vil søk og innsetting ha worst case kjøretid av orden O(n). I et balansert tre vil slike operasjoner ha kjøretid av orden O(log n). Det er en viss overhead knyttet til balanseringsmekanismen, men den er også logaritmisk i gode implementasjoner, så det spiller ingen rolle i denne grove kjøretidsanalysen. b) Det forventes bare at studenten fremstiller sluttresultatet. Her viser vi hele prosessen: Punkt (i): Må balanseres. V-rotasjon Balanseret
Punkt (i): Må balanseres. RV-rotasjon c) En hash-funksjon er en funksjon som produserer et et bestemt heltall (hash-koden) knyttet til hvert objekt, på en mest mulig usystematisk måte. Hash-koden kan brukes som utgangspunkt for plassering av objekter i en indeksert liste. Indeksen beregnes gjerne ved index = hashcode % list.length På denne måten kan man beregne indeksen tilhørende et objekt direkte, uten å vite noe om de andre objektene vi arbeider med. Dette kan gi meget effektiv søk og lagring. Det er bare en hake med dette: To objekter kan få samme indeks. Det finnes flere strategier for å håndtere dette. Fellesnevneren er sekvensiell gjennomgang av visse alternativer, men det er en annen historie. Oppgave 6 a) Hvis vi anvender denne metoden på listen,,,,..., n, så vil treet T degenerere til en lenket liste, og operasjonen T.add får kjøretid av orden O(n). Hele den første løkken får da kjøretid av orden O(n ). Dermed kan ikke worst case kjøretid for treesort være bedre enn O(n ). 5
b) Nå vil T.add ha kjøretid av orden O(log n), og den første løkken har kjøretid av orden O(n log n). Den andre løkken innebærer iterasjon gjennom treet, og vil har kjøretid av orden O(n). Kjøretiden for treesort blir dermed O(n log n). c) Her møter vi på Quick sort-spøkelset igjen: Når listen er sortert, så ender vi opp med kvadratisk kjøretid. En mulig forberding er følgende: Legg elementene inn i treet i tilfeldig rekkefølge. Man kan tenkes å skrive T.add(A.removeRandom ()) e.l. Denne metoden gjør at infernalske påfunn som listen,,,..., n ikke lenger gir kvadratisk kjøretid. På den annen side, så har vi ingen garanti mot at den tilfeldige rekkefølgen ikke tilfeldigvis er,,,..., n. Vi erstatter systematiske problemer med tilfeldige problemer. Dersom vi har grunn til å tro at dataene vi arbeider med er sortert, så kan dette altså fungere fint, siden frekvensen av sorterte lister kan bli lavere. Dermed kan gjennomsnitlig kjøretid bli bedre. Dersom vi ikke vet noe om datane som skal sorteres, så finnes det ingen grunn til å tro at denne randomiseringen hjelper stort. En annen mulig forbedring er inspirert av teknikker for fobedring av quicksort: Velg ut tre tilfeldige elementer og la A være medianverdien. Legg denne inn i roten i treet. Bruk samme teknikk på de elementene som skal legges inn i venstre og høyre undertre. I praksis kan dette gjøres på mange måter, og i noen av dem vil treesort bli veldig lik Quick Sort. 6
Oppgave 7 a) Grafen blir seende slik ut: B A C D Dette gir sclength = 7, lengden til veiene A, C, B, D, A og A, B, C, D, A, så sclength <= totallength er false. solveproblem returnerer altså false. b) solveproblem løser Hamiltonian cycle problem. Metoden svarer på om det finnes en rundreise i grafen som besøker hver node eksakt én gang. Vi vet at Hamilton cycle problem er NP-komplett. solveproblem innebærer en reduksjon av dette problemet til findshortestcircuit. Følgelig løser findshortestcircuit et NP-hardt problem. Vi skal altså ikke forvente at findshortestcircuit har polynomisk kjøretid. 7