ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012 1.1. (a) Jaettava m, jakaja n. Vähennetään luku n luvusta m niin kauan kuin m pysyy ei-negatiivisena. Jos jäljelle jää nolla, jaettava oli tasan jaollinen. int m, n; // oletetaan, että molemmat > 0 while (m >= n) m = m - n; if (m == 0) return true; return false; (b) Lasketaan lukumäärää laskuriin. Aluksi osoitin c merkkijonon alkuun. Siirretään c:tä yhdellä askeleella eteenpäin ja verrataan ensimmäiseen merkkiin. Jatketaan kunnes tulee eri merkki tai kunnes merkkijono loppuu. string mjono; // oletetaan, että pituus > 0 laskuri = 1; // ainakin yksi sama merkki c = ensimmainen(mjono); while (c!= viimeinen(mjono) c = seuraava(c); if (c == ensimmainen(mjono)) laskuri = laskuri + 1; break; // lopetetaan silmukka return laskuri; 1.2. (a) Käydään koko taulukko läpi ja etsitään sekä suurin että pienin luku, minkä jälkeen voidaan laskea erotus. // taulukko t, koko n > 0 // Suoritusaika: suurin = t[0]; // vakio pienin = t[0]; // vakio for (i = 1; i < n; i++) // silmukka suoritetaan n-1 kertaa if (t[i] > suurin) // ehtolause joka kierroksella suurin = t[i]; // sijoituslause tarvittaessa if (t[i] < pienin) // ehtolause joka kierroksella pienin = t[i]; // sijoituslause tarvittaessa erotus = suurin - pienin; // vakio Suoritusaika muotoa T (n) = n t 1 + t 2 = O(n). 1
(b) Muuttuja j osoittaa viimeisimpään eri lukuun. Muuttuja i käy läpi taulukon loput luvut. Jos i ja j osoittavat samansuuruiseen lukuun, paikka i nollataan ja j ei muutu. Muuten j siirtyy i:n kohdalle. // taulukko t, koko n > 0 // Suoritusaika: j = 0; // vakio laskuri = 0; // vakio for (i = 1; i < n; i++) // silmukka suoritetaan n-1 kertaa if (t[i] == t[j]) // ehtolause joka kierroksella t[i] = 0; // sijoituslauseet laskuri++; // tarvittaessa j = i; // sijoituslause tarvittaessa // nollien lukumäärä on nyt muuttujassa laskuri Suoritusaika muotoa T (n) = n t 1 + t 2 = O(n). 1.3. Luentomateriaalin perusteella tiedetään: Kertaluokan määrää termi, joka kasvaa nopeimmin kun n kasvaa. Joidenkin peruskertaluokkien suuruusjärjestys on log 2 n, n, n log 2 n, n 2, n 3, 2 n. Yleisesti n k kasvaa sitä nopeammin mitä suurempi k on. Tämä pätee myös kun k ei ole kokonaisluku, toisin sanoen n = n 1/2 on kertaluokaltaan pienempi kuin n. Siten saadaan (a) f(n) = O(n 5 ) (b) f(n) = O(n) (c) f(n) = O(2 n ) (d) f(n) = O(n) (e) f(n) = O(n log 2 n) (f) f(n) = O(n) Tarkempi perustelu laskemalla raja-arvot: Tiedetään, että f(n) = O(g(n)), jos = C 0. Saadaan lim n f(n) g(n) (a) lim n 3n 5 + 3n 3 + 24n n 5 800 + 50 n + 3n (b) lim n n ( 4n 3 + 2 n 4n 3 (c) lim = lim n 2 n n (d) lim n 4 log 2 n + 10n n (e) lim n 3n log 2 n + 5n n log 2 n ( = lim 3 + 3 n n + 24 ) 2 n 4 = 3 ( 800 = lim n n + 50 ) + 3 = 3 n ) 2 + 1 = 1 n ( ) 4 log2 n = lim + 10 = 10 n n ( = lim 3 + 5 n log 2 n 2 ) = 3
(f) lim n 2n/ log 2 n + n n ( ) 2 = lim n log 2 n + 1 = 1 Kohtien (c) ja (d) raja-arvoja laskettaessa seuraava L Hôpitalin sääntö voi olla f hyödyllinen: Jos lim n a f(n) = lim n a g(n) = 0 tai ja raja-arvo lim (n) n a g (n) f(n) on olemassa, niin lim n a = lim g(n) n a f (n). [Ei tarvitse osata tällä kurssilla.] g (n) 1.4. (a) Minuutti = 60 000 ms, joten laskutoimituksia on N = 60 000 kpl. 25n = N = n = N/25 = 2 400 = 2 400 5n 2 = N = n = N/5 109.5 = 109 1 2 n3 = N = n = 3 2N 49.3 = 49 2 n = N = n = log 2 N = ln N/ ln 2 15.9 = 15 (b) Laskutoimitusten lkm kasvaa 10-kertaiseksi. Merkitään n old = m = 10 000. 25n = 10 25m = n = 10m = 100 000 = 100 000 5n 2 = 10 5m 2 = n = 10m 31 622.8 = 31 622 1 n3 = 10 1 2 m3 = n = 3 10m 21 544.3 = 21 544 2 n = 10 2 m = n = log 2 10 + m 10 003.3 = 10 003 1.5. (a) d = 0; // vakio for (i = 0; i < n; i++) // silmukka n kertaa c[i] = 0; // vakio for (j = 0; j < n; j++) // silmukka n kertaa c[i] += a[i][j]*b[j]; // vakio d += b[i]*c[i]; // vakio Sisimmän silmukan sisältö suoritetaan n n kertaa = O(n 2 ). (b) for (i = 0; i < n; i++) // silmukka n kertaa if (i%2 == 0) // vakio for (j = 0; j <= i; j++) // silmukka i+1 kertaa a = 2*a; // vakio for (j = i; j < n; j++) // silmukka n-i kertaa b = b/2; // vakio // (j-silmukat vain parillisilla i) Molemmat j-silmukat suoritetaan vain jos i on parillinen, ts. vain joka toisella i-silmukan kierroksella. Siten sisempien silmukoiden sisällöt suoritetaan n 2 (i + 1 + n i) = 1 2 n2 + 1 n (jos n parillinen) 2 tai n + 1 (i + 1 + n i) = 1 2 2 n2 + n + 1 2 kertaa = O(n 2 ). 3 (jos n pariton)
(c) for (i = 1; i < n; i++) // silmukka n-1 kertaa for (j = n-1; j >= i; j--) // silmukka n-i kertaa if (t[j] < t[j-1]) // vakio swap(t[j-1], t[j]); // tod.näk. vakio Sisemmän silmukan sisältö suoritetaan n 1 (n i) = (n 1)n i=1 kertaa = O(n 2 ). (n 1)n 2 = 1 2 n2 1 2 n (d) for (i = 1; i < n; i++) // silmukka n-1 kertaa k = 0; // vakio p = t[i]; // vakio for (j = i-1; j >= 0; j--) // silmukka i kertaa if (t[j] <= p) // vakio leap(j, k); // tod.näk. vakio t[j+1] = t[j]; // vakio t[k] = p; // vakio Sisemmän silmukan sisältö suoritetaan kertaa = O(n 2 ). n 1 i = i=1 (n 1)n 2 = 1 2 n2 1 2 n 2.1. (a) etsipienin(t, n) if (n == 1) return t[0]; return min(etsipienin(t, n-1), t[n-1]); Ei-rekursiivinen: Silmukka suoritetaan n 1 kertaa = O(n). Rekursiivinen: Suoritusaika on muotoa a, kun n = 1, T (n) = T (n 1) + b, kun n > 1, missä a, b > 0 vakioita = T (n) = T (n 1) + b = T (n 2) + 2b = T (n 3) + 3b = = T (1) + (n 1)b = a b + bn = O(n). 4
(b) asetaluku(t, n, luku) if (n == 1) t[0] = luku; t[n-1] = luku; asetaluku(t, n-1, luku); return; Ei-rekursiivinen: Silmukka suoritetaan n kertaa = O(n). Rekursiivinen: Suoritusaika samaa muotoa kuin (a)-kohdassa = O(n). (c) laskenollat(t, int n) if (n == 0) return 0; if (t[n-1] == 0) return (laskenollat(t, n-1) + 1); return (laskenollat(t, n-1)); Ei-rekursiivinen: Silmukka suoritetaan n kertaa = O(n). Rekursiivinen: Suoritusaika muuten samaa muotoa kuin (a)-kohdassa paitsi että rekursio päättyy vasta kun n = 0 = T (n) = T (n 1) + b = = T (0) + nb = a + bn = O(n). 2.2. (a) Aputilaa tarvitaan vain vakiomäärä (muuttujat a, b ja c). operate(yhdiste,pino) if (size(pino) < 2) return error; a = pop(pino); b = pop(pino); c = yhdiste(a,b); push(c,pino); 5
(b) Siirretään muut alkiot apupinoon, josta ne siirretään takaisin alkuperäiseen pinoon sitten kun päällimmäinen alkio on ensin sijoitettu pohjalle. Aputilaa tarvitaan apupinolle (ja muille muttujille), joten aputilan koko on samaa kertaluokkaa kuin alkuperäisen pinon koko. sink(pino) if (size(pino) < 1) return error; a = pop(pino); while (!isempty(pino)) b = pop(pino); push(b,apu); // siirretään apupinoon push(a,pino); while (!isempty(apu)) b = pop(apu); push(b,pino); // siirretään takaisin (c) Käytetään aputilana taulukkoa, josta alkiot on helppo poimia halutussa järjestyksessä. Aputilan koko on siis samaa kertaluokkaa kuin alkuperäisen pinon koko. flip(pino) n = size(pino); for (i = 1; i <= n; i++) a[i] = pop(pino); for (i = 1; i <= n; i++) push(a[i],pino); 2.3. (a) Lisätään merkki s pinoon p operaatiolla push(s,p) ja poistetaan merkki pinosta operaatiolla pop(p). Pinoon ei saa lisätä merkkiä sellaisen merkin päälle, joka on aakkosissa aikaisemmin, koska silloin alle jäänyttä merkkiä ei voi poistaa oikealla hetkellä. Tarvitaan vähintään neljä pinoa: push(d,1), push(a,1), pop(1), push(g,2), push(e,2), push(b,1), pop(1), push(f,3), push(i,4), push(c,1), pop(1), pop(1), pop(2), pop(3), pop(2), push(h,1), pop(1), pop(4) (b) Tarvitaan vähintään kaksi pinoa: push(i,1), push(d,1), push(c,1), push(b,1), push(h,2), push(g,2), push(a,1), pop(1), pop(1), pop(1), pop(1), push(f,1), push(e,1), pop(1), pop(1), pop(2), pop(2), pop(1) 6
(c) Lisätään merkki s jonoon q operaatiolla enq(s,q) ja poistetaan merkki jonosta operaatiolla deq(q). Jonoon ei saa lisätä merkkiä sellaisen merkin perään, joka on aakkosissa myöhemmin, koska silloin taakse jäänyttä merkkiä ei voi poistaa oikealla hetkellä. Tarvitaan vähintään kolme jonoa: enq(d,1), enq(a,2), deq(2), enq(g,1), enq(e,2), enq(b,3), deq(3), enq(f,2), enq(i,1), enq(c,3), deq(3), deq(1), deq(2), deq(2), deq(1), enq(h,2), deq(2), deq(1) (d) Tarvitaan vähintään viisi jonoa: enq(i,1), enq(d,2), enq(c,3), enq(b,4), enq(h,2), enq(g,3), enq(a,5), deq(5), deq(4), deq(3), deq(2), enq(f,4), enq(e,5), deq(5), deq(4), deq(3), deq(2), deq(1) 2.4. (a) Täytyy etsiä listan viimeinen tietue. Joudutaan käymään läpi koko lista, joten suoritusaika on O(n), missä n listan koko. if (first == null) // erikoistapaus: tyhjä lista last = null; p = first; while (p.next!= null) p = p.next; // nyt p osoittaa listan loppuun last = p; (b) Operaatio mielekäs vain jos listassa on vähintään kaksi tietuetta. Täytyy etsiä listan viimeinen ja toiseksi viimeinen tietue, joten suoritusaika on O(n). if (first == null) // tyhjä lista return; if (first.next == null) // vain yksi tietue return; q = null; p = first; while (p.next!= null) q = p; p = p.next; // nyt p osoittaa listan loppuun, q edeltävään tietueeseen if (q == first) // käsiteltävä erikseen... p.next = first; p.next = first.next; q.next = first; first.next = null; first = p; 7
2.5. Kaikissa tapauksissa tarvittaviin tietueisiin päästään käsiksi suoraan (ilman listan läpikäyntiä), joten suoritusajat ovat O(1). (a) if (first == null) // tyhjä lista return; if (first.next == null) // vain yksi tietue first = last = null; first = first.next; first.prev = null; (b) (c) // p osoitin lisättävään tietueeseen p.next = null; p.prev = last; if (first == null) // lisäys tyhjään listaan first = p; last.next = p; last = p; if (first!= null) // vain ei-tyhjälle listalle first.prev = last; last.next = first; // siinä kaikki! 3.1. (a) F / \ C G / \ \ A D I \ \ / \ B E H J korkeus 3 lehtisolmuja 4 (b) F / \ B I / \ / \ A D G J / \ \ C E H korkeus 3 lehtisolmuja 5 (c) F / \ E I / / \ C G J / \ \ B D H / A korkeus 4 lehtisolmuja 4 3.2. (a) Esijärjestys: 30, 20, 15, 16, 23, 37, 32, 31, 33, 40 Sisäjärjestys: 15, 16, 20, 23, 30, 31, 32, 33, 37, 40 Jälkijärjestys: 16, 15, 23, 20, 31, 33, 32, 40, 37, 30 8
(b) Haetaan solmua h, jolle h.key=32: 1. c -> null, p -> 30 2. p -> 37 c -> 37, p -> 32 c -> 32, p -> 31 p -> null 3. c.key == h.key joten löytyi (c == h) (c) Haetaan solmua h, jolle h.key=24: 1. c -> null, p -> 30 2. c -> 30, p -> 20 p -> 23 p -> null 3. c.key!= h.key joten ei löytynyt 3.3. (a) Poistetaan ensimmäinen solmu (h.key=15): 1. q -> null, p -> 30 30 2. q -> 30, p -> 20 / \ q -> 20, p -> 15 20 37 3. q.left=p.right / \ / \ 16 23 32 40 / \ 31 33 (b) Poistetaan solmu h, jolle h.key=32: 1. c -> null, q -> null, p -> 30 30 2. q -> 30, p -> 37 / \ cp -> 30, c -> 37, q -> 37, p -> 32 20 37 cp -> 37, c -> 32, q -> 32, p -> 31 / \ / \ 3. c.key == h.key joten jatketaan 15 23 31 40 4. p.right=c.right \ \ (tässä tapauksessa q == c) 16 33 cp.left=p (c) Poistetaan solmu h, jolle h.key=37: 1. c -> null, q -> null, p -> 30 30 2. q -> 30, p -> 37 / \ cp -> 30, c -> 37, q -> 37, p -> 32 20 33 q -> 32, p -> 33 / \ / \ 3. c.key == h.key joten jatketaan 15 23 32 40 4. p.right=c.right \ / (tässä tapauksessa q!= c) 16 31 q.right=p.left, p.left=c.left cp.right=p 9
3.4. Tasapainottaminen: Solmut sisäjärjestykseen; keskimmäisestä solmusta uusi juuri; pienemmistä solmuista vasen alipuu (rekursiivisesti); suuremmista solmuista oikea alipuu (rekursiivisesti). Jos solmujoukossa parillinen määrä solmuja, valitaan seuraavassa keskimmäiseksi solmuksi aina kahdesta vaihtoehdosta pienempi: 15 16 20 23 30 31 32 33 37 40 30 ^^ / \ 15 16 20 23 31 32 33 37 40 16 33 ^^ ^^ / \ / \ 15 20 23 31 32 37 40 15 20 31 37 ^^ ^^ ^^ ^^ \ \ \ 23 32 40 23 32 40 ^^ ^^ ^^ Alkuperäisen puun korkeus 3, tasot 0, 1 ja 2 täynnä. Tasapainotetun puun korkeus 3, tasot 0, 1 ja 2 täynnä. Ei oleellista muutosta. 3.5. Riittää tutkia vain puun rakenteellisessa järjestyksessä ensimmäinen ja viimeinen solmu. Etsitään nämä kaksi solmua, lasketaan molemmat etäisyydet ja valitaan niistä suurempi. p = juurisolmu; // tutkitaan vasen alipuu if (p.left == null) // vasen alipuu tyhjä vasenetaisyys = -ääretön; while (p.left!= null) p = p.left; vasenetaisyys = p.key - juurisolmu.key ; p = juurisolmu; // tutkitaan oikea alipuu if (p.right == null) // oikea alipuu tyhjä oikeaetaisyys = -ääretön; while (p.right!= null) p = p.right; oikeaetaisyys = p.key - juurisolmu.key ; suurinetaisyys = max(vasenetaisyys, oikeaetaisyys); Kaksi hakuoperaatiota, joiden askelten lukumäärä puun korkeus = aikavaativuus tasapainoiselle puulle O(log n), degeneroituneelle puulle O(n). 10