58131 Tietorakenteet (kevät 2009) Harjoitus 9, ratkaisuja (Antti Laaksonen) 1. Lisäysjärjestämisessä järjestetään ensin taulukon kaksi ensimmäistä lukua, sitten kolme ensimmäistä lukua, sitten neljä ensimmäistä lukua jne., kunnes lopuksi koko taulukko on järjestyksessä. Taulukon luvut käydään läpi alusta loppuun, ja joka vaiheessa käsiteltävää lukua siirretään askel kerrallaan vasemmalle, kunnes se on oikeassa paikassa ja taulukon alkuosa on järjestetty. Seuraavassa esimerkissä taulukon järjestetty osuus on erotettu pystyviivalla ja tummennettu luku tarkoittaa oikeaan paikkaan siirrettyä käsiteltävää lukua. 6 2 3 8 1 4 10 5 7 9 2 6 3 8 1 4 10 5 7 9 2 3 6 8 1 4 10 5 7 9 2 3 6 8 1 4 10 5 7 9 1 2 3 6 8 4 10 5 7 9 1 2 3 4 6 8 10 5 7 9 1 2 3 4 6 8 10 5 7 9 1 2 3 4 5 6 8 10 7 9 1 2 3 4 5 6 7 8 10 9 (a) Kun taulukon luvut ovat nousevassa suuruusjärjestyksessä, jokainen luku on valmiiksi oikeassa paikassa eikä sitä tarvitse siirtää yhtään askelta vasemmalle. Tämän ansiosta aikaa kuluu vain Θ(n), koska riittää käydä taulukon luvut läpi ja siirtyä joka vaiheessa suoraan seuraavaan lukuun. (b) Kun taulukon luvut ovat laskevassa suuruusjärjestyksessä, toista lukua täytyy siirtää yksi askel vasemmalle, kolmatta kaksi askelta vasemmalle, neljättä kolme askelta vasemmalle jne., kunnes viimeistä lukua täytyy siirtää n 1 askelta vasemmalle. Siirtojen kokonaismäärä saadaan laskemalla summa 1 + 2 + 3 +... + (n 1), joka on tunnetusti (n 1) n 2. Asiaa voi myös ajatella niin, että lukua siirretään keskimäärin noin n/2 askelta ja lukuja on n, joten siirtoja on yhteensä noin n/2 n. Molemmat tavat johtavat samaan tulokseen: järjestäminen vie aikaa Θ(n 2 ). (c) Kun kaikki taulukon luvut ovat samat, taulukko on jälleen valmiiksi järjestyksessä eikä mitään lukua tarvitse siirtää vasemmalle. Aikavaativuus on (a)-kohdan tavoin Θ(n). 2. Kekojärjestämisessä on kaksi vaihetta: ensin taulukko muutetaan keoksi, sitten luvut poistetaan siitä järjestyksessä. Taulukko on mahdollista muuttaa keoksi ajassa Θ(n), joten aikavaativuuden ratkaisee, kuinka nopeasti luvut saadaan ulos keosta. Tämä taas riippuu siitä, kuinka suuri työ joudutaan tekemään keskimäärin kekoehdon korjaamiseksi luvun poiston jälkeen. Kun keon huipulta poistetaan luku, sen tilalle siirretään luku keon pohjalta, joka kenties rikkoo kekoehdon keon huipulla. Kekojärjestämisen pahimman tapauksen aikavaativuus on O(n log n), mutta joissain erikoistilanteissa aikavaativuus saattaa olla pienempi. Seuraavaksi kuitenkin osoitetaan (hieman mutkia oikoen), että jos taulukossa ei ole samoja lukuja, aikavaativuus on aina Θ(n log n). Tämä tarkoittaa, että kekoehdon korjaaminen vie keskimäärin logaritmisen ajan, olivatpa luvut keossa missä tahansa järjestyksessä. Seurataan luennoilla esitettyä kekojärjestämisen toteutusta, jossa luvut on tallennettu maksimikekoon. Keossa olevalla luvulla on kaksi tapaa päästä keon huipulle, josta se voidaan poistaa:
Luku siirtyy askel kerrallaan ylöspäin keossa muiden lukujen poiston aiheuttamien kekoehdon korjausten vuoksi. Tähän kuluu keskimäärin logaritminen aika. Keon pohjalla oleva pian poistettava luku nostetaan suoraan keon huipulle toisen luvun poiston yhteydessä. Tällöin luvun ei tarvitse kulkea koko kekoa ylös askel kerrallaan. Tarkoituksena on todistaa, että merkittävä osa luvuista joutuu nousemaan keon huipulle askel kerrallaan eikä jälkimmäinen ilmiö ole liian yleinen. Kun kekoon sijoitetaan n lukua, noin n/2 lukua on keon sisäsolmuissa ja noin n/2 lukua on keon lehtisolmuissa. Tarkastellaan nyt lukuja, jotka samaan aikaan ovat keon sisäsolmuissa ja kuuluvat taulukon n/2 suurimman luvun joukkoon. Näiden lukujen ainoa tapa päästä keon huipulle on kiivetä sinne yksi askel kerrallaan, koska luvut pitää poistaa keosta n/2 ensimmäisen luvun joukossa, mutta n/2 ensimmäisen poiston aikana vain alkuaan lehtisolmuissa olevia lukuja nostetaan oikotietä ylös. Jos melkein kaikki taulukon n/2 suurimmasta luvusta voisivat olla keon lehtisolmuissa, ne saattaisivat päästä liian nopeasti keon huipulle. Kuitenkin ainakin noin kolmannes taulukon n/2 suurimmasta luvusta on väistämättä keon sisäsolmuissa. Tämä johtuu siitä, että yhtä sisäsolmussa olevaa suurta lukua kohden voi olla korkeintaan kaksi lehtisolmussa olevaa suurta lukua, koska kekoehdon vuoksi lehtisolmussa olevien lukujen täytyy olla vanhempaansa pienempiä. Seuraavassa esimerkissä taulukossa on luvut 1, 2, 3,..., 12, joten keosta täytyy poistaa ensin taulukon suurimmat luvut järjestyksessä 12, 11, 10, 9, 8 ja 7. Näistä luvuista viisi eli yli kaksi kolmannesta on yritetty sijoittaa keon lehtisolmuihin, mutta nyt kelvolliset vanhemmat loppuvat kesken. (Tällainen keko on muutenkin mahdoton, koska suurimman luvun 12 pitäisi olla keon huipulla.) Siis taulukon luvuista ainakin noin n/6 on samaan aikaan keon sisäsolmuissa ja kuuluu taulukon n/2 suurimman luvun joukkoon. Kaikkien näiden lukujen täytyy nousta keon huipulle yksi askel kerrallaan, jotta ne voitaisiin poistaa keosta ajallaan. Lukujen määrä on n/6 eli luokkaa Θ(n) ja niiden keskimääräinen matka keon huipulle on ainakin noin log 2 (n/6) eli luokkaa Θ(log n). Pelkästään näiden lukujen poistaminen keosta vie siis aikaa Θ(n log n), joten vaikka muut luvut onnistuttaisiin poistamaan kuinka nopeasti tahansa, aikavaativuus ei voisi muuttua enää paremmaksi. Tässä päättelyssä tärkeä oletus oli, että keossa ei ole samoja lukuja. Oletuksen merkitys oli siinä, että on tietyt n/2 lukua, joiden on pakko nousta keon huipulle ensimmäisinä. (a) Kun taulukon luvut ovat nousevassa suuruusjärjestyksessä, aikaa kuluu Θ(n log n). Äsken nimittäin osoitettiin, että jos taulukossa ei ole samoja lukuja, aikaa kuluu joka tapauksessa Θ(n log n). Erityisesti tämä pätee, kun luvut sattuvat olemaan lisäksi nousevassa suuruusjärjestyksessä. (b) Kun taulukon luvut ovat laskevassa suuruusjärjestyksessä, aikaa kuluu Θ(n log n). Äsken nimittäin osoitettiin, että jos taulukossa ei ole samoja lukuja, aikaa kuluu joka tapauksessa Θ(n log n). Erityisesti tämä pätee, kun luvut sattuvat olemaan lisäksi laskevassa suuruusjärjestyksessä. (c) Kun taulukon kaikki luvut ovat samat, aikaa kuluu vain Θ(n). Tämä johtuu siitä, että kekoehtoa ei tarvitse korjata missään vaiheessa, vaan riittää, että luvut nostetaan yksi kerrallaan keon pohjalta huipulle ja siirretään suoraan ulos keosta. 2
3. Lomitusjärjestämisessä taulukon alkuosa ja loppuosa järjestetään ensin erikseen kutsumalla algoritmia rekursiivisesti. Jakokohta valitaan niin, että molemmissa osissa on suunnilleen yhtä monta lukua. Tämän jälkeen järjestetyistä osataulukoista muodostetaan lopullinen järjestetty taulukko, mikä on mahdollista tehokkaasti, koska osataulukot ovat valmiiksi järjestyksessä. Järjestysfunktion kutsut jakaantuvat tasoihin: ensimmäisellä tasolla n lukua sisältävä taulukko jaetaan kahteen osaan, toisella tasolla kaksi noin n/2 lukua sisältävää taulukkoa jaetaan kahteen osaan, kolmannella tasolla neljä noin n/4 lukua sisältävää taulukkoa jaetaan kahteen osaan jne. Haarautuminen päättyy, kun kussakin osataulukossa on enää yksi alkio, mikä tapahtuu noin tasolla log 2 n. Tämän jälkeen järjestetyt osataulukot täytyy vielä yhdistää taso kerrallaan, mikä vie aikaa kullakin tasolla Θ(n). Tasoja on Θ(log n) ja yhdellä tasolla kuluu aikaa Θ(n), joten kokonaisaikavaativuus on Θ(n log n). Tähän tulokseen ei vaikuta, mitä lukuja ja missä järjestyksessä taulukossa on, joten: (a) Kun taulukon luvut ovat nousevassa suuruusjärjestyksessä, aikaa kuluu Θ(n log n). (b) Kun taulukon luvut ovat laskevassa suuruusjärjestyksessä, aikaa kuluu Θ(n log n). (c) Kun kaikki taulukon luvut ovat samat, aikaa kuluu Θ(n log n). 4. Pikajärjestämisessä yksi taulukon luvuista valitaan jakoalkioksi ja taulukon lukuja siirretään niin, että taulukon alkuosassa kaikki luvut ovat korkeintaan jakoalkion suuruisia ja taulukon loppuosassa kaikki luvut ovat ainakin jakoalkion suuruisia. Tämän jälkeen taulukon alkuosa ja loppuosa järjestetään rekursiivisesti samalla menetelmällä. Jakoalkion valintaan on useita eri tapoja: luentojen mallin mukaisesti valitaan jakoalkioksi taulukon ensimmäinen luku. (a) Kun taulukon luvut ovat nousevassa suuruusjärjestyksessä, taulukko jaetaan joka vaiheessa kahteen osaan, joista toisessa on yksi luku ja toisessa kaikki muut. Tarkastellaan tilannetta, jossa taulukossa ovat luvut 1, 2, 3,..., 10. Ensimmäisessä jaossa jakoalkio on luku 1, jolloin vasemmassa osassa on luku 1 ja oikeassa osassa ovat luvut 2, 3,..., 10. Vasemman osan järjestäminen on helppoa, sillä siinä on vain yksi luku. Sen sijaan oikean osan järjestämiseksi luvut täytyy jälleen jakaa kahteen osaan. Nyt jakoalkio on luku 2, joka jää taas yksin vasempaan osaan. Pikajärjestäminen etenee vastaavasti luku kerrallaan kohti taulukon loppua. Lopuksi sekä vasemmassa että oikeassa osassa on vain yksi luku, jolloin molemmat osat voidaan järjestää suoraan eikä pikajärjestämisen tarvitse enää haarautua. Yleisemmin kun taulukossa on n lukua, järjestysfunktiota kutsutaan n kertaa niin, että osataulukossa on yksi luku, ja lisäksi kerran kaikilla muilla lukujen määrillä n, n 1, n 2,..., 2. Yksi järjestysfunktion kutsu vie lineaarisen ajan suhteessa lukujen määrään. Tämän vuoksi yhden luvun sisältävien osataulukoiden järjestäminen vie ajan Θ(n) ja muiden osataulukoiden järjestäminen vie ajan Θ(n 2 ), joten kokonaisaikavaativuus on Θ(n 2 ). (b) Kun taulukon luvut ovat laskevassa suuruusjärjestyksessä, pikajärjestäminen etenee samoin kuin edellisessä kohdassa, mutta nyt jakokohta on vuorotellen taulukon lopussa ja taulukon alussa. Tarkastellaan tilannetta, jossa taulukossa ovat luvut 10, 9, 8,..., 1. Ensimmäisessä jaossa jakoalkio on luku 10, joka siirtyy taulukon loppuun ja muodostaa yksin oikean osan. 1 9 8 7 6 5 4 3 2 10 3
Seuraava jakoalkio on luku 1, joka on osataulukon pienin luku. Tämän jälkeen tilanne vastaa taulukon järjestämisen aloitusta, sillä oikeaan osaan jäävät luvut 8, 7, 6,..., 2. Pikajärjestäminen siirtää siis toistuvasti samanaikaisesti taulukon pienimmän ja suurimman luvun oikeille paikoilleen. Viimeinen jakoalkio on tässä tapauksessa luku 5. 1 9 8 7 6 5 4 3 2 10 1 2 8 7 6 5 4 3 9 10 1 2 8 7 6 5 4 3 9 10 Yleisessä tapauksessa järjestysfunktiota kutsutaan samankokoisilla osataulukoilla kuin lukujen ollessa nousevassa suuruusjärjestyksessä, joten kokonaisaikavaativuus on jälleen Θ(n 2 ). (c) Kun kaikki taulukon luvut ovat samat, pikajärjestäminen toimii ihanteellisesti: osataulukko jakautuu joka vaiheessa kahteen suunnilleen yhtä suureen osaan. Tarkastellaan tilannetta, jossa taulukossa on kymmenen kertaa luku 5. Taulukon luvut käydään läpi pareittain vasemmalta oikealle ja oikealta vasemmalle ja joka vaiheessa kaksi lukua vaihdetaan keskenään. Läpikäynti päättyy taulukon keskikohdassa, ja vasen ja oikea osa ovat yhtä suuret. Pikajärjestäminen jatkuu vastaavasti, kunnes osataulukoissa on vain yksi luku. Yleisessä tapauksessa järjestysfunktion kutsut jakaantuvat tasoihin: ensimmäisellä tasolla funktiota kutsutaan kerran ja taulukossa on n alkiota, toisella tasolla funktiota kutsutaan kahdesti ja kussakin osataulukossa on noin n/2 alkiota, kolmannella tasolla funktiota kutsutaan neljästi ja kussakin osataulukossa on noin n/4 alkiota jne. Kun osataulukot ovat puolittuneet noin log 2 n kertaa, niissä on enää yksi luku ja haarautuminen päättyy. Joka tasolla aikaa kuluu Θ(n) ja tasoja on yhteensä Θ(log n), joten kokonaisaikavaativuus on Θ(n log n). 5. Jos funktio PARTITION palauttaisi arvon r, funktio QUICKSORT kutsuisi itseään uudestaan antaen samat parametrit, joilla funktiota on kutsuttu. Jos funktio PARTITION ei sisältäisi satunnaisuutta eli palauttaisi samoilla parametreilla saman arvon, pikajärjestäminen ei päättyisi koskaan, koska funktio QUICKSORT kutsuisi itseään koko ajan samoilla parametreilla. Jos funktio PARTITION sisältäisi satunnaisuutta ja palauttaisi arvon r vain silloin tällöin, pikajärjestäminen hidastuisi, koska osa funktion QUICKSORT kutsuista ei edistäisi järjestämistä yhtään. Funktion PARTITION palautusarvo on muuttujan j sisältö funktion päättymishetkellä. Ennen silmukan aloitusta j:n arvo on r+1. Jokaisella silmukan kierroksella j:n arvo vähenee ainakin yhdellä. Uhkatilanne syntyy siitä, että silmukka suoritettaisiin vain kerran ja j:n arvo vähenisi silloin vain yhdellä. Tällöin heti silmukan ensimmäisen kierroksen jälkeen pitäisi päteä i j, jotta silmukka päättyisi. Koska j:n pitäisi pysähtyä taulukon loppuun, jotta sen arvoksi jäisi r, vastaavasti i:n pitäisi nousta taulukon loppuun, jotta ehto i j olisi voimassa. Tällöin ensinnäkin pitäisi olla A[p] < a, jotta i pääsisi edes taulukon kohtaan p + 1. Kuitenkin on valittu a = A[p], joten i ei voi edetä silmukan ensimmäisellä kierroksella kohtaa p pidemmälle. Koska p < r, on mahdotonta, että samaan aikaan pätisivät i j ja j = r. 4
6. Tutkitaan aluksi esimerkkiä, jossa n on 8 ja taulukot ovat seuraavat: Yhdistämällä taulukot saadaan seuraava taulukko: A 1 3 3 6 8 11 15 19 B 2 5 6 9 9 12 15 16 1 2 3 3 5 6 6 8 9 9 11 12 15 15 16 19 Taulukon keskimmäiset luvut ovat 8 ja 9, joten taulukon mediaani on 8,5. Aletaan sitten kehittää algoritmia mediaanin laskemiseen, kun syötteenä annetaan mitkä tahansa kaksi taulukkoa, joissa molemmissa on n lukua suuruusjärjestyksessä. Jos n on 1, molemmissa taulukoissa on yksi luku ja yhdistetyn taulukon mediaani on lukujen keskiarvo. Jos n on 2, molemmissa taulukoissa on kaksi lukua ja yhdistetyn taulukon mediaani on kahden keskimmäisen luvun keskiarvo. Kaikilla n:n arvoilla lukuja on yhteensä parillinen määrä, joten mediaani on tässä aina kahden luvun keskiarvo. Algoritmin aikavaativuuden täytyy olla O(log n), mikä viittaa siihen, että algoritmin täytyy hylätä jokaisessa askeleessa merkittävä osa jäljellä olevista mahdollisista mediaanin osista. Tuttu esimerkki tällaisesta algoritmista on binäärihaku, joka vähentää toistuvasti käsiteltävien lukujen määrän puoleen. Osoittautuu, että binäärihaun idea soveltuu myös tähän tehtävään. Tutkitaan yleistä tapausta, jossa n on parillinen. Merkitään a = A[n/2] ja b = B[n/2], eli a ja b ovat A:n ja B:n keskimmäiset luvut. Oletetaan, että a on pienempi kuin b; jos a on suurempi kuin b, päättelysuunta vain vaihtuu. Nyt mikään luku, joka on taulukossa A ennen lukua a, ei voi olla yhdistetyn taulukon mediaanin osa. Lisäksi mikään luku, joka on taulukossa B luvun b oikealla puolella olevan luvun jälkeen, ei voi olla yhdistetyn taulukon mediaanin osa. Esimerkiksi jos n on 8, seuraavassa X:llä merkityt luvut eivät voi olla mediaanin osia. A X X X a???? B??? b? X X X Jos jokin taulukossa A ennen lukua a oleva luku olisi mediaanin osa, taulukoissa täytyisi olla ainakin n 1 sitä pienempää lukua. Kuitenkin ainoat mahdolliset pienemmät luvut ovat muut luvut, jotka ovat taulukossa A ennen lukua a, ja luvut, jotka ovat taulukossa B ennen lukua b. Näitä lukuja on taulukossa A korkeintaan n/2 2 ja taulukossa B korkeintaan n/2 1, siis yhteensä vain n 3. Tämän vuoksi mikään taulukossa A ennen lukua a oleva luku ei voi olla mediaanin osa. Jos jokin taulukossa B luvun b oikealla puolella olevan luvun jälkeen oleva luku olisi mediaanin osa, taulukoissa täytyisi olla ainakin n 1 sitä suurempaa lukua. Kuitenkin ainoat mahdolliset suuremmat luvut ovat muut luvut, jotka ovat taulukossa B luvun b oikealla puolella olevan luvun jälkeen, ja luvut, jotka ovat taulukossa A luvun a jälkeen. Näitä lukuja on taulukossa B korkeintaan n/2 2 ja taulukossa A korkeintaan n/2, siis yhteensä vain n 2. Tämän vuoksi mikään taulukossa B luvun b oikealla puolella olevan luvun jälkeen oleva luku ei voi olla mediaanin osa. Tutkitaan sitten yleistä tapausta, jossa n on pariton. Merkitään a = A[(n+1)/2] ja b = B[(n+1)/2], eli a ja b ovat A:n ja B:n keskimmäiset luvut. Oletetaan samoin kuin edellä, että a on pienempi kuin b. Nyt mikään luku, joka on taulukossa A ennen lukua a, ei voi olla yhdistetyn taulukon mediaanin osa. Lisäksi mikään luku, joka on taulukossa B luvun b jälkeen, ei voi olla yhdistetyn taulukon mediaanin osa. Esimerkiksi jos n on 7, seuraavassa X:llä merkityt luvut eivät voi olla mediaanin osia. A X X X a??? B??? b X X X 5
Päätellään kuten edellä: Jos jokin taulukossa A ennen lukua a oleva luku olisi mediaanin osa, taulukoissa täytyisi olla ainakin n 1 sitä pienempää lukua, mutta taulukoissa voi olla vain n 2 sitä pienempää lukua. Jos jokin taulukossa B luvun b jälkeen oleva luku olisi mediaanin osa, taulukoissa täytyisi olla ainakin n 1 sitä suurempaa lukua, mutta taulukoissa voi olla vain n 2 sitä suurempaa lukua. Mistä tahansa taulukoista, joissa on molemmissa n lukua, voidaan siis poistaa noin n/2 lukua toisen alusta ja noin n/2 lukua toisen lopusta kadottamatta yhdistetyn taulukon mediaanin osia. Jäljelle jääneissä taulukoissa on edelleen yhtä monta lukua, joten niistä voi etsiä mediaania samalla menetelmällä. Kun taulukoissa on enää muutama alkio, mediaanin saa selville suoraan. Yhdistetään kaikki aiemmat havainnot seuraavaan algoritmiin: MEDIAN(A, l A, r A, B, l B, r B ) 1 if r A l A 1 2 then return (MAX(A[l A ], B[l B ]) + MIN(A[r A ], B[r B ]))/2 3 c (r A l A )/2 4 if A[l A + c] < B[l B + c] 5 then return MEDIAN(A, l A + c, r A, B, l B, r B c) 6 else return MEDIAN(A, l A, r A c, B, l B + c, r B ) Funktion parametreissa l A tarkoittaa, mistä indeksistä taulukon A käsiteltävä osa alkaa, ja r A tarkoittaa, mihin indeksiin taulukon A käsiteltävä osa päättyy. Vastaavasti l B tarkoittaa, mistä indeksistä taulukon B käsiteltävä osa alkaa, ja r B tarkoittaa, mihin indeksiin taulukon B käsiteltävä osa päättyy. Joka tilanteessa r A l A = r B l B, koska taulukot ovat yhtä suuria. Riveillä 1 2 käsitellään tapaukset, joissa taulukoissa on 1 tai 2 lukua. Laskutavan toimivuudesta voi vakuuttua käymällä kaikki mahdolliset järjestykset läpi. Riveillä 3 6 käsitellään tapaukset, joissa lukujen määrä on suurempi: jos A:n keskimmäinen alkio on pienempi kuin B:n keskimmäinen alkio, hylätään A:n alkuosa ja B:n loppuosa, ja muussa tapauksessa hylätään A:n loppuosa ja B:n alkuosa. Katsotaan vielä lopuksi, kuinka algoritmi laskee ensimmäisen esimerkin taulukkojen mediaanin. Molemmissa taulukoissa on 8 alkiota, joten funktiota kutsutaan MEDIAN(A, 1, 8, B, 1, 8). A 1 3 3 6 8 11 15 19 B 2 5 6 9 9 12 15 16 A 6 8 11 15 19 B 2 5 6 9 9 A 6 8 11 B 6 9 9 A 8 11 B 6 9 Vihdoin (MAX(8, 6) + MIN(11, 9))/2 = (8 + 9)/2 = 8, 5. 6