Tietorakenteet ja algoritmit I Turun yliopisto, Informaatioteknologian laitos, periodi 2 / 2011 Lasse Bergroth Kurssin sisältö • Kurssi perustuu suoraan oppikirjaan • Cormen, T. H. – Leiserson, C. E – Rives, R. L – Stein, C.: ”Introduction to Algorithms”, 3. painos, MIT press (2009) Myös vanhemmat painokset (erityisesti 2. painos) kelpaavat • Kyseinen, lähes 1300-sivuinen kirja löytyy myös verkosta brasilialaisen Recifessä sijaitsevan Pernambucon yliopiston sivuilta osoitteesta http://www.cin.ufpe.br/~ass4/ALGORITMOS/ALG_3rd.pdf. • Samaisesta oppikirjasta luennoidaan myös kurssi: • Tietorakenteet ja algoritmit II (aineopintotasoinen, keväällä 2012 periodilla 3) – jatkoa tälle kurssille Kirjaa on aikaisemmin käytetty oppikirjana myös kurssilla: Algoritmien suunnittelu ja analysointi (syventävä kurssi, syksyllä 2012 periodeilla 1 – 2) – luennoitsijana professori Olli Nevalainen Monet kurssilla käsiteltävistä asioista löytyvät edelleen tästä kirjasta. Kurssin sisältö (jatkoa) Sisällysluettelo • Luentokalvoissa on käytetty samaa osa- ja lukunumerointia kuin oppikirjassa I Perusteet 1. 2. 3. 4. Algoritmien asema tietojenkäsittelyssä - mitä algoritmiikka tutkii? Algoritmiikan perusteet - perusmenetelmät algoritmien analyysiä varten Funktioiden kasvunopeus - algoritmien suorituskyvyn mittaaminen ”Hajota ja hallitse” –menettely - tehtävän ratkaiseminen osittamalla se alkuperäisen kaltaisiin mutta pienempiin osaongelmiin II Lajittelualgoritmit 6. Kekolajittelu - lajittelun suorittaminen käyttämällä aputietorakenteena kekoa 7. Pikalajittelu - käytännössä tehokkaaksi osoittautunut rekursiivinen yleinen lajittelumenetelmä 8. Lajittelu lineaarisessa ajassa - lajittelun nopeuttaminen käyttämällä hyväksi ennakkotietoja syötteen ominaisuuksista 9. Mediaanit ja järjestysstatistiikat - lajittelua helpomman tehtävän – syötteen i. suurimman alkion etsiminen Kurssin sisältö (jatkoa) III Tietorakenteet 10. 11. 12. Perustietorakenteet - jono, pino ja lista sekä vaihtoehtoja niiden toteuttamiseksi Hajautustaulut - esitellään tekniikoita hajautustaulun toteuttamiseksi ja -funktion valitsemiseksi Binääriset hakupuut - toteutus, eri selausjärjestykset - luennoidaan vasta kurssilla TRAK II, mutta kalvot esitetään TRAKLA-demonstraatioita varten. I Perusteet 1. Algoritmien asema tietojenkäsittelyssä • • Algoritmilla tarkoitetaan hyvin määriteltyä toimintosarjaa, joka muuntaa syötteen (lähtötiedot) tulosteeksi. Algoritmi ratkaisee jonkin reaalimaailman ongelman, ja ratkaisijana – eli algoritmin suorittajana – voi toimia joko ihminen tai (tieto)kone. Ongelma Algoritmi Syötteet Suorittaja Tuloste 1. Algoritmien asema tietojenkäsittelyssä • Algoritmeilta vaaditaan (yleensä) seuraavien kriteerien täyttämistä: 1) Yleisyyskriteeri: algoritmia on pystyttävä soveltamaan kaikille niille syötteille, jotka täyttävät sille asetettavan alkuehdon 2) Deterministisyyskriteeri: kaikissa laskennan vaiheissa pitää olla yksikäsitteisesti tiedossa, miten suoritusta jatketaan tästä kriteeristä joustetaan sovellettaessa ns. vapaajärjesteisiä eli ei-imperatiivisia algoritmeja (funktionaalinen ja logiikkaohjelmointi) 3) Tuloksellisuuskriteeri: algoritmin on aina palautettava tulos, joka a) on oikeellinen ja b) saavutetaan äärellisen ajan kuluessa tästä kriteeristä voidaan joustaa käytettäessä esimerkiksi heuristisia, likimääräis- tai todennäköisyysalgoritmeja, sillä toisinaan ei-optimaalinen, epätarkka tai jopa tietyllä riskillä virheellinenkin lopputulos saattaa kaikesta huolimatta olla tyhjää parempi 1. Algoritmien asema tietojenkäsittelyssä • • • • Algoritmiikka tutkii, miten tietokone saadaan yksinkertaisia käskyjä peräkkäin suorittamalla ratkaisemaan tehokkaasti reaalimaailman ongelmia. Tietämys algoritmiikasta helpottaa hyödyntämään aikaisemmin löydettyjä ratkaisuja jonkin tehtävän ratkaisemiseksi. samankaltaiset alitehtävät, vaikkapa lajittelu, esiintyvät lukuisan eri ongelman ratkaisun vaiheina kaikkea ei siis välttämättä tarvitse ratkaista aina itse alkutekijöistä lähtien! Vaikka valmiina saatavia algoritmeja on saatavilla paljon ja jopa valmiiksi ohjelmoituinakin, ei tämä kuitenkaan tee algoritmiikan osaamista vähemmän merkittäväksi tai suorastaan tarpeettomaksi, sillä sopivan algoritmin valitseminen useasta ehdokkaasta pelkästään ”arpomalla” voi johtaa huonolla tuurilla varsin tehottomaan lopputulokseen ohjelmoijan pitää pystyä ymmärtämään, miksi jokin tietty algoritmi sopii hänen tarkoituksiinsa paremmin kuin jokin toinen saman tehtävän ratkaiseva Lisäksi kannattaa huomioida, että algoritmit ovat ohjelmointikielistä ja pienin varauksin myös tietokoneista riippumattomia vaikka tietokoneet tehostuvat ja ohjelmointiympäristöt kehittyvät tämän tästä, algoritmit eivät kuitenkaan jää tämän kehityksen jalkoihin, vaan niiden käyttökelpoisuutta kuvaavat ominaisuudet säilyvät muutosten yli algoritmiikan opiskelulla saavutetut tiedot eivät siis käy vanhanaikaisiksi! 1. Algoritmien asema tietojenkäsittelyssä • • • Suomennettu sitaatti D. Harelin kirjasta The spirit of Computing (1992): ”Algoritmiikka on enemmän kuin vain yksi tietojenkäsittelytieteen osa-alue. Se on tietojenkäsittelytieteen ydin, ja sen voidaan rehellisesti sanoa olevan relevantti useimpien tieteiden, liiketoiminnan ja teknologian kanssa.” Tämän kurssin tärkeimpinä tavoitteina ovat siten: Esitellä vuosien mittaan hyviksi osoittautuneita ideoita ja menetelmiä tiettyjen, käytännössä usein esiintyvien tehtävien ratkaisemisessa. Antaa opiskelijalle riittävät perustiedot, mitä tarvitaan algoritmien tehokkuuden analysoimiseksi, jotta hän pystyisi punnitsemaan tarkastelemansa menetelmän hyvyyttä ja soveltuvuutta jonkin tietyn (ali)ongelman ratkaisemiseksi. Tutustutaan seuraavaksi kahden vaihtoehtoisen menetelmän ominaisuuksiin arvon etsimiseksi järjestetystä vektorista: Syöte: yhteensä n alkiota (n > 0) sisältävä järjestetty vektori A, joka on indeksoitu välille [1..n] sekä etsittävä arvo x Tuloste: alkion x sijaintipaikka (indeksi) vektorissa A. Ellei x:ää esiinny kyseisessä vektorissa, palautetaan arvo 0 merkkinä epäonnistuneesta hausta. Oletus: jos x esiintyy vektorissa A useita kertoja peräkkäin (x:llä on duplikaatteja), voidaan palauttaa mikä tahansa x:n esiintymiskohdista) 1. Algoritmien asema tietojenkäsittelyssä • Esimerkkisyöte: 11-paikkainen kokonaislukuvektori A, josta etsitään alkiota 16 1 2 3 4 5 6 7 8 9 10 11 A= 1 4 5 8 11 15 16 22 25 31 34 Ratkaistaan tehtävä ensiksi käyttämällä ns. lineaarihakua: Lineaarihaku(A, n, x): 1. Selaa vektorin A alkioita vuoron perään aloittamalla paikasta 1. Merkitään kulloistakin tarkastelukohtaa muuttujalla i. 2. Jos A[i] = x jollain i:n arvolla, palauta kutsujalle arvo i ja poistu. 3. Jos yhtään alkiota ei ole enää tutkimatta, eli kaikilla i:n arvoilla 1, 2, …, n A[i] oli erisuuri kuin x, palauta arvo 0 ja poistu. Esimerkissämme vektoriin joudutaan vektoriin kohdistamaan 7 hakua, kunnes etsitty alkio 16 löytyy. alkiota haetaan järjestyksessä paikoista 1, 2, 3, 4, 5, 6 ja 7. Analyysi: Parhaassa tapauksessa joudutaan tutkimaan ainoastaan yksi alkio, A[1], jos sieltä löytyy heti etsitty x. Pahimmassa tapauksessa x esiintyy A:ssa ensi kerran vasta indeksissä n, tai sitten x:ää ei löydy lainkaan vektorista A. kaikki alkiot eli n kappaletta joudutaan tutkimaan. 1. Algoritmien asema tietojenkäsittelyssä A= 1 2 3 4 5 6 7 8 9 10 11 1 4 5 8 11 15 16 22 25 31 34 … ja sitten käyttämällä puolitushakua: Puolitushaku(A, n, x): 1. alaraja := 1; yläraja := n; 2. keskikohta := A[(alaraja + yläraja)/2]; 3. Tutki paikassa A[keskikohta] oleva alkio. 4. Jos x = A[keskikohta], lopeta haku ja palauta indeksi keskikohta 5. Jos x < A[keskikohta] yläraja := keskikohta – 1 (* x ei keskikohdasta oikealle *) muutoin alaraja := keskikohta + 1 (* x ei keskikohdasta vasemmalle *) 6. Jos alaraja ≤ yläraja palaa takaisin riville 2 /* Vielä mahdollisia paikkoja x:lle */ muutoin lopeta haku ja palauta indeksi 0 /* Haku epäonnistui. */ Esimerkissämme alkiota 16 etsittäisiin järjestyksessä paikoista 6, 9, 8 ja 7 tarvitaan ainoastaan 4 hakua Analyysi: Parhaassa tapauksessa joudutaan nytkin tutkimaan ainoastaan yksi alkio, A[keskikohta], jos se on etsityn x:n sijaintipaikka. Pahimmassa tapauksessa x löytyy A:sta ensi kerran vasta silloin, kun hakualue on puristunut yhden pituiseksi (alaraja = yläraja), tai sitten x:ää ei löydy lainkaan vektorista A. alkioita x joudutaan etsimään tarkalleen yksi kerta enemmän kuin rivin 6 ehto toteutuu. Tämä on mahdollista enintään log2n kappaletta. Esimerkkitapauksemme oli siten pahin tapaus (!) tarkastellulle syöteaineistolle. MUTTA: puolitushausta ei ole iloa, jos syöte ei ole lajiteltu! 1. Algoritmien asema tietojenkäsittelyssä • • • • • • Algoritmin tehokkuutta mitataan useimmiten sillä, miten paljon se vaatii aikaa ja/tai muistitilaa eli resursseja suoritusta varten. Jotta algoritmin analyysi voidaan pitää yksinkertaisena, sovitaan, että jokainen suoritettava perusoperaatio vie aikaa yhden aikayksikön. Algoritmin resurssivaativuus eli kompleksisuus ilmoitetaan yleensä ainoastaan kuvaamalla sen suuruus- eli kertaluokka ilman absoluuttisia mittauksia, jotka tietystikin vaihtelevat koneittain. Tähän tarkoitukseen käytetään joko Θ- tai Ο-notaatiota (theta / (iso) ordo). Lineaarihaun aikavaativuus on kertaluokkaa Θ(n). Puolitushaulla se on sen sijaan kertaluokkaa Θ(log2n). Seuraava taulukko esittää funktioiden y = log2x ja lg x arvon riippuvuutta x:stä (Huom! Tällä kurssilla log x = log10x ja lg x = log2x): n 10 100 1 000 10 000 100 000 1 000 000 10 000 000 log n 1 2 3 4 5 6 7 lg n ≈ 3.3 ≈ 6.6 ≈ 10.0 ≈ 13.3 ≈ 16.6 ≈ 19.9 ≈ 23.3 • Taulukosta voi selvästi havaita, että logaritmit reagoivat hyvin hitaasti syötteen koon n kasvuun. mitä pidempi järjestetty vektori, sitä halvemmaksi puolitushaku pitkän päälle tulee! • Tosin myös järjestetyn vektorin lineaarihakua voidaan tehostaa, mutta pahin tapaus silti Ο(n). 1. Algoritmien asema tietojenkäsittelyssä • • Edellä tutustuttiin kahteen vaihtoehtoiseen hakumenetelmään. Nyt vertaillaan puolestaan kahta yleiskäyttöistä lajittelumenetelmää. Seuraavassa oletetaan, että 1) Syötteenä annetaan n (n > 0) alkiota sisältävä vektori, joka sisältää alkiot A[1], A[2], … A[n] indeksointi aloitetaan ykkösestä 2) Tulosteeksi halutaan täsmälleen syötteen alkuperäiset alkiot, mutta lajiteltuina ei-vähenevään suuruusjärjestykseen, eli lajittelun valmistuttua A[1] ≤ A[2] ≤ … ≤ A[n]. • Tarkastellaan ensiksi lisäyslajittelua, joka toimii seuraavasti: 1) Kierroslaskuri i saa silmukassa vuoron perään arvot 2, 3, …, n. Kierroksen i alkaessa A[1..i–1] on jo järjestetty. 2) Kopioidaan alkio A[i] muuttujaan j. Oletetaan, että siitä tulee järjestetyn osan k. pienin alkio. 3) Viedään j oikealle paikalleen. Kuitenkin ennen asetusta A[k] := j joudutaan kaikki alkiot väliltä A[k..i–1] siirtämään vähenevässä indeksijärjestyksessä yhdellä positiolla oikealle päin, ettei kyseisellä indeksialueella olevia arvoa menetetä kirjoittamalla niiden päälle. 4) Vaiheita 1–3 toistetaan, kunnes viimeinenkin alkio on viety oikealle paikalleen. kun i kasvaa arvoon n + 1, on vektori valmiiksi lajiteltu. 1. Algoritmien asema tietojenkäsittelyssä • Selvitetään luennolla lisäyslajittelun etenemistä numeerisella esimerkillä, jossa edeltäneiden lineaari- ja puolitushakuesimerkkien vektorin A alkiot on sekoitettu nyt mielivaltaiseen järjestykseen. Alempana nähtävissä silmukan kierrokset i:n arvoilla 2–7. A= 1 2 3 4 5 6 7 8 9 10 11 31 22 15 34 11 1 16 4 25 5 8 i=2 22 31 15 34 11 1 16 4 25 5 8 i=3 15 22 31 34 11 1 16 4 25 5 8 i=4 15 22 31 34 11 1 16 4 25 5 8 i=5 11 15 22 31 34 1 16 4 25 5 8 i=6 1 11 15 22 31 34 16 4 25 5 8 i=7 1. Algoritmien asema tietojenkäsittelyssä • Seuraavassa lisäyslajittelun eteneminen pääsilmukan viimeisillä kierroksilla (i:n arvot 8–11): 1 2 3 4 5 6 7 8 9 10 11 1 11 15 16 22 31 34 4 25 5 8 i=8 1 4 11 15 16 22 31 34 25 5 8 i=9 1 4 11 15 16 22 25 31 34 5 8 i = 10 1 4 5 11 15 16 22 25 31 34 8 i = 11 i = 12, algoritmin suoritus päättyy, ja lopputuloksena saadaan A lajiteltuna: 1 • • 4 5 8 11 15 16 22 25 31 34 Esimerkissä järjestetyt alkiot on värjätty punaisella, tutkittava alkio keltaisella ja vielä järjestämättömät valkoisella. Sininen nuoli kuvaa tutkittavan alkion siirtoa ja punainen nuoli alkupään alkioiden siirtoa yhdellä eteenpäin oikeasta reunasta aloittaen. Havaitaan, että järjestetyn osan pidetessä vielä järjestämättömien, mutta alkuosaan kuuluvien siirtomatkat ja siirroista aiheutuva järjestetyn osan kopiointityö alkavat lisääntyä melkoisesti! 1. Algoritmien asema tietojenkäsittelyssä • Tarkastellaan seuraavaksi vaihtoehtoista lajittelualgoritmia, limityslajittelua, joka toimii seuraavasti (jos alla olevassa algoritmissa alku ≥ loppu, ei tehdä mitään): Syöte: Vektorin A osavektorin A[alku], …, A[loppu] alkiot. Aluksi alku = 1 ja loppu = n. • JOS alku < loppu /* vieläkö tarkasteltava osavektori voidaan osittaa (likimain) kahtia? */ 1) Laske taulukon keskikohta, joka on (alku + loppu)/2. 2) Lajittele rekursiivisesti alkuosa eli alkiot A[alku], … A[keskikohta]. 3) Lajittele rekursiivisesti loppuosa eli alkiot A[keskikohta + 1], … A[loppu]. 4) Limitä rekursiivisesti lajitellut vektorinpuoliskot lajitelluksi osavektoriksi. Katsotaan seuraavaksi, miten limityslajittelu etenee samalle syöteaineistolle kuin edellä lisäyslajittelulla käsitellylle: 1) Alkutilanne: 1 2 31 22 3 4 5 6 7 8 9 10 11 15 34 11 1 16 4 25 5 8 keskikohta (1 + 11) / 2 = 6 halkaisu osavektoreiksi A[1..6] ja A[7..11] 2) Tilanne 1. tason halkaisun jälkeen: 1 2 3 4 5 6 7 8 9 10 11 31 22 15 34 11 1 16 4 25 5 8 1. Algoritmien asema tietojenkäsittelyssä keskikohdat = (1 + 6) / 2 = 3 ja (7 + 11) / 2 = 9 halkaisu osavektoreiksi A[1..3], A[4..5], A[7..9] ja A[10..11] 3) Tilanne 2. tason halkaisujen jälkeen: 1 2 3 4 5 6 7 8 9 10 11 31 22 15 34 11 1 16 4 25 5 8 saadaan neljä osavektoria A[1..3], A[4..6], A[7..9] ja A[10..11], jotka halkaistaan edelleen paikoista 2, 5, 8 ja 10. halkaisu kahdeksaksi osavektoriksi A[1..2], A[3..3], A[4..5], A[6..6], A[7..8], A[9..9], A[10..10] ja A[11..11] 4) Tilanne 3. tason halkaisujen jälkeen: 1 2 3 4 5 6 31 22 15 34 11 1 7 8 9 10 11 16 4 25 5 8 ainoastaan osavektoreiden 1, 3, ja 5 ositusta voidaan jatkaa muut eli jo yhden mittaisiksi puristuneet osavektorit jäävät toistaiseksi ”lepäämään” ja odottamaan, että vielä halkaisemattomat osavektorit on käsitelty loppuun asti 1. Algoritmien asema tietojenkäsittelyssä 5) Tilanne 4. tason halkaisujen jälkeen: (vain halkaisuun osallistuneet osavektorit esitetty) 1 31 2 3 4 5 6 7 8 9 10 11 22 (15) 34 11 (1) 16 4 (25) (5) (8) Nyt on halkaisut suoritettu loppuun asti, joten rekursio alkaa palautua. lähdetään kokoamaan ratkaisua limittämällä kullakin tasolla olevat osavektorit pareittain ei-vähenevään suuruusjärjestykseen limityksessä tulokseen viedään aina tarkasteltavien osavektoreiden pienin alkio tarjolla olevista parin muodostavat aina ne kaksi ositetta, jotka ovat muodostuneet rekursiossa yhden ja saman aktivaation aikana (algoritmin kohdat 2 ja 3) tasolla 4 järjestetään siis pareittain osavektorit 1 – 2, 4 – 5 ja 7 – 8 saadaan aikaan seuraavat, kahden mittaiset järjestetyt osavektorit (kaikissa kolmessa parissa vaihtuu tällä kertaa järjestys alkuperäisestä): 22 31 11 34 4 16 palataan rekursiossa yksi taso ylöspäin tasolle 3, jossa viisi yhden mittaista osavektoria odottaa limitystä 1. Algoritmien asema tietojenkäsittelyssä 6) Tilanne ennen tason 3 limitystä: 1 2 3 4 5 6 7 8 9 10 11 22 31 15 11 34 1 4 16 25 5 8 limitetään pareittain tason osavektorit 1 – 2, 3 – 4, 5 – 6 sekä 7 – 8 saadaan tulokseksi 4 järjestettyä osavektoria, joiden pituus on 2 tai 3 rekursion taso 3 päättyy, ja siirrytään takaisin tasolle 2 limitysvaiheeseen 7) Tilanne ennen tason 2 limitystä: 1 2 3 4 5 6 7 15 22 31 1 11 34 4 8 9 10 11 16 25 5 8 limitetään pareittain tason osavektorit 1 – 2 sekä 3 – 4 saadaan tulokseksi 2 järjestettyä osavektoria, joiden pituudet ovat 6 ja 5 rekursion taso 2 päättyy, ja palataan ylimmälle rekursiotasolle: alkuperäisen kutsun limityskomentoon 1. Algoritmien asema tietojenkäsittelyssä 8) Tilanne ennen viimeistä eli tason 1 limitystä: 1 2 3 4 5 6 1 11 15 22 31 34 7 8 9 10 11 4 5 8 16 25 limitetään pareittain jäljellä olevat osavektorit 1 – 2 saadaan tulokseksi alkuperäinen vektori järjestettynä ei-vähenevään järjestykseen koko limityslajittelualgoritmin suoritus päättyy 9) Lopputilanne: • 1 2 3 4 5 6 7 8 9 10 11 1 4 5 8 11 15 16 22 25 31 34 Muutamia ensi havaintoja limityslajittelun etenemisestä (tarkempi analyysi myöhemmin): 1) Halkaisuoperaatio on itse asiassa erittäin yksinkertainen: siihen kuuluu ainoastaan uuden osituskohdan määrääminen mitään muita toimenpiteitä ei tapahdu 2) Kaikki alkiot käydään läpi ainoastaan niin monta kertaa, miten monelle rekursiotasolle ne osallistuvat 3) Syötteen koon kaksinkertaistuminen tuottaa vain yhden rekursiotason lisää! alkioparien vertailujen määrä kasvaa hitaasti verrattuna lisäyslajitteluun 1. Algoritmien asema tietojenkäsittelyssä • Seuraavassa esitetään vielä äskeisen limityslajitteluesimerkin kutsupino, jotta nähtäisiin, miten suoritus etenee järjestyksessä (edellä näytettiin, mitä tapahtuu eri tasoilla): 1. Limityslajittelu([31, 22, 15, 34, 11, 1, 16, 4, 25, 5, 8]), alku = 1, loppu = 11 2. Limityslajittelu([31, 22, 15, 34, 11, 1]), alku = 1, loppu = 6 3. Limityslajittelu([31, 22, 15]), alku = 1, loppu = 3 4. Limityslajittelu([31, 22]), alku = 1, loppu = 2 5. Limityslajittelu([31]) X (alku = loppu = 1: kutsu päättyy) 6. Limityslajittelu([22]) X (alku = loppu = 2: kutsu päättyy) Limitä([31], [22]) osavektori A[1..2] := [22, 31] 7. Limityslajittelu([15]) X (alku = loppu = 3: kutsu päättyy) Limitä([22, 31], [15]) osavektori A[1..3] := [15, 22, 31] 8. Limityslajittelu([34, 11, 1]), alku = 4, loppu = 6 9. Limityslajittelu([34, 11]), alku = 4, loppu = 5 10. Limityslajittelu([34]) X (alku = loppu = 4: kutsu päättyy) 11. Limityslajittelu([11]) X (alku = loppu = 5: kutsu päättyy) Limitä([34], [11]) osavektori A[4..5] := [11, 34] 12. Limityslajittelu([1]) X (alku = loppu = 6: kutsu päättyy) Limitä([34, 11], [1]) osavektori A[4..6] := [1, 11, 34] Limitä([15, 22, 31], [1, 11, 34]) osavektori A[1..6] := [1, 11, 15, 22, 31, 34] 13. Limityslajittelu([16, 4, 25, 5, 8]), alku = 7, loppu = 11) 14. Limityslajittelu([16, 4, 25]), alku = 7, loppu = 9 15. Limityslajittelu([16, 4]), alku = 7, loppu = 8 16. Limityslajittelu([16]) X (alku = loppu = 7: kutsu päättyy) 17. Limityslajittelu([4]) X (alku = loppu = 8: kutsu päättyy) Limitä([16, 4]) osavektori A[7..8] := [4, 16] 18. Limityslajittelu([25]) X (alku = loppu = 9: kutsu päättyy) Limitä([4, 16], [25]) osavektori A[7..9] := [4, 16, 25] 19. Limityslajittelu([5, 8], alku = 10, loppu = 11 20. Limityslajittelu([5]) X (alku = loppu = 10: kutsu päättyy) 21. Limityslajittelu([8]) X (alku = loppu = 11: kutsu päättyy) Limitä([5], [8]) osavektori A[10..11] := [5, 8] Limitä([4, 16, 25], [5, 8]) osavektori A[7..11] := [4, 5, 8, 16, 25] Limitä[1, 11, 15, 22, 31, 34], [4, 5, 8, 16, 25]) vektori A[1..11] = [1, 4, 5, 8, 11, 15, 16, 22, 25, 31, 34] tehtävä valmis 1. Algoritmien asema tietojenkäsittelyssä • • • Seuraavassa on taulukko, joka esittää muutamien käytännön lajittelumenetelmien toteutuneita suoritusaikoja, kun kone pystyy laskemaan miljardi alkeisoperaatiota sekunnissa Taulukkoon ei ole merkitty aikakompleksisuuteen vaikuttavia korkeinta termiä alempiasteisia termejä Tästä syystä esimerkiksi aikavaativuudeltaan samaa kertaluokkaa olevat limitys- ja pikalajittelu eroavat suoritusajoiltaan: pikalajittelu osoittautuu tehokkaammaksi selitys: pienemmät kertoimet alempiasteisilla termeillä kuin limityslajittelussa Aikavaativuuden kertaluokka n = 50 000 n = 2 000 000 Lisäyslajittelu Ο(n2) 17.956 s lähes 8 tuntia Limityslajittelu Ο(nlog2n) 0.062 s 6.797 s Pikalajittelu Ο(nlog2n) 0.031 s 3.047 s Ο(n) 0.015 s 1.734 s Lajittelumenetelmä Laskentalajittelu • Taulukosta on helposti pääteltävissä, että jo 50 000:n kokoisella syötteellä lisäyslajittelu on tuntuvasti hitaampi kuin muut menetelmät, mutta syötteen koon ollessa 2 000 000 se on jo kelvoton menetelmä ei ole siis yhdentekevää, mikä lajittelumenetelmä kannattaa valita pitkille syötteille! lyhyillä syötteillä menetelmän valinnalla on vähemmän merkitystä. 1. Algoritmien asema tietojenkäsittelyssä • • • Algoritmin suoritusaikaa kuvaava lauseke antaa verrattain hyvän ennakkokäsityksen siitä, miten tehokkaan tarkasteltavan algoritmin voi olettaa olevan sovellettavaksi. käytännön tehokkuus tosin paljastuu parhaiten lopulta vasta suorittamalla algoritmia – isot vakio- ja alempiasteisten termien kertoimet voivat tuntua käytännön suoritusajassa – mutta huono teoreettinen suoritusaika ei lupaa hyvää käytännön kannalta Kaikki taulukossa esitetyt lajittelumenetelmät ovat aikavaativuudeltaan polynomiaalisia, mikä tarkoittaa sitä, että suoritusajan korkeinta astetta oleva termi on enintään jokin syötteen koon potenssi k, eli suoritusaika on tällöin Ο(nk), missä k on vakio. On kuitenkin olemassa myös sellaisia ongelmia, joita ei ole mahdollista ratkaista polynomiaalisessa ajassa, vaan niiden ratkaisemiseen kuluva aika on eksponentiaalinen. Tällöin syötteen kokoa kuvaava muuttuja n esiintyy suoritusaikalausekkeen eksponentissa, eli lauseke on muotoa T(n) = kn, missä k > 1, n > 0 (merkintä T(n) tarkoittaa n kokoisen syötteen ratkaisemiseksi tarvittavaa aikaa). muun muassa Hanoin tornit on esimerkki algoritmista, jonka aikakompleksisuus on eksponentiaalinen, ja sitä kuvaa lauseke T(n) = 2n. mikäli oletetaan, että n:n kokoinen tehtävä vaatii tasan 2n alkeisoperaatiota, ja kone laskee miljardi (1 000 000 000) alkeisoperaatiota sekunnissa voidaan todeta, että n = 20 T(20) = 220 = 1 048 576 alkeisoperaatiota aikaa tarvitaan 0.001 s n = 40 T(40) = 240 = 1 099 511 627 776 alkeisoperaatiota aikaa tarvitaan ≈ 1099.5 s ≈ 18 min 20 s n = 60 T(60) = 260 = 1 152 921 504 606 846 976 alkeisoperaatiota aikaa tarvitaan ≈ 1 152 921 504.6 s ≈ 36 v 7 kk Koneen tehon kymmenkertaistuminenkaan ei juuri lohduta, jos n = 60 ja T(n) = 2n … ! • 1. Algoritmien asema tietojenkäsittelyssä • • Eksponentiaalisia ongelmia voidaan pitää käytännössä kelvottomina. Tähän ryhmään kuuluvat lisäksi muun muassa seuraavat ongelmat: Graafin väritys: voidaanko graafi värittää k värillä siten, että sen jokaisesta pisteestä lähtevät viivat ovat erivärisiä? Hamiltonin sykli: löytyykö graafista polkua siten, että sitä pitkin kuljettaessa vieraillaan graafin jokaisessa pisteessä tarkalleen kerran ja viimeisestä pisteestä palataan vielä takaisin lähtöpaikkaan? Kauppamatkustajan ongelma: löytyykö graafista Hamiltonin sykliä, jonka pituus on enintään ennalta asetetun ylärajan k suuruinen, kun graafin jokaiseen viivaan liittyy sen päätepisteiden etäisyyttä paino? 1. Algoritmien asema tietojenkäsittelyssä • Algoritmien esittämisestä tällä kurssilla: 1) Aluksi esitetään sanallinen kuvaus siitä, että Millaisesta ongelmasta on kyse Miten algoritmi etsii ratkaisua tarkasteltavaan ongelmaan Mitä annetaan syötteeksi ja mitä tulostetaan Mitä erityisiä merkintöjä käytetään algoritmin kuvaamiseksi 2) Algoritmista esitetään pseudokielinen ohjelma Käytetty pseudokoodi muistuttaa melko lailla kursseilla JIT1 ja JIT2 käytettyä Käskysulkuja, kuten myöskään ehto- ja toistorakenteiden lopetinsanoja ei käytetä, vaan ne on korvattu sisennyksillä (poikkeus: REPEAT-UNTIL), ellei se ole välttämätöntä selvyyden kannalta (esimerkiksi ehdon tai toiston vaikutusalueen ollessa hyvin pitkä) 3) Esitetään algoritmin toimintaa havainnollistavia esimerkkejä 4) Todetaan algoritmin oikeellisuus 5) Analysoidaan algoritmin vaatima suoritusaika 2. Algoritmiikan perusteet 2.1 Algoritmien analysointi • • • • Algoritmia analysoitaessa pyritään arvioimaan sen resurssien tarvetta. Tarkasteltavana resurssina on useimmiten aika, mutta myös sen muistinkulutusta voidaan mitata. Näitä harvemmin voi tarkasteltavana resurssina olla myös jokin muu, kuten laitteistovaatimukset (tietoliikenteen tarpeisiin) yms. Tällä kurssilla keskitytään lähestulkoon ainoastaan algoritmin ajankäytön analysointiin. Tätä merkitään termillä T(n), missä n edustaa syötteen kokoa, joka voi tarkoittaa esimerkiksi 1) käsiteltävien alkioiden lukumäärää (listan, taulukon tai muun tietorakenteen koko) 2) tietokantaan tallennettujen tietueiden lukumäärä (esimerkiksi B-puiden yhteydessä) 3) pisteiden ja kaarten määrää graafialgoritmeissa (molemmat arvot esitetään parina) Miten arvioida algoritmien suoritusaikaa? 1) lasketaan ohjelman ns. alkeisaskeleiden eli yksinkertaisten operaatioiden kokonaismäärä 2) analyysissä oletetaan, että jokaisen samaa tyyppiä olevan alkeisaskeleen suorituskustannus on vakio (esimerkiksi minkä tahansa alkion välinen vertailu, kahden luvun välinen aritmeettinen operaatio jne.) 3) useimmiten turvaudutaan yksistään algoritmin pahimman tapauksen analysointiin ainakaan tätä kauemmin ei algoritmin suoritus voi teoriassa kestää! 4) tällä kurssilla ei kiinnitetä huomiota algoritmien empiiriseen testaamiseen! 2.1 Algoritmien analysointi • Ennen analysointiin ryhtymistä esitellään vielä muutamia kurssilla käytettävään pseudokoodiin liittyviä oletuksia: 1) käskysulkuja eikä ehto- ja toistolauseiden lopettavia sanoja ei käytetä, ellei se ole esityksen selvyyden kannalta välttämätöntä (poikkeus: REPEAT-UNTIL); muutoin käytetään pelkkiä sisennyksiä 2) kommentti merkitään merkkiparilla /* */, ja se voi jatkua rivinvaihdonkin ylitse 3) taulukoiden indeksointi alkaa aina ykkösestä 4) kaikki käytetyt muuttujat ovat tällä kurssilla paikallisia. Sen sijaan jatko-osassa käytetään graafialgoritmeissa tapahtuman aikaleiman osoittamiseksi globaalia muuttujaa. 5) taulukosta A[1..n] voidaan valita osataulukko A[i..j], missä 1 ≤ i, j ≤ n 6) muuttujien tunnukset esitetään kursiivilla 7) FOR-silmukan laskuria saa käyttää silmukan jo päätyttyäkin. Tällöin oletetaan, että laskurin silmukan jälkeinen arvo on sama, jolla toistoa ei enää jatkettu (yhden askeleen verran yli maksimin tai alle minimin) 8) on käytettävissä funktio pituus(x), joka palauttaa argumenttina annettavan vektorin pituuden, eli vektorin pituustietoa ei tarvitse välttämättä välittää syötteenä 2.1 Algoritmien analysointi 9) asetusoperaattorina käytetään merkintää ”:=” 10) merkintä nil tarkoittaa tyhjää osoitinta linkitetyissä rakenteissa 11) loogisia lausekkeita lasketaan vasemmalta oikealle ja sen arvon määräämiseksi käytetään ns. oikosulkuevaluaatiota jos lausekkeen totuusarvo käy jo ilmi, se kiinnitetään heti, kun tämä on mahdollista (vaikkei lauseketta olekaan käsitelty vielä loppuun asti) esimerkki: IF x ≠ nil AND x.arvo > 6 jos x on tyhjä osoitin, ei oikeanpuoleista ehtoa mennä enää testaamaan, joten lauseke ei johda ajonaikaiseen virheeseen vältytään usein sisäkkäisten ehtolauseiden kirjoittamiselta, jottei määrittelemätöntä arvoa käytäisi testaamassa 12) algoritmeissa esiintyvät pseudokielen varatut sanat (IF, FOR jne.) kirjoitetaan isolla ja ne myös lihavoidaan 13) samoin algoritmien nimet on kirjoitettu isolla 14) jos samalla algoritmin rivillä esiintyy useita käskyjä, niiden erottimena toimii puolipiste ”;” 15) syötteen kokoa (usein sama kuin alkioiden lukumäärä) merkitään tunnuksella n 2.1 Algoritmien analysointi • • Aloitetaan algoritmien analysointi lähtemällä liikkeelle lisäyslajittelusta, jota käsiteltiin jo edellä sanallisen kuvauksen ja esimerkin avulla kalvopaketin sivuilla 11 – 13. Lisäyslajittelun pseudokoodilistaus on esitetty seuraavassa: LISÄYSLAJITTELU(A) 1 FOR j := 2, 3, …, pituus(A) DO 2 alkio := A[j] /* Otetaan paikassa j oleva alkio talteen käsiteltäväksi. */ 3 i := j – 1 4 WHILE i > 0 AND A[i] > alkio DO 5 A[i + 1] := A[i] 6 i := i – 1 7 A[i + 1] := alkio • • Lisäyslajittelu on esimerkki ns. minimitilassa toimivista lajittelualgoritmeista. Minimitilaisuudella tarkoitetaan, että menetelmä tarvitsee toimiakseen ainoastaan vakiomäärän työmuistia. Toisin sanoen, lajittelun suorittamiseksi tarvittavan lisämuistin määrä ei riipu syötteen koosta n. Lisäyslajittelualgoritmissa ainoat tarpeelliset apumuuttujat ovat silmukkalaskurit i ja j sekä kullakin ulomman silmukan kierroksella tarkasteltavan alkion kopioimiseen tarvittava apumuuttuja alkio. 2.1 Algoritmien analysointi • • • Lähdetään nyt analysoimaan, montako kertaa algoritmin eri rivejä suoritetaan lajittelun ollessa käynnissä. Rivikohtaiset tiedot on koottu seuraavaan taulukkoon. Rivi Rivin kustannus Suorituskertojen määrä 1 c1 n 2 c2 n–1 3 c3 n–1 4 c4 5 c5 6 c6 7 c7 n–1 Taulukon riveillä 4 – 6 esiintyvä termi tj tarkoittaa, montako kertaa algoritmin rivillä 4 esiintyvän WHILE-silmukan alkuehtoa joudutaan testaamaan tietyllä j:n arvolla Kannattaa huomioida, että rivin 1 FOR-silmukan alkuehtoa testataan yhden kerran enemmän kuin silmukassa on kierroksia. Viimeisellä kerralla ainoastaan todetaan, että laskurin arvo on kasvanut n + 1:een, eli suoritusta ei enää jatketa. 2.1 Algoritmien analysointi • Nyt pystytään edellisen taulukon avulla laskemaan algoritmin kokonaissuoritusaika summaamalla rivikohtaiset kustannukset: T(n) = c1n + c2(n – 1) + c3(n – 1) + c4 • • • + c5 + c6 Algoritmin kokonaissuoritusaika vaihtelee selvästikin sen mukaisesti, mikä tj:n arvoksi kulloinkin määräytyy. Tutkitaan ensiksi paras tapaus: jos vektori A on jo alun perin järjestettynä ei-vähenevään suuruusjärjestykseen, ei eri kierroksilla käsiteltäviä alkioita eikä niiden edeltäjiä tarvitse siirtää minnekään (A[1] ≤ A[2] ≤ … ≤ A[j – 1] ≤ A[j] = alkio). tällöin jokaisella ulomman eli FOR-silmukan kierroksella j (2, 3, …, n) WHILE-silmukan aloitusehdon jälkimmäinen osa (A[i] > alkio) on epätosi silloin tj = 1 jokaiselle j:n arvolle 2, 3, … n. Koska WHILE-ehtoa testataan aina vain kertaalleen, saadaan = 1 + 1 + … + 1 = n – 1 (ykkösiä yhteensä n – 1 kappaletta) • + c7(n – 1) Tällöin vastaavasti riveillä 5 ja 6 ei vierailla kertaakaan! 2.1 Algoritmien analysointi • Siten parhaassa tapauksessa: T(n) = c1n + c2(n – 1) + c3(n – 1) + c4(n – 1) + c7(n – 1) = (c1 + c2 + c3 + c4 + c7)n – (c2 + c3 + c4 + c7) = an + b edellä kertoimien c1, c2, c3, c4 ja c7 summa on nimetty uudelleen vakiolla a ja vastaavasti summa c2 + c3 + c4 + c7 vakiolla b syötteen koko n esiintyy lausekkeessa ainoastaan ensimmäisen asteen termissä parhaassa tapauksessa lisäyslajittelun suoritusaika on lineaarinen eli suoraan verrannollinen lajiteltavien alkioiden lukumäärään • Tarkastellaan nyt vastaavasti tilannetta pahimmassa tapauksessa, jolloin lajittelun suorittamiseksi tarvittavan työn määrä maksimoituu: jos lajiteltava vektori on alun perin aidosti vähenevässä järjestyksessä, joudutaan jokaisella ulomman silmukan kierroksella testaamaan WHILE-silmukan aloitusehtoa j kertaa. Viimeisellä eli j. testauskerralla ehdon alkuosa i > 0 ei enää toteudu. tällöin T(n) = c1n + c2(n – 1) + c3(n – 1) + c4 + c5 + c6 + c7(n – 1) • Nyt pitäisi pystyä vielä avaamaan termien 4 – 6 summalausekkeet. 2.1 Algoritmien analysointi • Aritmeettisen sarjan Sn = summa voidaan määrätä seuraavasti: Sn = 1 + 2 + 3 + … + n – 1 + n (alusta loppuun päin) Sn = n + n – 1 + n – 2 + … + 2 + 1 (lopusta alkuun päin) __________________________________________________________________ 2Sn = (n + 1) + (n + 1) + (n + 1) + … + (n + 1) + (n + 1) (termien 2-kertainen summa) • • Koska yhteenlaskettavia on yhteensä n kappaletta, summa 2Sn = n(n + 1) Sn = n(n + 1)/2 Siten lisäyslajittelun pahimmassa tapauksessa ... = – 1 = n(n + 1)/2 – 1, ja vastaavasti = • = ½(n – 1)n, joten … T(n) = c1n + c2(n – 1) + c3(n – 1) + c4(n(n + 1)/2 – 1) + c5n(n – 1)/2 + c6n(n – 1)/2 + c7(n – 1) = ½(c4 + c5 + c6)n2 + (c1 + c2 + c3 + ½c4 + ½c5 + ½c6 + c7)n – (c2 + c3 + c4 + c7) = an2 + bn + c, missä a = ½(c4 + c5 + c6), b = (c1 + c2 + c3 + ½c4 + ½c5 + ½c6 + c7) ja c = -(c2 + c3 + c4 + c7) Lisäyslajittelun pahinta tapausta kuvaava suoritusaika on neliöllinen (syötteen koon 2. potenssi) 2.1 Algoritmien analysointi • • • Lisäyslajittelusta tekee aikavaativuudeltaan neliöllisen se, että siihen kuuluu kaksi sisäkkäistä silmukkaa, joiden kummankin suorituskertojen määrä riippuu n:stä teoreettinen silmukoiden sisältämien lauseiden suorituskertojen yläraja olisi n * n, mutta todellisuudessa tätä ei lisäyslajittelussa koskaan saavuteta (luennolla tästä esimerkki) Silmukoiden sisällä suoritettavat yksittäiset operaatiot ovat kustannukseltaan vakioaikaisia esimerkiksi kahden alkion välinen vertailu, alkion siirto toiseen paikkaan yksittäisen tällaisen operaation kustannus ei ole riippuvainen syötteen koosta Miksi analysoida juuri pahinta tapausta? Perusteita tälle: 1) Saadaan yläraja algoritmin suoritusajalle: suoritus ei varmasti kestä pidempään! 2) Pahin tapaus saattaa esiintyä verrattain usein etsitään esimerkiksi vektorista tai linkitetystä listasta alkiota, jota siellä ei esiinny 3) Niin sanotun ”keskimääräisen tapauksen” määritteleminen ei ole välttämättä aivan suoraviivaista, ja vaikka näin olisikin, se saattaa olla vaikeudeltaan samaa kertaluokkaa pahimman tapauksen kanssa! ajatellaanpa esimerkkinä lisäyslajittelua Oletetaan, että kierroksella j tutkittava alkio on aina suurempi kuin järjestetyn osan alkupuoliskon alkiot mutta aina pienempi kuin sen loppupuoliskon alkiot. Tällöin tj ≈ j / 2. Jos tämä sijoitetaan suoritusaikalausekkeeseen, saadaan ≈ • =½ = ¼n(n + 1) – ½ = ¼(n2 + n – 2) Saatu lauseke on yhä edelleen neliöllinen n:n funktio! keskimääräinen tapaus ≈ pahin tapaus! 2.2 Algoritmien suunnittelu • • • • Tietyn tehtävän suorittava algoritmi voidaan yleensä suunnitella ja toteuttaa usealla vaihtoehtoisella tavalla. Yksi mahdollinen tapa algoritmin ratkaisutapa on rekursio. Rekursiivinen algoritmi pyrkii ratkaisemaan alkuperäisen tehtävän kokoamalla ratkaisun samankaltaisten, mutta alkuperäistä pienempien ongelmien osaratkaisuista. Siten tällainen algoritmi kutsuu itseään, kunnes tarkasteltava osaongelma on niin pieni, että se ratkeaa suoraan eli triviaalisti. Kun algoritmin kontrolli siirtyy rekursiivisen kutsun kohdalle, algoritmista käynnistyy tällöin ns. uusi aktivaatio. Samalla aikaisempi aktivaatio keskeytyy odottamaan sitä, että uusi saadaan vietyä päätökseen. Yhdestä algoritmista voi olla samanaikaisesti luotuna mielivaltaisen monta aktivaatiota, mutta vain yhtä niistä käsitellään kerrallaan: muut ovat ”lepäämässä” rekursiopinossa. Aktivaatioita käsitellään pinomaisesti, eli mitä aikaisemmin aktivaatio on käynnistynyt, sitä myöhemmin se päättyy (vrt. edellä esitetty limityslajittelun rekursiopino: viimeksi valmistuu alkuperäinen tehtävä). Jokaisella aktivaatiolla on omat paikalliset muuttujansa, vaikkakin ne ovat muodollisesti saman nimisiä toisten aktivaatioiden vastaavien muuttujien kanssa. Siten ei ole pelkoa, että vaikkapa nykyisessä aktivaatiossa tehtävä asetus a := a + b muuttaisi aikaisemmin käynnistyneissä aktivaatioissa esiintyvän muuttujan a arvoa. Suoraan eli ilman rekursiivista kutsua ratkeavaa tehtävää kutsutaan rekursion kannaksi tai perustapaukseksi. 2.2 Algoritmien suunnittelu • Jotta rekursiivinen ratkaiseminen olisi mahdollista, pitää seuraavien kahden ehdon täyttyä: 1) Algoritmille on määriteltävä ainakin yksi perustapaus 2) Joka kerta, kun muodostetaan uusi rekursiivinen kutsu, tehtävän pitää helpottua aikaisemmasta. Tämä tarkoittaa sitä, että kutsun suorittaminen vie lähemmäs jotain tällaista suoraan ratkeavaa tapausta. • • Elleivät molemmat ehdot täyty, rekursio ei milloinkaan pääty muutoin paitsi mahdollisesti tietokoneen ajonaikaisen muistipinon täyttymiseen, jos algoritmi ajautuu aina vain loitommas kaikista perustapauksesta (jokainen uusi rekursiivinen kutsu varaa muistia uusien aktivaatioiden paikallisia muuttujia varten, mikä ei voi jatkua loputtomiin) Tarkastellaan seuraavaksi ns. hajota ja hallitse -tekniikkaa (lat. divide et impera), joka perustuu rekursioon ja jota käytetään hyväksi muun muassa lajittelualgoritmeissa. Tekniikan voi todeta sisältävän kolme eri vaihetta: 1) Ellei ongelma ole triviaali, hajota se aliongelmiksi 2) Hallitse ratkaisemalla rekursiivisesti jokainen aliongelma 3) Yhdistä aliongelmien ratkaisut alkuperäisen ongelman ratkaisuksi • Yksi tunnetuimmista hajota ja hallitse -algoritmeista on jo edellä alustavasti tarkasteltu limityslajittelu. 2.2 Algoritmien suunnittelu • Seuraava hahmotelma kuvaa limityslajittelun etenemistä Hajota Hallitse Yhdistä 2.2 Algoritmien suunnittelu • • Limityslajittelussa hajottaminen tapahtuu halkaisemalla tarkasteltava osavektori kahtia, hallitseminen ratkaisemalla syntyneet osaongelmat rekursiivisesti ja yhdistäminen limittämällä osaratkaisujen tulokset yhdeksi pidemmäksi järjestetyksi vektoriksi. Hajottaminen vaatii pelkän keskikohdan määräämisen ja hallitseminen kaksi rekursiivista kutsua , eli ne ovat erittäin helppoja toimenpiteitä. Limitys vaatii sen sijaan enemmän työtä. Tarkastellaan seuraavaksi limitysalgoritmia. LIMITYS(A, p, q, r) 1 n1 := q – p + 1 2 n2 := r – q 3 Perusta apuvektorit V[1..n1 + 1] ja O[1..n2 + 1] 4 FOR i := 1, 2, …, n1 DO 5 V[i] := A[p + i – 1] 6 FOR j := 1, 2, …, n2 DO 7 O[i] := A[q + j] 8 V[n1 + 1] := ∝ 9 O[n2 + 1] := ∝ 10 i := 1; j := 1; 11 FOR k := p, p + 1, …, r DO 12 IF V[i] ≤ O[j] 13 THEN A[k] := V[i] 14 i := i + 1 15 ELSE A[k] := O[j] 16 j := j + 1 2.2 Algoritmien suunnittelu • Ennen limityslajittelun analyysiä mainittakoon vielä muutama sana algoritmien suoritusaikojen kasvunopeudesta Lisäyslajittelun pahimman tapauksen suoritusajaksi laskettiin edellä T(n) = an2 + bn + c, missä a, b ja c ovat joitain nollasta eroavia vakioita Koska jatkossa ollaan kiinnostuneita ainoastaan suoritusajan kasvunopeuden suuruusluokasta, tarkastellaan suoritusaikalausekkeesta ainoastaan korkeinta astetta olevaa termiä. alempiasteisten termien vaikutus heikkenee sitä mukaa, kun n kasvaa sama koskee myös korkeinta astetta olevan termin kerrointa tarkastellaan myöhemmin asiaa koskevia esimerkkejä Siten voidaan todeta, että lisäyslajittelun pahimman tapauksen suoritusaika on suuruusluokkaa Ο(n2). • Seuraavassa esitetään limityslajittelualgoritmi kokonaisuudessaan … LIMITYSLAJITTELU(A, p, r) 1 IF p < r 2 THEN q := (p + r)/2 3 LIMITYSLAJITTELU(A, p, q) 4 LIMITYSLAJITTELU(A, q + 1, r) 5 LIMITYS(A, p, q, r) 2.2 Algoritmien suunnittelu • … ja suoritetaan sille seuraavaksi analyysi: Todetaan aluksi, että yhdistämisvaiheessa käytettävä algoritmi LIMITYS toimii ajassa O(n),sillä Rivien 1 – 3 ja 8 – 10 suoritus vie vakioajan (pelkkiä asetuslauseita ja muistin varauksia) Rivien 4 – 7 FOR-silmukoiden suoritus vie ajan O(n1 + n2) = O(n) Rivien 11 – 16 FOR-silmukka pyörii n kertaa, ja kaikki siinä suoritettavat lauseet ovat vakioaikaisia Sitten itse pääalgoritmi: Rivin 1 testi: vakioaikainen eli O(1) Rivillä 2 tapahtuva halkaisu: samoin vakioaikainen Rivien 3 ja 4 rekursiiviset kutsut: T(n/2) + T(n/2) = 2T(n/2) Rivin 5 limitys: O(n) Siten saadaan: T(n) = Ο(1), kun n = 1 = 2T(n/2) + Ο(n) , kun n > 1 • • Kannattaa huomioida, että Ο(n) + Ο(1) = Ο(n). Vastaavasti Ο(n2) + Ο(n) = Ο(n2). Myöhemmin tullaan näyttämään toteen, että limityslajittelulle T(n) = Ο(n log2n) 3 Funktioiden kasvunopeus 3.1 Asymptoottinen merkintätapa • • • • • Kuten jo edellä lyhyesti mainittiin, algoritmin aikavaativuus kuvataan yleensä esittämällä sen tarkan teoreettisen suoritusajan asemesta vain sen suuruus- eli kertaluokka. tällä tarkoitetaan sitä, miten algoritmin suoritusaika muuttuu suhteessa syötteen kokoon silloin, kun syötteen koon annetaan kasvaa rajatta Yleisimmin käytetyt kuvaustavat ovat ns. Θ- (theta) ja Ο- (iso ordo) notaatiot Θ-merkinnällä tarkoitetaan ns. asymptoottista yhdistettyä ylä- ja alarajaa Mikäli jonkin algoritmin suoritusaikaa kuvaa lauseke T(n) = Θ(n), tarkoittaa se sitä, että kyseisen algoritmin ajankäyttö on kaikilla kelvollisilla syötteillä suoraan verrannollinen syötteen pituuteen n, kunhan n on riittävän iso. Matemaattisesti Θ-merkintä voidaan ilmaista seuraavasti: ∃ c1, c2 ∈ R+ ja ∃ n0 ∈ N siten, että Θ(g(n)) = { f(n) | 0 ≤ c1g(n) ≤ f(n) ≤ c2g(n) jokaiselle n ≥ n0 } • ”Suomennettuna” edellinen merkintä tarkoittaa, että algoritmin ajankäyttöä ilmaiseva funktio (lauseke) f(n) kuuluu kasvunopeusluokkaan Θ(g(n)) silloin, kun löydetään mielivaltaiset kaksi positiivista reaalilukuvakiota c1 ja c2 sekä jokin syötteen koko n0 ≥ 0 , josta lähtien lauseke c1g(n) pysyy aina pienempänä tai yhtä suurena kuin f(n), ja tämä puolestaan pysyy aina pienempänä tai yhtä suurena kuin c2g(n). funktion f(n) kuvaaja sijoittuu n:n arvosta n0 lähtien kuvaajien c1g(n) ja c2g(n) väliin sillä, miten f(n) käyttäytyy n0:aa pienemmillä n:n arvoilla, ei ole merkitystä! 3.1 Asymptoottinen merkintätapa • Esimerkki: Osoitetaan, että ½n2 – 3n = Θ(n2) Nyt pitää pystyä löytämään sellaiset positiiviset reaalilukuvakiot c1 ja c2 sekä syötteen koko n0, josta lähtien on kaikilla n:n arvoilla voimassa c1n2 ≤ ½n2 – 3n ≤ c2n2 Tarkastellaan aluksi epäyhtälön oikeaa puolta: ½n2 – 3n ≤ c2n2 On helppo havaita, että jos c2:n paikalle sijoitetaan ½ tai tätä suurempi arvo, epäyhtälö on tosi kaikille ei-negatiivisille n:n arvoille (½n2 – 3n ≤ ½n2). Siten voidaan valita vaikkapa c2 = ½. Sitten epäyhtälön vasen puoli: c1n2 ≤ ½n2 – 3n ⇔ c1 ≤ ½ – 3/n jaetaan epäyhtälö puolittain n2:lla sitä mukaa kun n kasvaa, vähentäjä 3/n pienenee. Kun n saavuttaa arvon 7, tulee erotuksen ½ – 3/n arvoksi aidosti positiivinen ½ – 3/7. Tästä laventamalla saadaan edelleen = 7/14 – 6/14 = 1/14. n:n arvosta 7 eteenpäin erotus ½ – 3/n vain kasvaa jatkuvasti. Siten voimme valita vakion c2 paikalle arvon 1/14 ja vakion n0 paikalle arvon 7. Vakion c1 paikalle valittiin jo edellä ½. siten (1/14)n2 ≤ ½n2 – 3n ≤ ½n2, kun n ≥ 7, joten todetaan, että ½n2 – 3n = Θ(n2).□ 3.1 Asymptoottinen merkintätapa • • Kannattaa huomioida, että algoritmien analyysissä on käytetään merkintätapaa f(n) = Θ(x), missä x on jokin aikakompleksisuusluokka, siitä huolimatta, että Θ(x) edustaa itse asiassa funktioiden joukkoa eikä mitään yksittäistä funktiota (oikeaoppisempi merkintä olisi f(n) ∈ Θ(x)). Esimerkki: Osoitetaan, että 6n3 ≠ Θ(n2) Tehdään vastaoletus ja uskotaan, että pystytään sittenkin löytämään sellaiset positiiviset reaalilukuvakiot c1 ja c2 sekä syötteen koko n0, josta lähtien on kaikilla n:n arvoilla voimassa c1n2 ≤ 6n3 ≤ c2n2 Jaetaan epäyhtälö puolittain termillä n2 ja tarkastellaan aluksi epäyhtälön vasenta puolta: c1 ≤ 6n Voidaan valita c1:n paikalle vaikkapa vakio 1, jolloin vasen puoli toteutuu kaikilla positiivisilla n:n arvoilla. Mutta mitä mahtaa tapahtua epäyhtälön oikealla puolella? Saadaan: 6n ≤ c2 Sitä mukaa kun n kasvaa, myös termin 6n arvo kasvaa, mutta vakio c2 pysyy ennallaan. valittiinpa vakio c2 miten suureksi tahansa, ennemmin tai myöhemmin kohdataan sellainen n:n arvo, jolloin 6n ylittää vakion c2 arvon, toisin sanoen mille tahansa vakiolle c2 > 0 on voimassa = ∞. vastaoletus virheellinen, joten alkuperäinen väite pitää paikkansa. □ 3.1 Asymptoottinen merkintätapa • Jotta funktio f(n) kuuluisi johonkin luokkaan Θ(x), pitää f(n)-lausekkeen korkeimman asteen termin olla sama kuin kasvunopeusluokan termi x. • Lause: Jos p(n) on astetta k oleva polynomi, eli p(n) = , missä ai:t ovat vakioita ja ak > 0, niin silloin p(n) = Θ(nk). Lauseen todistus sivuutetaan tässä, mutta todistaminen onnistuu jakamalla aikaisempien esimerkkien tapaan muodostettava kaksoisepäyhtälö termillä nk. • Esimerkki: Jos p(n) = ⅓n7 + 1192n6 + 4330n + 171009, niin p(n) = Θ(n7). • Muut paitsi polynomin korkeinta astetta olevan termi ja sen mahdollinen n:stä riippuva kerroin voidaan jättää huomiotta kasvunopeutta ilmoitettaessa, sillä n:n kasvaessa tarpeeksi paljon mainittu termi tulee peittämään muiden termien vaikutuksen alleen. korkeinta astetta oleva termi ns. dominoi lauseketta n:n ollessa riittävän iso edellisessä esimerkissä seitsemännen asteen termin vaikutus tulee pitkän päälle kaikkein suurimmaksi, vaikka vielä esimerkiksi n:n arvolla 3000 toinen termeistä on arvoltaan tätä isompi. Edellisen perusteella kannattaa muistaa, että vaikkapa lauseke p(n) = nlog2n + 4n + 1 ≠ Θ(n), sillä n:stä riippuvan logaritmikertoimen ansiosta nlog2n kasvaa nopeammin kuin n. Kannattaa lisäksi huomioida, että vakio on nollannen asteen polynomi, joten vakiofunktio kuuluu kasvunopeusluokkaan Θ(n0) = Θ(1). • • 3.1 Asymptoottinen merkintätapa • • • Ο-merkinnällä tarkoitetaan ns. asymptoottista ylärajaa Mikäli jonkin algoritmin suoritusaikaa kuvaa lauseke T(n) = Ο(n), tarkoittaa se sitä, että kyseisen algoritmin ajankäyttö on kaikilla kelvollisilla syötteillä verrannollinen korkeintaan syötteen koon 1. potenssiin, mutta ylärajan ei tarvitse olla tiukka. algoritmi saa toimia tätä nopeamminkin (toisin kuin Θ-merkinnän yhteydessä)! Matemaattisesti Ο-merkintä voidaan ilmaista seuraavasti: ∃ c ∈ R+ ja ∃ n0 ∈ N siten, että Ο(g(n)) = { f(n) | 0 ≤ f(n) ≤ cg(n) jokaiselle n ≥ n0 } • • mikäli funktio f(n) toteuttaa nämä ehdot , merkitään, että f(n) = Ο(g(n)). ”Suomennettuna” edellinen merkintä tarkoittaa, että algoritmin ajankäyttöä ilmaiseva funktio (lauseke) f(n) kuuluu kasvunopeusluokkaan Ο(g(n)) silloin, kun löydetään mielivaltainen positiivinen reaalilukuvakio c sekä jokin syötteen koko n0 ≥ 0 , josta lähtien f(n) pysyy aina pienempänä tai yhtä suurena kuin cg(n). funktion f(n) kuvaaja sijoittuu n:n arvosta n0 lähtien kuvaajan cg(n) alapuolelle. sillä, miten f(n) käyttäytyy n0:aa pienemmillä n:n arvoilla, ei ole merkitystä! Kannattaa huomioida, että f(n) = Θ(g(n)) ⇒ f(n) = Ο(g(n)). Sen sijaan päinvastaisesta ei ole mitään takeita (eli implikaatio ei ole yleisesti voimassa toiseen suuntaan)! 3.1 Asymptoottinen merkintätapa • • • • Esimerkki: Lisäyslajittelun parhaan tapauksen aikakompleksisuus T(n) = an + b = O(n2), mutta se ei kuitenkaan ole suuruusluokkaa Θ(n2). lauseketta an + b ei pysty rajoittamaan alhaalta termillä c1n2 millään kertoimen c1 arvolla! Ο-merkintää käytetään usein kuvaamaan algoritmin pahinta tapausta, eli se rajoittaa sen suoritusaikaa kaikilla kelvollisilla syötteillä vain ylhäältä päin. Algoritmin aikavaativuus on polynomiaalinen, jos se kuuluu luokkaan O(nk), kun k on jokin einegatiivinen vakio. Seuraavat laskusäännöt ovat voimassa O-merkinnöille. Ne pätevät myös Θ -merkinnöille, joten säännöistä voi ordot korvata kaikkialta thetoilla. Jos T1(n) = O(f(n)) ja T2(n) = O(g(n)), niin 1) T1(n) + T2(n) = max{O(f(n)), O(g(n))} 2) T1(n) ⋅ T2(n) = O(f(n) ⋅ (g(n)) Jos f(n) = Θ(g(n)) ⇔ = c ≠ 0. Esimerkki: Osoitetaan, että aritmeettisen sarjan summa Todistus: = ½n(n + 1) ja =½ = Θ(n2). =½ = ½. □ 3.1 Asymptoottinen merkintätapa • • Seuraavassa taulukossa on lueteltu muutamien, algoritmien analyysissä hyödyllisten funktioiden arvoja eri n:n arvoilla. n lg n n lg n n2 n3 2n 10 3.3 33 100 1 000 1 024 100 6.6 660 10 000 1 000 000 > 1030 1 000 10.0 10 000 1 000 000 1 000 000 000 > 10301 10 000 13.3 133 000 100 000 000 1 000 000 000 000 > 103 010 100 000 16.6 1 660 000 10 000 000 000 1 000 000 000 000 000 > 1030 102 1 000 000 19.9 19 900 000 1 000 000 000 000 1 000 000 000 000 000 000 > 10301 029 Hyviä muistisääntöjä: 1) mikä tahansa kasvava logaritmifunktio kasvaa asymptoottisesti nopeammin kuin yksikään vakiofunktio (sen arvo ei tietystikään riipu lainkaan syötteen koosta) 2) mikä tahansa kasvava potenssifunktio kasvaa nopeammin kuin yksikään logaritmifunktio 3) mikä tahansa kasvava eksponenttifunktio kasvaa nopeammin kuin yksikään potenssifunktio 3.2 Matemaattisia merkintöjä ja funktioita • Seuraavassa esitetään muutamia kurssilla toistuvasti käytettäviä matemaattisia määritelmiä ja laskusääntöjä. 1) Kertoma sekä katto- ja lattiafunktiot Luonnollisen luvun n kertoma (merkitään n!) määritellään rekursiivisesti seuraavasti: n! = 1, jos n = 0 = n ⋅ (n – 1)!, jos n > 0 Toisin sanoen, n! = 1 ⋅ 2 ⋅ 3 ⋅ … ⋅ n, kun n > 0 n! ≤ nn kaikilla n > 0, ja lisäksi nn kuuluu kertomaa ylempään aikavaativuusluokkaan Kattofunktiolla x tarkoitetaan luvun x pyöristämistä ylöspäin lähimpään kokonaislukuun. Lattiafunktiolla x tarkoitetaan puolestaan luvun x pyöristämistä alaspäin lähimpään kokonaislukuun. 2) Eksponenttifunktio Oletetaan, että a > 0, sekä m ja n ovat mielivaltaisia reaalilukuja a0 = 1 a1 = a a-1 = 1/a ja a-n = 1 / an (am)n = amn = (an)m aman = am + n Mikäli a > 1, niin mille tahansa vakiolle b ∈ R on voimassa seuraava tulos: =0 tämä tarkoittaa sitä, että mikä tahansa eksponenttifunktio, jonka kantaluku a > 1, kasvaa nopeammin kuin yksikään polynomifunktio sama voitaisiin ilmaista myös merkinnällä nb = ο(an), missä merkintä ο (pikku-ordo) tarkoittaa ns. epätarkkaa ylärajaa (tarkempi esittely sivuutetaan tällä kurssilla) 3.2 Matemaattisia merkintöjä ja funktioita 3) Logaritmit lg n = log2n (2-kantainen logaritmijärjestelmä: tarvitaan useimmin tietojenkäsittelyssä) log n = log10n (10-kantainen logaritmijärjestelmä) ln n = logen (luonnollinen logaritmijärjestelmä, jonka kantalukuna on Neperin luku e ≈ 2.718) logkn = (log n)k log log n = log(log n) Kaikille reaaliluvuille a, b, c > 0 on voimassa: • logc(ab) = logca + logcb logc(a/b) = logca – logcb logcan = n logca blogba = a logba = 1 / logab logba = (1 / logcb) ⋅ logca /* logaritmien muunnossääntö toiseen kantalukuun */ Viimeisestä laskusäännöstä pystyy päättelemään, että eri logaritmit eroavat toisistaan vain vakion suuruisella suhdeluvulla n:stä riippumatta. eri logaritmeilla on sama asymptoottinen kasvunopeus! kaikki logaritmit kasvavat hitaammin kuin yksikään kasvava polynomifunktio! 4 Rekursioyhtälöistä • • Rekursioon ja rekursiivisiin algoritmeihin ehdittiin tutustua jo kursseilla ”Johdatus informaatioteknologiaan I / II”. rekursiivisten algoritmien ongelmanratkaisutapa syötteen koolle n perustuu tätä aidosti pienemmän (esimerkiksi n – 1:n kokoisen) syötteen ratkaisemiseen. ajatuksena on, että sitä mukaa kun n jatkuvasti pienenee, tehtävä ratkeaa alkuperäistä helpommin. kun aikanaan n tulee riittävän pieneksi, tehtävän tiedetään ratkeavan vakioajassa Esimerkkejä: 1) kertoman laskennassa voidaan asettaa perustapaukseksi n = 0, jolloin vastaukseksi palautetaan 1 (algoritmille voidaan asettaa useampiakin perustapauksia, mutta se ei kertoman tapauksessa ole tarpeen, mutta esimerkiksi Fibonaccin lukujen rekursiivisessa laskennassa näin on) 2) (limitys)lajitteluongelma on triviaali, jos lajiteltavia alkioita on korkeintaan yksi: pääohjelmassa testataan ainoastaan, onko syötevektorin vasen raja aidosti pienempi kuin oikea. Ellei ole, ei tehdä mitään. siten voidaan olettaa, että korkeintaan 1 alkion lajittelu vie vakioajan. Tällä kurssilla olemme juuri limityslajittelun kohdalla törmänneet suoritusaikalausekkeeseen, jossa yhtälön oikea puoli ei ole ratkaistussa muodossa, vaan sisältää termiä T(x), missä x < n on jokin osa alkuperäisen syötteen koosta. Limityslajittelun tarkka suoritusaikalauseke olisi muotoa: T(n) = Θ(1), kun n ≤ 1 T(n/2) + T (n/2) Θ(n), kun n > 1 • Kyseessä on rekursioyhtälö, joka on tarpeen ratkaista, jotta algoritmin kokonaissuoritusaika pystyttäisiin lausumaan turvautumatta jonkin kooltaan n:ää pienemmän syötteen ratkaisuaikaan. 4 Rekursioyhtälöistä • • Yleensä tyydytään esittämään rekursioyhtälöstä vain rekursiivisia termejä sisältävä tapaus (edellä se, jossa n > 1), sillä tehtävän voidaan olettaa ratkeavan vakioajassa, kun n on riittävän pieni. Myös katto- ja lattiafunktiot jätetään usein merkitsemättä, sillä niillä ei ole merkitystä asymptoottisen suoritusajan kannalta. siten limityslajittelun suoritusaika kuvataan ratkaisemattomassa muodossa usein seuraavalla tavalla (ilman perustapausta sekä katto- ja lattiafunktioita): T(n) = 2T(n/2) + Θ(n) • • Tällä kurssilla tarkastellaan iterointimenetelmää rekursioyhtälöiden ratkaisemiseksi. Muitakin menetelmiä on toki olemassa (esimerkiksi ratkaisun arvaaminen ja todistaminen oikeaksi). Lähdetään aluksi tarkastelemaan rekursioyhtälöä T(n) = T(n/2) + 1. Kaavan sanomana on, että n:n kokoisen syötteen ratkeaminen vaatii työtä yhden yksikön enemmän kuin puolet lyhyempi syöte. Sovelletaan kaavaa toistuvasti sijoittamalla aina oikean puolen T(n) lausekkeen syötteen koko vasemmalle n:n paikalle. Aluksi saadaan: T(n/2) = T(n/4) + 1, seuraavalla yrityksellä T(n/4) = T(n/8) + 1, sitten T(n/8) = T(n/16) + 1 ja niin edelleen. Joka kerta T(x):n argumentti puolittuu edellisestä. Käytetään tällä tavoin saatuja termejä hyväksi alkuperäisen syötteen työmäärän T(n) ilmaisemiseksi: 4 Rekursioyhtälöistä • T(n) = T(n/2) + 1 /* lausutaan T(n/2) summan T(n/4) + 1 avulla */ = (T(n/4) + 1) + 1 = T(n/4) + 2 /* korvataan nyt puolestaan T(n/4) termillä T(n/8) + 1 … */ = (T(n/8) + 1) + 2 = T(n/8) + 3 /* … ja jatketaan samaan tapaan … */ = (T(n/16) + 1) + 3 = T(n/16) + 4 = T(n/24) + 4 … Nähdään, että yleisesti i. iteraatiolla saadaan muoto = T(n/2i) + i Koska voimme olettaa, että pienillä n:n arvoilla – esimerkiksi syötteen koolla 1 – suoritusaika on vakio, pitää ratkaista, milloin T:n argumenttina oleva n/2i saavuttaa arvon 1, eli montako iteraatiokierrosta pitää tehdä. Ratkaistaan yhtälö n/2i = 1 termin i suhteen: kerrotaan ensin puolittain 2i:llä … ⇔ n = 2i … ja otetaan molemmilta puolilta log2: ⇔ i = log2n Tarvitaan siis i = log2n iteraatiokierrosta, että saavutetaan n:n arvo 1. Sijoitetaan tämä i:n arvo ylempänä saatuun yleiseen muotoon: • T(n) = T(n/2i) + i = T(n/2log2n) + log2n /* 2log2n = n */ = T(1) + log2n = c + log2n, missä c on jokin positiivinen vakio (kuvaa tapauksen n = 1 ratkeamiseen kuluvaa aikaa) Siten T(n) = Θ(log2n), koska logaritmi kasvaa nopeammin kuin vakio (joka on tietysti kiinteä). 4 Rekursioyhtälöistä • Lähdetään seuraavaksi tarkastelemaan limityslajittelun rekursioyhtälön yksinkertaistettua muotoa T(n) = 2T(n/2) + Θ(n) • Muunnetaan algoritmin ajankäytössä limityksen osuutta kuvaava termi Θ(n) muotoon dn, sillä limitykseen menevää aikaa voidaan rajoittaa tulotermillä, joka on n:n ensimmäistä potenssia. T(n) = 2T(n/2) + dn /* lausutaan T(n/2) T(n/4):n avulla sijoittamalla kaavassa n/2 n:n paikalle */ = 2(2T(n/4) + dn/2) + dn = 22T(n/4) + 2dn /* lausutaan nyt puolestaan T(n/4) termin T(n/8) avulla … */ = 22(2T(n/8) + dn/4) + 2dn = 23T(n/8) + 3dn /* … ja jatketaan samaan tapaan … */ = 23(2T(n/16) + dn/8) + 3dn = 24T(n/16) + 4dn … Nähdään, että yleisesti i. iteraatiolla saadaan muoto = 2iT(n/2i) + idn • Koska voimme nytkin olettaa, että pienillä n:n arvoilla – esimerkiksi syötteen koolla 1 – suoritusaika on vakio, pitää ratkaista, milloin T:n argumenttina oleva n/2i saavuttaa arvon 1, eli montako iteraatiokierrosta pitää tehdä. Ratkaistaan yhtälö n/2i = 1 termin i suhteen: kerrotaan ensin puolittain 2i:llä … ⇔ n = 2i … ja otetaan molemmilta puolilta log2: ⇔ i = log2n Tarvitaan tässäkin tapauksessa siis i = log2n iteraatiokierrosta, että saavutetaan n:n arvo 1. Sijoitetaan tämä i:n arvo ylempänä saatuun yleiseen muotoon: • T(n) = 2iT(n/2i) + idn = 2log2n T(n/2log2n) + log2n⋅dn /* 2log2n = n */ = nT(1) + dlog2n = cn + dnlog2n, missä c ja d ovat joitain positiivisia vakioita (d kuvaa limityksen tulotermin kerrointa ja c tapauksen n = 1 ratkeamiseen kuluvaa aikaa) Siten T(n) = Θ(nlog2n), koska nlog2n kasvaa nopeammin kuin pelkkä n (n:stä riippuvaa kerrointa ei saa hävittää, jos se liittyy n:n korkeinta astetta olevaan termiin!). 4 Rekursioyhtälöistä • Seuraavaksi siirrytään tarkastelemaan Hanoin tornien ongelman rekursioyhtälöä, jonka yleinen muoto on: T(n) = 2T(n – 1) + 1 Jotta saataisiin siirrettyä n:stä levystä koostuva torni lähtötolpasta maalitolppaan, pitää ensinnä siirtää n – 1:n kokoinen torni pois tieltä aputolppaan, minkä jälkeen suurin levy saadaan siirrettyä (tästä termi + 1), ja lopulta aputolpassa odottava n – 1:n kokoinen torni tuodaan suurimman levyn päälle maalitolppaan. Perustapaukseksi voidaan valita tyhjän tornin siirtäminen, sillä sitä varten ei tarvitse tehdä yhtään mitään. Valitaan siis T(0) = 0. Lähdetään nyt iteroimaan alkuperäistä rekursioyhtälöä tavoitteena, että n saavuttaa arvon 0: T(n) = 2T(n – 1) + 1 /* Sijoitetaan n:n paikalle n – 1, …*/ = 2(2T(n – 2) + 1) + 1 = 22T(n – 2) + 2 + 1 /* … sitten n – 1 korvataan n – 2:lla … = 22(2T(n – 3) + 1) + 2 + 1 = 23T(n – 3) + 4 + 2 + 1 /* … ja jatketaan T(x):n argumentin pienentämistä 1:llä = 24T(n – 4) + 8 + 4 + 2 + 1= 24T(n – 4) + 23 + 22 + 21 + 20 … Nähdään, että yleisesti i. iteraatiolla rekursioyhtälö saa muodon = 2iT(n – i) + 2i – 1 + 2i – 2 + … + 22 + 21 + 20 = 2iT(n – i) + 4 Rekursioyhtälöistä • Koska T(0) = 0, eli tyhjän tornin siirtäminen on ilmaista. Lisäksi n:n arvo 0 saavutetaan n:n iteraatiokierroksen jälkeen, sillä n–i=0 ⇔ i=n Sijoittamalla arvo i = n edellisen sivun viimeiseen yhtälöön saadaan: T(n) = 2nT(0) + =0+ = Nyt pitää vielä ratkaista summalausekkeen arvo. Tarkastellaan, mitä saataisiin summan Sn = Tehdään tämä muodostamalla kaksinkertainen summa 2Sn ja vähentämällä siitä Sn. 2Sn = 21 + 22 + 23 + … Sn = 20 + 21 + 22 + 23 + … 2Sn – Sn = 20 + 21 + 22 + 23 + … -20 + 0 + 0 + 0 + … arvoksi. + 2n + 2n+1 + 2n + 2n_________ + 0 + 2n+1 Siten Sn = 2n+1 – 1, ja kun summalausekkeen yläraja i korvataan termillä i – 1, saadaan Hanoin tornien työmääräksi äskeisen perusteella T(n) = = 2n – 1 Hanoin tornien ongelman ratkaisuaika on siten eksponentiaalinen. 4 Rekursioyhtälöistä • Otetaan vielä yksi esimerkki: tarkastellaan rekursioyhtälöä T(n) = T( ) + 1 Ratkaistaan aluksi lausekkeen arvo muutamilla seuraavilla, aina puolitetuilla n:n arvoilla (merkitään jatkossa = n½) … T(n) = T(n½) + 1 T(n½) = T(n½)½ + 1 = T(n1/4) + 1 T(n1/4) = T(n1/8) + 1 T(n1/8) = T(n1/16) + 1 … ja iteroidaan tämän jälkeen alkuperäistä yhtälöä: T(n) = T(n½) + 1 = T(n1/4) + 1 + 1 3 = T(n1/8) + 1 + 1 + 1 = T(n1/2 ) … Saadaan lopulta yleinen muoto: i T(n) = T(n1/(2 )) + i 4 Rekursioyhtälöistä • Oletetaan taasen, että pienellä n:n arvolla tehtävä ratkeaa vakioajassa. Sovitaan, että tällainen n:n arvo olisi 2. Tällöin pitää ratkaista yhtälö … i n1/2 = 2 … joten eipä muuta kuin ratkaisemaan: i ⇔ ⇔ ⇔ ⇔ ⇔ n1/2 = 2 i log2n1/2 = log22 i log2n1/2 = 1 1/2i log2n = 1 log2n = 2i i = log2log2n Tehdään lopuksi sijoitus i = log2log2n yhtälön yleiseen muotoon: i T(n) = n1/(2 ) + i log log n = n1/(2 2 2 ) + log2log2n = c + log2log2n • Siten T(n) = Θ(log2log2n) (polylogaritmifunktio) Kyseinen funktio kasvaa hyvin hitaasti. Se saavuttaa esimerkiksi arvon 4 vasta, kun n = 65536. 4 Rekursioyhtälöistä • Rekursioyhtälön muodosta pystyy usein ainakin jossain määrin aavistamaan, mitä kasvunopeusluokkaa lausekkeen ratkaistu muoto edustaa. Kannattaa aluksi pysähtyä hetkeksi miettimään, mitä rekursioyhtälö itse asiassa kertoo algoritmin reagoinnista syötteen koon muutoksiin. Ei siis välttämättä ole aina tarvetta lähteä suoraan iteroimaan rekursioyhtälöä! Esimerkki 1: T(n) = T(n/2) + 1. Syötteen koon kaksinkertaistuminen aiheuttaa työmäärän kasvamisen nykyisestä ainoastaan yhdellä yksiköllä. Tästä voi päätellä, että funktio kasvaa hyvin hitaasti, ja erityisesti lg on juuri halutun kaltainen funktio: sen arvo kasvaa ykkösellä aina kun syötteen koko kaksinkertaistuu. On siis perusteltua aavistaa, että lausekkeen ratkaistu muoto olisi kasvunopeudeltaan logaritminen. Esimerkki 2: T(n) = T(n – 1) + 2. Syötteen koon kasvattaminen yhdellä lisää työmäärää kahdella yksiköllä. Työmäärä kasvaa siten tasaisesti kulloisestakin n:n arvosta riippumatta: muutos n:ssä näkyy aivan vastaavan suuruusluokan muutoksena työmäärässä. Voidaan hyvällä syyllä olettaa ratkaistun muodon kasvunopeuden olevan lineaarinen – suoraan verrannollinen syötteen kokoon n. 4 Rekursioyhtälöistä Esimerkki 3: T(n) = T(n – 1) + n. Syötteen koon kasvaminen yhdellä johtaa n:n suuruiseen työmäärän lisäykseen. Toisin kuin esimerkissä 2, nyt syötteen kasvattamisesta aiheutuva lisätyö ei olekaan enää vakioaikaista vaan riippuu n:n sen hetkisestä arvosta. Vaikuttaisi, että ratkaistu muoto näyttäisi aritmeettiselta sarjalta, kun yhteenlaskettavat ovat väliltä 1..n. Voidaan päätellä suoritusajan olevan neliöllinen. Esimerkki 4: T(n) = 2T(n – 1) + 1. Syötteen koon kasvattaminen yhdellä yksiköllä yli kaksinkertaistaa tätä ennen tarvitun työmäärän. Ongelman täytyy olla vaikea – eksponentiaalinen. • Mikäli rekursioyhtälö on jo ehditty ratkaista iteroimalla, kannattaa aina tarkastaa, onko saatu tulos alkuperäisen rekursioyhtälön muodon kanssa järkeenkäypä. mahdolliset isot laskuvirheet saattavat paljastua! 6 Kekolajittelu • • • • Asymptoottisesti tehokkaimpien, alkioparien välisiin vertailuihin perustuvien yleisten lajittelumenetelmien esittely aloitetaan kekolajittelusta. Keko-tietorakenne ehdittiin lyhyesti esitellä kurssilla ”Johdatus informaatioteknologiaan II”. Keko muistuttaa rakenteensa puolesta binääripuuta, joka on mahdollisesti alinta tasoaan lukuun ottamatta täydellinen. Jokaista keon tasoa täytetään vasemmalta oikealle. On olemassa sekä maksimi- että minimikekoja. Maksimikeon kaikille solmuille pätee seuraava ominaisuus: tarkasteltavan solmun isäsolmu on arvoltaan aina vähintään yhtä suuri kuin solmu itse. maksimikeon suurin alkio löytyy aina sen juuresta Minimikeossa puolestaan kunkin solmun isäsolmun arvo on tarkasteltavan solmun arvoan kanssa korkeintaan yhtä suuri. minimikeon juuresta löytyy sen pienin alkio Maksimikeko 31 17 22 13 5 10 2 7 4 6 6 Kekolajittelu • • • • Keko voidaan tallentaa hyvin vektoriin, joka on indeksoitu välille [1..n]. Funktio pituus[A] ilmaisee taulukon A alkioiden lukumäärän. Attribuutti KeonKoko[A]osoittaa, miten monta alkiota A:sta kuuluu kekoon Kannattaa huomioida, että KeonKoko[A] ≤ pituus[A], eli vektori A ei välttämättä ole tarkasteluhetkellä kokonaan keon käytössä. Paikasta A[1] löytyy keon huipulla oleva alkio (eli kekoa edustavan puun juuri). • Maksimikeko … 8 13 3 5 9 5 2 10 7 6 17 22 13 22 7 4 6 10 … ja sen esitys vektorimuodossa 1 2 3 4 5 31 31 17 2 4 1 10 6 7 8 9 10 4 6 5 2 7 6 Kekolajittelu • Seuraavassa on esiteltyinä tärkeimmät keon solmulle suoritettavat siirtymisoperaatiot: 1) vanhempi(i) palauttaa solmun i isäsolmun indeksin tässä esitettävät algoritmit on toteutettu siten, ettei juuren isäsolmuun milloinkaan viitata, sillä tällaista ei ole olemassa pseudokoodi: VANHEMPI(i) RETURN i/2 2) vasen(i) palauttaa solmun i vasemman lapsisolmun indeksin pseudokoodi: VASEN(i) RETURN 2i • 3) oikea(i) palauttaa solmun i oikean lapsisolmun indeksin pseudokoodi: OIKEA(i) RETURN 2i + 1 Maksimikeolle kaikilla arvoilla i > 1 on voimassa A[VANHEMPI(i)] ≥ A[i] • Vastaavasti minimikeolle on jokaista i > 1 kohti voimassa A[VANHEMPI(i)] ≤ A[i] • Tällä kurssilla tarkastellaan vastedes aina maksimikekoja, mutta kurssilla ”Tietorakenteet ja algoritmit II” käsitellään tarkemmin myös minimikekoja. 6 Kekolajittelu • Määritelmiä: solmun korkeus tarkoittaa pisimmän polun pituutta solmusta lehteen (lehtisolmu = solmu, jonka poikapuut ovat tyhjiä). keon korkeudella tarkoitetaan juuren etäisyyttä kaukaisimmasta lehdestä • Tarkastellaan seuraavassa kekoa, jonka korkeutena on h. Tällöin kekoon mahtuu alkioita vähintään (olettaen, että keon alimmalla tasolla on vain yksi alkio, eli keko on mahdollisimman vajaa) + 1 = 20 + 21 + 22 + … + 2h–1 + 1 = 2h – 1 + 1 = 2h alkiota. • Vastaavasti, jos keko on mahdollisimman täynnä (alinkin taso täyttyneenä), siihen mahtuu = 20 + 21 + 22 + … + 2h–1 + 2h = 2h+1 – 1 alkiota. • Kun keon alkioiden lukumäärä on n, sen korkeus pystytään laskemaan äskeisen analyysin perusteella seuraavasti ratkaisemalla seuraava kaksoisepäyhtälö korkeuden h suhteen: 2h ≤ n ≤ 2h+1 – 1 ⇔ 2h ≤ n < 2h+1 ⇔ h ≤ lg n < h + 1 ⇔ h = lg n Esimerkiksi 20 alkion keko on korkeudeltaan lg 20 = 4 (lg 20 ≈ 4.32), ja siinä on 5 tasoa. 6.2 Keon ylläpito • • Seuraavaksi esitettävällä algoritmilla KORJAA_MAKSIMIKEKO voidaan korjata yhden solmun kohdalta mahdollisesti rikkoutunut maksimikeko uudelleen kuntoon. Algoritmissa oletetaan, että solmun A[i] molemmat alipuut ovat kekoja, mutta sen sijaan A[i]:n itsensä kohdalla keko-ominaisuus ei välttämättä ole voimassa vaan on saattanut tilapäisesti rikkoutua. KORJAA_MAKSIMIKEKO(A, i) 1 v := vasen(i) 2 o := oikea(i) 3 IF v ≤ KeonKoko[A] AND A[v] > A[i] 4 THEN suurin := v 5 ELSE suurin := i 6 IF o ≤ KeonKoko[A] AND A[o] > A[suurin] 7 THEN suurin := o 8 IF suurin ≠ i 9 THEN vaihda A[i] <---> A[suurin] 10 KORJAA_MAKSIMIKEKO(A, suurin) • Algoritmin ajatuksena on siirtää indeksiin i päätynyt liian pieni avain keossa alaspäin oikealle paikalleen. 6.2 Keon ylläpito 1 17 13 5 22 10 2 4 6 7 Juurisolmun (i = 1) ja sittemmin sen oikean pojan (i = 3) kohdalta rikkoutunut keko korjautuu 22 17 13 5 6 10 2 7 4 1 6.2 Keon ylläpito • Analysoidaan algoritmin KORJAA_MAKSIMIKEKO(A, i) aikakompleksisuus: Kaikki rivit viimeistä lukuun ottamatta vaativat vakiomäärän suoritusaikaa, sillä niissä tehdään ainoastaan asetuksia ja vertailuja, joiden kesto ei riipu syötteen koosta n. Viimeinen eli 10. rivi sisältää rekursiivisen kutsun, jollainen voi pahimmassa tapauksessa kerran jokaista keon tasoa kohti. Keossa on tasoja h + 1 eli yksi enemmän kuin keolla on korkeutta. Koska h = log2n, niin algoritmin suoritusajaksi saadaan T(n) = log2n. 6.3 Keon rakentaminen • • • Keko saadaan perustettua vektorista A[1..n] järjestämällä se kekojärjestykseen. Kekojärjestys saadaan toteutumaan kutsumalla toistuvasti edellä esitettyä keon korjausalgoritmia KORJAA_MAKSIMIKEKO. Keon rakentamismenettely perustuu havaintoon, että kaikki keon lehtisolmut toteuttavat triviaalisti keko-ominaisuuden (ne ovat yhden alkion kekoja: molemmat alipuut tyhjiä), joten niitä ei tarvitse mennä korjaamaan. Kyseiset alkiot sijaitsevat vektorin A paikoissa n/2 + 1, n/2 + 2, …, n – 1, n 6.3 Keon rakentaminen • • • • Maksimikeon muodostava algoritmi on esitettynä seuraavassa: MUODOSTA_MAKSIMIKEKO(A) 1 KeonKoko[A] := pituus[A] 2 FOR i := pituus[A]/2, pituus[A]/2 - 1, …, 2, 1 DO 3 KORJAA_MAKSIMIKEKO(A, i) Tehdään algoritmille ensi alkuun yksinkertainen analyysi: rivin 1 suorituskustannus on vakioaikainen rivin 2 silmukassa tehdään kiinteät n/2 kierrosta, ja joka kerta siinä ainoana käskynä kutsutaan rivillä 3 proseduuria KORJAA_MAKSIMIKEKO, jonka suoritusaika todettiin jo edellä logaritmiseksi. Analyysin perusteella maksimikeon rakennuskustannus tuntuisi siten olevan suuruusluokkaa T(n) = O(nlog2n). Perusteellisempi, seuraavassa esitettävä analyysi osoittaa kuitenkin, että keko saadaan muodostettua lineaarisessa ajassa, eli T(n) = O(n). Kyseinen tulos perustuu lauseeseen, jonka mukaan keon solmujen korkeuksien summa on korkeintaan 2h+1 – h – 2. Todistetaan lause seuraavaksi. Oletetaan, että tarkastellaan täyttä kekoa, jonka korkeutena on h. Tällöin keossa on 1 = 20 solmua, joiden korkeutena on h (yksinomaan juurisolmu on korkeudella h) 2 = 21 solmua, joiden korkeutena on h – 1 (juuren molemmat lapset) 4 = 22 solmua, joiden korkeutena on h – 2 (juuren lapsisolmujen lapset) … 2i solmua sijaitsee korkeudella h – i kaikille i = 0, 1, …, h 2h solmua sijaitsee korkeudella 0 (alimman tason solmut) 6.3 Keon rakentaminen • Solmujen korkeuksien summaksi saadaan nyt (h – j) = h + 2(h – 1) + 4(h – 2) + … + 2h-1(h – (h – 1)) =1 Muodostetaan laskennan helpottamiseksi kaksinkertainen summa … S= • 2S = 2h + 4(h – 1) + 8(h – 2) + … + 2h • • • …, josta sittemmin vähennetään alkuperäinen summa: 2S – S = (0 – h) + (2h – (2h – 2)) + (4h – 4 – (4h – 8)) + … + 2h-1(h – (h – 2)) + 2h(h – (h – 1)) = -h + 2 + 4 + 8 + … + 2h-1 + 2h /* Nyt lisätään ja vähennetään ykkönen. */ = -h + (1 + 2 + 4 + 8 + … + 2h) – 1 = -h + (20 + 21 + 22 + 23 + … + 2h) – 1 = -h + (2h+1 – 1) – 1 = 2h+1 – h – 2 Edellä oletettiin, että keko olisi täysi. Jos puolestaan keko on vajaa, jää sen solmujen korkeuksien summa edellä esitettyä pienemmäksi. Aikaisemmin ratkaistiin, että n-alkioisen keon juuren korkeus h = log2n, joka on ≤ log2n. Sijoittamalla tämä tulos edelliseen korkeuksien summaan todetaan, että keossa, jossa on yhteensä n alkiota, solmujen korkeuksien summa on enintään S = 2h+1 – h – 2 < 2h+1 ≤ 2log2n+1 = 2 ∙ 2log2n = 2n. • Koska solmujen korkeuksien summa on pienempi kuin 2n, voidaan todeta, että keon rakentaminen tosiaankin onnistuu lineaarisessa ajassa alkioiden määrän suhteen. 6.4 Kekolajittelualgoritmi • • • Lopuksi esitetään vielä varsinainen kekolajittelualgoritmi, jossa aluksi perustetaan keko, ja sen valmistuttua siitä irrotetaan aina suurin alkio, joka viedään vektorin loppuosaan oikealle paikalleen. Tilalle juureen viedään tilapäisesti keon viimeinen alkio. Aina, kun juuresta poistetaan suurin alkio, keko korjataan jälleen kelvolliseksi käyttämällä aiemmin esiteltyä korjausalgoritmia. Algoritmi toimii minimitilassa, eli se vaatii toimiakseen ainoastaan vakiomäärän muistia syötevektorin lisäksi. KEKOLAJITTELU(A) 1 MUODOSTA_MAKSIMIKEKO(A) 2 FOR i := pituus[A], pituus[A] – 1, …, 3, 2 DO 3 vaihda A[1] <---> A[i] 4 KeonKoko[A] := KeonKoko[A] – 1 5 KORJAA_MAKSIMIKEKO(A, 1) • Algoritmin kompleksisuus: Keon muodostaminen rivillä 1: Ο(n) Riviltä 2 alkavaa silmukkaa suoritetaan n – 1 kertaa, jonka sisällä: Rivit 3-4 ovat vakioaikaisia Rivin 5 korjausalgoritmi vaatii Ο(log2n) suuruisen työn • • Siten T(n) = Ο(n) + (n – 1) ∙ Ο(log2n) = Ο(n) + Ο(n) ∙ Ο(log2n) = Ο(nlog2n) Luennolla esitetään esimerkki kekolajittelun toiminnasta yksittäiselle syötevektorille. 6.5 Prioriteettijonot • • • • • Töidenjärjestelyongelmien yhteydessä on tarpeellista pitää kirjaa siitä, mikä työ otetaan odottamassa olevien töiden jonosta ensimmäisenä käsittelyyn. odottavilla töillä on jokin keskinäinen prioriteetti, joka vaikuttaa työn käsittelyyn ottamisen ajankohtaan Kun yksi työ saadaan valmiiksi, valitaan seuraavaksi käsittelyyn se odottamassa oleva työ, jolla on korkein prioriteetti. tähän tarkoitukseen sopii erinomaisesti maksimikeko tietorakenteeksi Prioriteettijonosta on selvästikin pystyttävä ottamaan korkeimman prioriteetin mukainen työ pois; samoin sinne pitää pystyä lisäämään uusia töitä. Lisäksi saattaa olla tarpeen muuttaa jonkin jonossa olevan työn prioriteettia. Seuraavaksi esitellään operaatiot: 1) LISÄÄ(S, x) 2) MAKSIMI(S) 3) POISTA_MAKSIMI(S) 4) KASVATA_ARVOA(S, x, k) Kyseisissä algoritmeissa parametri S edustaa tarkasteltavaa prioriteettijonoa, x sen solmun indeksiarvoa ja k kyseiseen solmuun asetettavaa uutta avainarvoa. 6.5 Prioriteettijonot • Aloitetaan maksimin palauttavasta algoritmista: MAKSIMI(A) 1 IF KeonKoko[A] < 1 2 THEN virheilmoitus ”Keko on tyhjä.” 3 ELSE RETURN A[1] • Kyseessä on vakioaikainen algoritmi, eli syötteen koko ei mitenkään vaikuta sen suoritusaikaan: T(n) = Θ(1). Seuraavaksi esitellään maksimiarvon keosta poistava algoritmi: • POISTA_MAKSIMI(A) 1 IF KeonKoko[A] < 1 2 THEN virheilmoitus ”Keko on tyhjä.” 3 ELSE 4 max := A[1] 5 A[1] := A[KeonKoko[A]] /* Siirtää keon viimeisen alkion juureen */ 6 KeonKoko[A] := KeonKoko[A] – 1 7 KORJAA_MAKSIMIKEKO(A, 1) 8 RETURN max Algoritmin suoritusaika on sama kuin keon korjausalgoritmin eli Ο(log2n). 6.5 Prioriteettijonot • Seuraavaksi avainarvon (prioriteetin) kasvattaminen: KASVATA_ARVOA(A, i, avain) 1 IF avain < A[i] 2 THEN virheilmoitus ”Uusi avainarvo ei saa olla aikaisempaa pienempi.” 3 ELSE 4 A[i] := avain 5 WHILE i > 1 AND A[VANHEMPI(i)] < A[i] DO 6 vaihda A[i] <---> A[VANHEMPI(i)] 7 i := VANHEMPI(i) Kannattaa huomioida, että solmusta i alaspäin keko-ominaisuus säilyy, sillä aikaisempi avainarvo ei pienene. Algoritmin kompleksisuus on Ο(log2n): pahimmassa tapauksessa kuljetaan keon pisin polku ylöspäin lehdestä juureen. • Lopuksi esitellään vielä alkion lisääminen kekona toteutettuun prioriteettijonoon: LISÄÄ(A, avain) 1 KeonKoko[A] := KeonKoko[A] + 1 2 A[KeonKoko[A]] := -∝ 3 KASVATA_ARVOA(A, KeonKoko[A], avain) • Lisäysalgoritmin suoritusaika on sama kuin avainarvon kasvatusalgoritmin eli Ο(log2n). Tarkastellaan viimeksi esitellyistä algoritmeista esimerkkejä luennolla. 7 Pikalajittelu 7.1 Algoritmi • • • • Pikalajittelu on esiteltiin jo 50 vuotta sitten vuonna 1961. Sen kehittäjä on C.A.R. Hoare. Samoin kuin limityslajittelu, niin myös pikalajittelu käyttää hajota ja hallitse -tekniikkaa. Pikalajittelun pahin tapaus on Asymptoottisesti suuruusluokkaa Θ(n2), mutta keskimäärin menetelmä toimii ajassa Θ(n log2n). pahin tapaus esiintyy erittäin harvoin (silloin, kun syötevektori on jo valmiiksi lajiteltuna, tai syötevektorin ositus tapahtuu jatkuvasti mahdollisimman epäedullisesti) keskimääräisen tapauksen vakiokerroin pieni, joten menetelmä toimii käytännössä sekä limitys- että kekolajittelua nopeampi Algoritmin toimintaperiaatteena on syötevektorin osittaminen eli ns. partitiointi kahteen alueeseen yhden määrätyn alkion suhteen. Kyseistä alkiota kutsutaan pivot-alkioksi. Hajota- ja hallitse -tekniikan soveltaminen pikalajitteluun • • • • Hajottaminen: Partitioidaan vektori A[p..r] kahdeksi osavektoriksi A[p..q-1] ja A[q+1..r] siten, että osassa A[p..q-1] esiintyy ainoastaan pienempiä ja yhtä suuria alkioita kuin A[q], ja vastaavasti osan A[q+1..r] kaikki alkiot ovat suurempia tai yhtä suuria kuin A[q]. Hallitseminen: Lajitellaan partitioinnin synnyttämät kaksi osavektoria rekursiivisesti, kunnes osavektori puristuu korkeintaan yhden mittaiseksi. Yhdistäminen: Ei tarvitse tehdä yhtään mitään: kun kaikki partitioinnit on saatettu loppuun asti, vektori on samalla saatu jo lajiteltua! Ilmeinen ero: limityslajittelussa osittaminen on halpaa mutta ratkaisujen yhdistäminen vaatii työtä, kun taas pikalajittelussa kaikki työ tarvitaan ositukseen: yhdistäminen saadaan ”kaupan päälle”. 7.1 Algoritmi • Pikalajittelualgoritmi on esiteltynä seuraavanlainen: PIKALAJITTELU(A, p, r) 1 IF p < r 2 THEN q := PARTITIOI(A, p, r) 3 PIKALAJITTELU(A, p, q – 1) 4 PIKALAJITTELU(A, q+1, r) Ylimmän tason kutsu on muotoa PIKALAJITTELU(A, 1, pituus[A]) Algoritmi suoritetaan, mikäli tarkasteltava osavektori on vähintään kahden mittainen. Suoritus koostuu funktion PARTITIOI kutsusta, joka määrää seuraavan alkion, joka määrää osavektorien toisen indeksirajan rivien 3 ja 4 rekursiivisille kutsuille. • Toiminta-ajatus: jokainen rekursiotaso vie rivin 2 suorituksen jälkeen määräytyvän pivotalkion vektorissa oikealle paikalleen. Lisäksi tulee voimaan ominaisuus, että kyseisen alkion eli A[q]:n vasemmalle puolelle sijoitetaan pelkästään tämän kanssa korkeintaan yhtä suuria alkioita, ja oikealle puolelle pelkästään A[q]:n kanssa suurempia tai yhtä suuria alkioita. Seuraavaksi esitellään partitioinnin suorittava algoritmi. 7.1 Algoritmi PARTITIOI(A, p, r) 1 x := A[r] /* Algoritmi tekee osavektorin viimeisestä alkiosta pivot-alkion */ 2 i := p – 1 3 FOR j := p, p + 1, …, r – 1 DO 4 IF A[j] ≤ x THEN 5 i := i + 1 6 vaihda A[i] <--->A[j] 7 vaihda A[i+1] <---> A[r] 8 RETURN i + 1 Partitiointialgoritmin aikakompleksisuus on Θ(n), sillä se sisältää vain vakioaikaisia asetusehto- ja palautuslauseita, sekä silmukan, jossa tehdään enintään n – 1 kierrosta. • Esitellään luennolla partitiointialgoritmin toiminta (ja samalla pikalajittelun alkuvaiheet) syötteelle A = 19, 11, 37, 8, 62, 3, 9, 28, 26, 30 (sama syöte kuin 1. demonstraatioiden tehtävässä 4) 7.2 Algoritmin analyysi • • Aloitetaan pikalajittelun analyysi tarkastelemalla pahinta tapausta. Pahimmassa tapauksessa käy aina niin huono tuuri, että pivot-alkioksi päätyy joka kerta tarkasteltavan osavektorin joko suurin tai pienin alkio Tällöin pivot-alkion toispuoleinen osite jää tyhjäksi, ja toiselle puolelle päätyvät kaikki muut ositukseen osallistuvat alkiot (paitsi pivot-alkio itse). Saadaan seuraavanlainen rekursioyhtälö: T(n) = T(n – 1) + T(0) + Θ(n) /* pitkän ositteen lajittelu + tyhjän ositteen lajittelu + ositus */ = T(n – 1) + Θ(n) /* olettaen karkeasti, että T(0) = 0 */ • Tämä voidaan siten lausua muodossa T(n) = T(n – 1) + cn Lähdetään nyt iteroimaan edellä olevaa rekursioyhtälöä. Saadaan: T(n) = T(n – 1) + cn = T(n – 2) + c(n – 1) + cn = T(n – 3) + c(n – 2) + c(n – 1) + cn … = T(0) + c + 2c + 3c + … + c(n – 1) + cn = 0 + c(1 + 2 + 3 + … + n) = cn(n + 1)/2 = (c/2)n2 + (c/2)n Pahimmassa tapauksessa pikalajittelu vaatii siten neliöllisen suoritusajan, eli T(n) = Θ(n2). 7.2 Algoritmin analyysi • • Tarkastellaan sitten parasta tapausta. Parhaassa tapauksessa pivot-alkio jakaa ositettavan osavektorin mahdollisimman keskeltä Tällöin pivot-alkion toiselle puolelle päätyy n/2 alkiota ja toiselle puolelle n/2 – 1 alkiota. Kumpaakin lauseketta voidaan arvioida ylöspäin arvoon n/2, niin päästään eroon lattia- ja kattofunktioiden merkinnöistä Saadaan siten seuraavanlainen rekursioepäyhtälö: T(n) ≤ 2T(n/2) + Θ(n) /* kummankin likimain yhtä pitkän ositteen ratkaiseminen + ositus */ • Tämä voidaan lausua nyt muodossa T(n) ≤ 2T(n/2) + dn Muistuttaa hyvin paljon limityslajittelun rekursiivista kustannusta, mutta nyt on kyseessä epäyhtälö yhtälön sijaan Lähdetään nyt iteroimaan edellä olevaa rekursioepäyhtälöä. Saadaan: • T(n) ≤ 2T(n/2) + dn ≤ 2(2T(n/4) + dn/2) + dn = 22T(n/4) + 2dn ≤ 22(2T(n/8) + dn/4) + 2dn = 23T(n/8) + 3dn ≤ 23(2T(n/16) + dn/8) + 3dn = 24T(n/24) + 4dn ≤ 25T(n/25) + 5dn … ≤ 2iT(n/2i) + idn Mikäli oletetaan, että T(1) = c (siis vakio), ratkaistaan, milloin T:n argumentti n/2i saa arvon 1. 7.2 Algoritmin analyysi Ratkaistaan yhtälö n/2i = 1 ⇔ n = 2i ⇔ i = log2n … … ja sijoitetaan saatu i:n arvo edellisen sivun epäyhtälön yleiseen muotoon: T(n) ≤ 2iT(n/2i) + idn /* Sijoitus: i = log2n */ = 2log2nT(n/2log2n) + log2ndn /* Laskusääntö: 2log2n = n */ = nc + log2n ⋅ dn ≤ cnlog2n + dnlog2n /* Voimassa, kun n ≥ 2 */ = (c + d)nlog2n /* Termit c ja d ovat vakioita. */ Parhaassa tapauksessa pikalajittelu vaatii siten suoritusajan, eli T(n) = Ο(nlog2n). Partitiointialkion valinnasta: • Edellä esitetyssä pikalajittelun versiossa pivot-alkioksi valittiin joka kerta tarkasteltavan osavektorin viimeinen alkio. Toimii ihan kelvollisesti, mikäli alkioiden järjestys vektorissa on täysin satunnainen ja duplikaatit ovat harvinaisia. Edellä kuitenkin jo todettiin, että vektorin ollessa jo alun perin lajiteltuna suoritusajaksi tulee neliöllinen, sillä edellä esitetyllä tekniikalla osavektorin suurimmasta alkiosta tulee joka kerta väkisin pivot-alkio: partiointi jättää aina toisen osavektoreista tyhjäksi. Samoin kävisi, jos ensimmäinen alkio päätyisi pivot-alkioksi. Pulma: miten estetään huonojen ositusten syntyminen? 7.3 Pikalajittelun satunnaistettu versio • • • • • • Pikalajittelun satunnaistetun version tarkoituksena on yrittää päästä eroon toistuvista huonoista vektorin osituksista. Seuraavassa on tarkoitus osoittaa, että samoin kuin parhaassa, niin myös keskimääräisessä tapauksessa pikalajittelu toimii ajassa Ο(nlog2n). Satunnaistetuille algoritmeille on ominaista, että niiden suoritusaikaan vaikuttaa paitsi syötteen koko, niin myös satunnaislukugeneraattorin tuottamat arvot. Seuraavassa oletetaan, että funktio SATUNNAISLUKU(p, r) palauttaa mielivaltaisen kokonaisluvun väliltä p..r. Edelleen oletetaan, että funktion tulosteet noudattavat tasaista todennäköisyysjakaumaa, eli funktio palauttaa minkä tahansa luvun annetulta alueelta todennäköisyydellä 1/(r – p + 1). Esimerkiksi SATUNNAISLUKU(5, 14) palauttaa minkä tahansa luvun väliltä 5..14 todennäköisyydellä 1/10. Lisäksi tehdään satunnaistetun pikalajittelun analyysiä varten oletus, että syötteenä saatavan lajiteltavan vektorin A[1..n] kaikilla mahdollisilla permutaatioilla on keskenään sama esiintymistodennäköisyys. Esitellään ensiksi satunnaistettua partitiointia … SATUNNAISTETTU_PARTITIOINTI(A, p, r) 1 i := SATUNNAISLUKU(p, r) 2 vaihda A[r] <---> A[i] 3 RETURN PARTITIOI(A, p, r) 7.3 Pikalajittelun satunnaistettu versio • • • • … ja sitten satunnaistettua pikalajittelua suorittava algoritmi: SATUNNAISTETTU_PIKALAJITTELU(A, p, r) 1 IF p < r 2 THEN q := SATUNNAISTETTU_PARTITIOINTI(A, p, r) 3 SATUNNAISTETTU_PIKALAJITTELU(A, p, q – 1) 4 SATUNNAISTETTU_PIKALAJITTELU(A, q + 1, r) Analysoidaan seuraavaksi satunnaistetun pikalajittelun vaatima suoritusaika. Koska satunnaislukugeneraattori tuottaa luvun väliltä 1..n todennäköisyydellä 1/n, on syötteen minkä tahansa alkion todennäköisyys päätyä pivot-alkioksi 1/n. Olettaen, että pivot-alkio sijoittuu syötevektorissa paikkaan q, saadaan suoritusajan rekursioyhtälöksi T(n) = T(q – 1) + T(n – q) + Θ(n) • Yhtälön oikealla puolella ensimmäinen termi kuvaa pivot-alkion vasemmalle puolelle jäävien alkioiden lajitteluaikaa, toinen termi pivot-alkion oikeanpuoleisen osavektorin lajitteluaikaa sekä viimeinen termi partitioinnin kustannusta. Lasketaan seuraavaksi suoritusaikojen keskiarvo, kun pivot-alkion lopullisen sijainnin annetaan vaihdella vapaasti: 7.3 Pikalajittelun satunnaistettu versio T(n) = (1/n) (T(q – 1) + T(n – q)) + Θ(n) = (1/n) ((T(0) + T(1) + … + T(n – 2) + T(n – 1)) + (T(n – 1) + T(n – 2) + … + T(1) + T(0))) + Θ(n) = (2/n) T(q) + Θ(n) Siten saadaan (1): T(n) = (2/n) T(q) + cn Kerrotaan edellinen yhtälö puolittain syötteen koolla n, niin päästään eroon murtoluvusta: (2): nT(n) = 2 T(q) + cn2 Sijoitetaan näin saatuun yhtälöön arvo n – 1 termin n paikalle: (3): (n – 1)T(n – 1) = 2 T(q) + c(n – 1)2 Vähennetään yhtälöstä (2) yhtälö (3): nT(n) – (n – 1)T(n – 1) = 2T(n – 1) + 2cn – c ⇔ (4): nT(n) = (n + 1)T(n – 1) + 2cn – c /* Jaetaan puolittain n:llä */ ⇔ (5): T(n) = ((n + 1)/n)T(n – 1) + 2c – c/n 7.3 Pikalajittelun satunnaistettu versio Kun n kasvaa rajatta, tulee vähentävästä termistä c/n vähitellen merkityksetön, joten se voidaan jättää jatkossa pois yksinkertaisuuden vuoksi: Jaetaan seuraavaksi yhtälö (5) luvulla n + 1, jolloin saadaan: = + Tästä yhtälöstä saadaan edelleen iteroimalla: = + = + = … = + + Lausuttaessa T(n):n arvo T(1):n avulla saadaan: = + 2c 7.3 Pikalajittelun satunnaistettu versio Lukua Hn = = 1 + ½ + 1/3 + … + 1/n kutsuntaan n. harmoniseksi luvuksi. dx = 1 + ln n. Tiedetään, että suurilla luvuilla Hn ≈ ln n + γ, missä γ ≈ 0.57721566 Hn ≤ 1 + on niin sanottu Eulerin vakio. Täten Hn = Θ(n). Tämän tarkastelun perusteella: = + 2c = + Θ(ln n) Tästä seuraa edelleen, että T(n) = Θ(n ln n) = Θ(nlog2n) 8 Lajittelu lineaarisessa ajassa 8.1 Alkioiden välisiin vertailuihin perustuvasta lajittelusta • • • Kaikki tällä kurssilla tähän mennessä käsitellyt lajittelualgoritmit ovat perustuneet alkioparien välisiin vertailuihin, minkä perusteella niiden järjestys on määräytynyt. Tarkastellaan seuraavassa, miten lisäyslajittelun suorittama päätöksenteko etenee, kun sille annetaan lajiteltavaksi 3-alkioinen vektori. Seuraavassa vielä lisäyslajittelualgoritmin pseudokoodi muistin virkistämiseksi: LISÄYSLAJITTELU(A) 1 FOR j := 2, 3, …, pituus[A] DO 2 alkio := A[j] 3 i := j – 1 4 WHILE i > 0 AND A[j] > alkio DO 5 A[i + 1] := A[i] 6 i := i – 1 7 A[i + 1] := alkio • Kiinnitetään seuraavassa huomio ainoastaan algoritmin tekemiin vertailuihin. Muodostuu seuraavalla sivulla esitetyn kaltainen päätöspuu, jonka sisäsolmuissa tapahtuu vertailuun perustuva haarautuminen, ja mahdolliset syöttöjärjestykset sijaitsevat lehtisolmuissa. 8.1 Alkioiden välisiin vertailuihin perustuvasta lajittelusta • Lisäyslajittelun päätöspuu, kun syötteen koko on kolme: a1 ≤ a2 KYLLÄ EI a1 ≤ a3 a2 ≤ a3 KYLLÄ EI a1 ≤ a3 a1, a2, a3 KYLLÄ a1, a3, a2 • • KYLLÄ EI a2 ≤ a3 a2, a1, a3 EI a3, a1, a2 KYLLÄ a2, a3, a1 EI a3, a2, a1 Lehtinä (vihreät solmut) esiintyvät syötteen kaikki mahdolliset 6 permutaatiota. Kutakin syöttövaiheen suuruusjärjestystä vastaa yksikäsitteinen polku juuresta lehteen. 8.1 Alkioiden välisiin vertailuihin perustuvasta lajittelusta • • • Eri lajittelumenetelmillä on erilainen päätöspuu. Olipa kuitenkin kyseessä mikä tahansa pareittaisiin vertailuihin perustuva lajittelumenetelmä, syötteen kaikkien mahdollisten permutaatioiden pitää esiintyä päätöspuun lehtinä tämä vaatimus on ilmeinen, sillä lajittelumenetelmän tulee ratkaista ongelma kaikille mahdollisille laillisille syötteille! Mikäli syötteen koko on n, sen alkioista voi muodostaa n! kappaletta permutaatioita. Esimerkki: n = 10, ja syöte sisältää luvut 1 – 10 jossain järjestyksessä. • • • Ensimmäinen luku voidaan valita vapaasti väliltä 1 – 10. Toiseksi voidaan valita jäljelle jääneistä yhdeksästä mikä tahansa. … Yhdeksännen luvun valitsemiselle on enää kaksi vaihtoehtoa. Viimeiseksi on pakko valita se ainoa, jota ei ole vielä käytetty. mahdollisia syöttöjärjestyksiä yhteensä 10 ⋅ 9 ⋅ 8 ⋅ 7 ⋅ 6 ⋅ 5 ⋅ 4 ⋅ 3 ⋅ 2 ⋅ 1 = 10! (eli 3 628 800) kappaletta! Jo aikaisemmin olemme tämän kurssin aikana ehtineet todeta, että binääripuuhun, jonka korkeutena on h, mahtuu korkeintaan 2h lehteä. Tätä tietoa ja syötteen permutaatioiden lukumäärää hyväksi käyttäen pystytään laskemaan minkä tahansa lajittelumenetelmän päätöspuun minimaalinen korkeus h: Pitää olla voimassa n! ≤ 2h ⇔ h ≥ log2n! Seuraavaksi todistettava lause antaa teoreettisen optimaalisen aikakompleksisuuden alkioparien vertailuihin perustuville lajittelumenetelmille. Parempaan aikavaativuusluokkaan ei ole mahdollista päästä! 8.1 Alkioiden välisiin vertailuihin perustuvasta lajittelusta • Lause: Mikä tahansa alkioiden vertailuun perustuva lajittelumenetelmä vaatii vähintään ⅟₄ nlog2n vertailua pahimmassa tapauksessa. Todistus: Syötteen kaikkien mahdollisten permutaatioiden pitää olla päätöspuun lehtinä edellisen tarkastelun perusteella, joten päätöspuun korkeus h on tällöin vähintään log2(n!). Arvioidaan seuraavaksi n!:n suuruusluokkaa: n! = n(n – 1)(n – 2) ⋅ … ⋅ 3 ⋅ 2 ⋅ 1 Kertoman muodostavan tulon tekijät voidaan ryhmitellä seuraavasti: 1) Suurimpiin n/2 :een, eli tekijöihin n, n – 1, n – 2, …, n/2 + 1 2) Pienimpiin n/2 :een, eli tekijöihin n/2, n/2 - 1, …, 2, 1 Selvästikin pitää paikkansa, että n! ≥ n/2 n/2 ⋅ … ⋅ n/2, sillä jokainen ryhmään 1 kuuluvista termeistä on vähintään n/2:n suuruinen n! ≥ n/2n/2 ≥ (n/2)(n/2) 8.1 Alkioiden välisiin vertailuihin perustuvasta lajittelusta • • • Siten voidaan päätellä: h ≥ log2(n!) ≥ log2(n/2)(n/2) = ½nlog2(n/2) = ½nlog2n – (n/2) log22 = ½nlog2n – (n/2) ≥ ½nlog2n – ⅟₄nlog2n /* Voimassa ehdolla, että n ≥ 4 */ = ⅟₄nlog2n Tästä tuloksesta seuraa, että kooltaan n olevan syötteen lajittelemiseen tarvitaan vähintään ⅟₄nlog2n alkioparivertailua, eli jos halutaan päästä tätä nopeampaan asymptoottiseen suoritusaikaan, on käytettävä muuta lähestymistapaa kuin alkioparien välisiä vertailuja tekevään lajittelualgoritmiin. Seuraavaksi siirrytään tarkastelemaan näitä toisenlaista lähestymistapaa hyödyntäviin lajittelualgoritmeihin ja osoitetaan, että lajittelu lineaarisessa ajassa on tietyin reunaehdoin mahdollista. Nämä menetelmät eivät siis perustu alkioparien väliseen vertailuun. päästään siis asymptoottisesti lyhyempään suoritusaikaan kuin Θ(nlog2n), johon pystyvät mm. limitys- ja kekolajittelu sekä pikalajittelu keskimääräisessä tapauksessa MUTTA: syötteiden pitää täyttää tietyt kriteerit, jotta olisi mahdollista päästä tehokkaampaan suoritusaikaan. 8.2 Laskentalajittelu • • • • • Aloitetaan lineaarisessa ajassa toimivien lajittelumenetelmien esittely laskentalajittelusta. Laskentalajittelun taustaoletuksena on, että syötevektorin A[1..n] sisältönä on kokonaislukuja, joiden vaihteluväli on 0..k. Mikäli nyt k = Ο(n), toimii algoritmi ajassa Θ(n). algoritmi toimii tehokkaasti, jos syötteen lukualue on verrattain kapea Algoritmin toiminta-ajatuksena on laskea eri alkioiden esiintymiskerrat syötevektorissa. Jokaista alkiota x kohti lasketaan, montako x:ää pienempää alkiota syötteessä on. Esimerkki: Jos käy ilmi, että vektorin A alkioista 21 on pienempiä kuin x, pitää x:n ensimmäinen esiintymä sijoittaa vektorissa paikkaan 22. Seuraavassa on esitettynä laskentalajittelualgoritmi: LASKENTALAJITTELU(A, B, k) /* Vektoria B käytetään tulosvektorina. */ 1 FOR i := 0, 1, …, k DO 2 C[i] := 0 /* Alustetaan frekvenssivektorin jokainen positio nollalla. */ 3 FOR j := 1, 2, …, pituus[A] DO 4 C[A[j]] := C[A[j]] + 1 /* Kasvatetaan syötevektorista löydetyn merkin frekvenssiä. */ 5 FOR i := 1, 2, …, k DO 6 C[i] := C[i] + C[i – 1] /* Kumuloidaan frekvenssit. */ 7 FOR j := pituus[A], pituus[A] – 1, …, 1 DO /* Viedään A[j]:n esiintymät oikealta … */ 8 B[C[A[j]]] := A[j] /* … vasemmalle tulosvektoriin B. */ 9 C[A[j]] := C[A[j]] – 1 8.2 Laskentalajittelu • Tarkastellaan luennolla laskentalajittelun toimintaa syötteelle A = < 6, 2, 11, 4, 3, 1, 6, 3, 0, 8, 10, 10, 5, 1, 1, 9, 13, 6, 1, 1, 9, 4, 5, 14, 2 >. • olettaen, että syötettävien lukujen sallittu vaihteluväli on 0..14. Seuraavaksi suoritetaan lisäyslajittelun analyysi: Rivejä 1 – 2 suoritetaan Ο(k) kertaa. Jokaisen syötteessä mahdollisesti esiintyvän merkin frekvenssi pitää nollata. Rivejä 3 – 4 suoritetaan Ο(n) kertaa. Tutkitaan syötteen merkit läpi yksi kerrallaan ja kirjataan niiden esiintymiskerrat. Rivejä 5 – 6 suoritetaan Ο(k) kertaa. Frekvenssivektori rullataan kertaalleen läpi. Rivejä 7 – 9 suoritetaan Ο(n) kertaa. Syötevektori käsitellään kertaalleen lopusta alkuun päin. Kokonaissuoritusajaksi määräytyy siten T(n) ≤ c1k + c2n + c3k + c4n = (c1 + c3)k + (c2 + c4)n = d1k + d2n /* Tässä d1 = c1 + c3 ja d2 = c2 + c4) ≤ ck + cn = c(k + n) Edellisessä c = max{d1, d2}. ⇒ T(n) = Ο(n + k). 8.2 Laskentalajittelu • • • • Kannattaa huomioida, että laskentalajittelu ei toimi minimitilassa, sillä sen tarvitseman työmuistin määrä riippuu syötevektorin A sekä hyväksytyn lukualueen pituudesta k. Lajittelumenetelmän sanotaan olevan stabiili, mikäli se säilyttää syötteen sisältämät duplikaatit alkuperäisessä järjestyksessään. Laskentalajittelu on stabiili lajittelumenetelmä, kun taas esimerkiksi kekolajittelu ei ole stabiili. Stabilisuus on toisinaan tarpeen – esimerkiksi haluttaessa lajitella kahdesti peräkkäin eri kriteerien mukaan hävittämättä ensimmäisen kriteerin mukaisen järjestyksen mukaisia välituloksia. Esimerkki: lajitellaan seuraavassa kerätyt henkilötiedot ensiksi sukunimen ja sittemmin stabiilisti syntymävuoden mukaan. Vaihe 1: Enberg Heinonen Laaksonen Malinen Paju Perho Saarinen Torikka Tuominen Välimäki 1982 1985 1983 1982 1984 1985 1984 1982 1983 1985 Vaihe 2: Enberg Malinen Torikka Laaksonen Tuominen Paju Saarinen Heinonen Perho Välimäki 1982 1982 1982 1983 1983 1984 1984 1985 1985 1985 Ellei olisi käytetty toisessa lajitteluvaiheessa stabiilia lajittelumenetelmää, ei aakkosjärjestyksen säilymisestä eri syntymävuosien sisällä olisi ollut varmuutta. 8.3 Kantalukulajittelu • • • • Kantalukulajittelu voidaan soveltaa parhaiten kokonaisluvuille, jotka ovat numeroesitykseltään kiinteän mittaisia (tarvittaessa käytetään täytteenä etunollia). Algoritmin toiminta-ajatuksena on lajitella numero kerrallaan aloittamalla vähiten merkitsevästä numerosta ensiksi ykkösten, sitten kymmenten, tämän jälkeen satojen mukaan jne. Yksittäisen numeron mukainen lajitteluun voidaan käyttää jotain stabiilia lajittelumenetelmää kuten juuri edellä esiteltyä laskentalajittelua. Kantalukulajittelualgoritmi on lyhykäisyydessään seuraavanlainen: KANTALUKULAJITTELU(A, d) 1 FOR i := 1, 2, …, d DO 2 Lajittele A:n alkiot stabiilisti paikan i mukaan oikealta vasemmalle • Esitetään luennolla esimerkki, miten syöte A = < 329, 281, 457, 734, 586, 091, 657, 839, 838, 436, 115, 720, 611, 919 ja 355 > lajitellaan soveltamalla kantalukulajittelua. 8.3 Kantalukulajittelu • Seuraavassa esitetään kantalukulajittelualgoritmin analyysi: Oletukset: 1) lajiteltavia lukuja on yhteensä n kappaletta 2) kantalukuna esiintyy k (esimerkiksi kymmenjärjestelmässä k = 10) Jokaista numeroa kohti joudutaan suorittamaan laskentalajittelu, jonka suoritusaika yhtä numeroa kohti on Ο(n + k). Laskentalajitteluja tehdään yhteensä niin monta, kuin lajiteltavien lukujen numeroesityksessä on numeroita. Olkoon numeroiden lukumäärä d. Kantalukulajittelun aikakompleksisuudeksi saadaan siten O(d(n + k)). Aikakompleksisuus on lineaarinen syötteen pituuden suhteen eli O(n), mikäli d ja k ovat vakioita. 8.4 Nippulajittelu • • • • Tällä kurssilla esitettävä nippulajittelun versio perustuu oletukseen, että lajiteltavat avaimet, jotka on tallennettu vektoriin A[1..n], ovat puoliavoimelle välille [0..1) sijoittuvia reaalilukuja. Menetelmän tarkastelu ja analyysi yksinkertaistuvat oletuksen myötä. Käytännön tilanteissa tehdään luonnollisestikin skaalaus halutulle vaihteluvälille. Algoritmin toiminta-ajatus: Jaetaan lukualue [0, 1) yhteensä n:ään osaan seuraavasti: [0..1/n), [1/n..2/n), [2/n..3/n), …, [(n – 1)/n..1). Kutakin tällaista osaa vastaa yksi nippu B[0], B[1], B[2], …, B[n – 1]. Syötteen alkiot sijoitetaan kyseisiin nippuihin, jotka esitetään indeksipaikoista alkavina linkitettyinä listoina. 8.4 Nippulajittelu • • • • Nippulajittelun taustalla on oletus syötteen avaimien tasaisesta jakaumasta välillä [0..1). Mikäli oletus pitää paikkansa, ei ole odotettavissa, että yhteen yksittäiseen nippuun päätyy kovinkaan monta syötevektorin alkiota. Kunkin nipun sisällä tehdään erikseen lajittelu. Kun jokainen nippu on sisäisesti lajiteltu, ei tarvitse muuta kuin yhdistää nippujen sisältämät lajitellut alkiolistat peräkkäin. Nipun indeksi toimii nippujen välisenä suuruuserottimena: nipussa x sijaitsee isompia avainarvoja kuin nipussa y, jos x:n indeksi > y:n indeksi. Seuraavassa esitetään nippulajittelualgoritmi: NIPPULAJITTELU(A) 1 n := pituus[A] 2 FOR i := 1, 2, …, n DO 3 Lisää A[i] linkitettyyn listaan, joka alkaa indeksipaikasta B[n ⋅ A[i] 4 /* Lisäys tapahtuu aina listan alkuun. */ 5 FOR i := 0, 1, …, n – 1 DO 6 Lajittele lista B[i] käyttämällä lisäyslajittelua 7 Yhdistä (eli katenoi) listat B[0], B[1], …, B[n – 1] • Kannattaa huomioida, että lisäyslajittelu voidaan tarvittaessa muuntaa muotoon, joka tukee linkitettyjen listojen käsittelyä (lisäyslajittelun syötteen ei välttämättä tarvitse sijaita vektorissa). 8.4 Nippulajittelu • Esitetään luennolla esimerkki, jossa lajitellaan seuraava syöte käyttämällä nippulajittelua: A = < 0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.49, 0.23, 0.63 > Nippujen esittämiseen käytettävät linkitetyt listat sijoitetaan nippuvektoriin B indeksipaikkoihin 0..9. Nippuun B[i] sijoitetaan alkiot väliltä [i/10, …, (i + 1)/10). • Algoritmin analyysi: 1) Pahin tapaus: Taustalla olevasta tasaisen jakauman oletuksesta huolimatta syötteen kaikki n alkiota päätyvät kuitenkin yhteen ja samaan nippuun. Kyseistä nippua edustavan listan pituudeksi tulee siten n. Tämän listan lajittelu vie huonoimmassa (ja myös keskimääräisessä) tapauksessa aikaa Θ(n2). Siten myös koko nippulajittelun pahimman tapauksen suoritusaika on Θ(n2). 2) Keskimääräinen (ja samalla myös paras) tapaus: Algoritmissa on kaksi silmukkaa, joissa kummassakin tehdään n kierrosta. Alkion lisääminen nippuun B[i] vie vakioajan, sillä lisäys tapahtuu aina alkuun. Koska alkioita on n kappaletta, rivien 2 – 3 silmukkaa suoritetaan Ο(n) kertaa. Jos alkiot ovat tasaisesti jakautuneet, kuhunkin nippuun tulee keskimäärin 1 alkio. Yhden alkion sisältävien nippujen B[i] lajitteleminen vie selvästikin vakioajan ci. Tuolloin yhteensä n kappaletta nippuja voidaan lajitella ajassa Ο(n), sillä c0 + c1 + … + cn ≤ cn, missä c = max{ci | 0 ≤ i ≤ n – 1} Listojen yhdistäminen tapahtuu ajassa Ο(n), sillä ne vain linkitetään toistensa perään. Keskimääräinen kokonaissuoritusaika nippulajittelulle on siten Ο(n). 9 Valinta-algoritmit 9.1 Taulukon minimi ja maksimi • Valinta-algoritmien tarkoituksena on löytää syötteen suuruusjärjestyksessä tietty – yleensä yksittäinen alkio. • Syötteeksi annetaan lajittelualgoritmien tapaan vektori A[1..n]. • Tulosteeksi halutaan useasti joko syötevektorin 1) minimi 2) maksimi 3) sekä minimi että maksimi samalla kerralla etsittyinä tai 4) järjestyksessä i. pienin alkio (1 ≤ i ≤ n) • Haluttiinpa näistä mikä tahansa, ongelma voidaan ratkaista triviaalisti lajittelemalla annettu syöte ja palauttamalla asetetut kriteerit täyttävä alkio minimiä haettaessa lajitellun vektorin 1. alkio maksimia haettaessa lajitellun vektorin n. alkio molempia haettaessa sekä ensimmäinen että viimeinen alkio järjestyksessä i. alkio löytyisi selvästikin indeksistä i • On kuitenkin turhaa mennä lajittelemaan syötevektoria, jos ollaan kiinnostuneita ainoastaan yhdestä alkiosta (tai maksimi-minimi -parista), sillä paras yleiskäyttöinen lajittelumenetelmä vaatii suoritusajan Ο(nlog2n), kun taas yksittäisen alkion etsintä onnistuu lineaarisessa ajassa eli Ο(n). • Esitellään seuraavassa menetelmät edellä numeroin 1-4 lueteltujen alkioiden löytämiseksi. 9.1 Taulukon minimi ja maksimi • Aloitetaan minimin (tai maksimin) etsinnästä (seuraavassa oletetaan, että syötevektori A ei ole tyhjä): MINIMI(A) 1 min := A[1] 2 FOR i := 2, 3, …, pituus[A] DO 3 IF min > A[i] 4 THEN min := A[i] 5 RETURN min MAKSIMI(A) 1 max := A[1] 2 FOR i := 2, 3, …, pituus[A] DO 3 IF max < A[i] 4 THEN max := A[i] 5 RETURN max • Kummassakin tapauksessa joudutaan tekemään yhteensä n – 1 vertailuoperaatiota, mikäli syötteen alkiot voivat olla mielivaltaisessa järjestyksessä algoritmit ovat optimaaliset – vähemmin vertailuin ei pärjätä! • Yhteys käytäntöön: miten löydetään pudotuspelitekniikalla turnauksen paras n:stä osallistujasta? muut paitsi voittaja häviävät tarkalleen kerran voittajan lisäksi osallistujia on yhteensä n – 1 • Pelkän minimin tai maksimin etsinnän aikakompleksisuus on siten Θ(n). 9.1 Taulukon minimi ja maksimi • • • Tarkastellaan seuraavaksi sekä minimin että maksimin etsintää. Selvästikin molemmat pystytään saamaan selville vertailumäärällä 2(n – 1), sillä voitaisiin ensinnä etsiä vain jompaakumpaa ja sen perään toista. tarvitaan yhteensä erikseen minimin ja maksimin etsintään kuluva aika Tätä vähemmällä työllä on kuitenkin mahdollista selviytyä (joskaan ei asymptoottisesti). Esitellään algoritmi MINMAX, joka löytää molemmat tekemällä vain 3n/2 vertailua. Algoritmissa oletetaan, että vektori A ei ole tyhjä. MINMAX(A) 1 FOR i := 1, 2, …, n/2 DO 2 IF A[2i – 1] > A[2i] 3 THEN vaihda A[2i – 1] <---> A[2i] 4 min := A[1] 5 FOR i := 3, 5, 7, …, 2n/2 - 1 DO 6 IF min > A[i] 7 THEN min := A[i] 8 max := A[n] 9 i := 2 10 WHILE i < n DO 11 IF max < A[i] 12 THEN max := A[i] 13 i := i + 2 14 RETURN (min, max) • • • Algoritmin toimintaperiaate: verrataan paikoissa 2i – 1 ja 2i olevia alkioita keskenään kaikille i:n arvoille väliltä 1..n/2. Sijoitetaan aina pienempi näiden parien alkioista parittomaan ja suurempi parilliseen indeksiin. Jos n on pariton, jää viimeinen alkio toistaiseksi käsittelemättä. Tämän jälkeen etsitään minimiä pelkästään parittomista indeksipaikoista ja maksimia pelkästään parillisista sekä vertailematta jääneestä viimeisestä indeksistä, jos n on pariton (sieltä voi löytyä yhtäläisesti minimi tai maksimi). 9.1 Taulukon minimi ja maksimi • Tarkastellaan luennolla esimerkkiä, kun syötevektorina esiintyy A = < 7, 3, 2, 4, 9, 1, 5, 2, 6 > • Algoritmin analyysi: 1) Jos n on parillinen, niin vertailujen määrä on: n/2 + (n/2 – 1) + (n/2 – 1) = 3n/2 – 2 ≤ 3n/2 2) Jos n on puolestaan pariton, niin vertailuja tehdään: n/2 + (n/2 – 1) + (n/2 – 1) + 2 = 3n/2 • Kummassakin tapauksessa joudutaan tekemään enintään 3n/2 ≈ 1½n 9.2 Valinta keskimäärin lineaarisessa ajassa • • • • Tarkastellaan seuraavaksi ”pikavalintaa”, joka muistuttaa toteutukseltaan melkoisesti pikalajittelua. Pikavalinta palauttaa tarkasteltavan osavektorin A[p..r] i. alkion. Lisäksi oletetaan, että parametrin i valinta on kelvollinen, eli 1 ≤ i ≤ r – p + 1, eli tarkasteltavassa osavektorissa on tarpeeksi eli vähintään i alkiota. Pikavalintaa suorittava algoritmi (satunnaistettu versio) on esitetty seuraavassa: SATUNNAISTETTU_VALINTA(A, p, r, i) 1 IF p = r 2 THEN RETURN A[p] 3 q := SATUNNAISTETTU_PARTITIOINTI(A, p, r) 4 k := q – p + 1 5 IF i = k 6 THEN RETURN A[q] 7 ELSE IF i < k 8 THEN RETURN SATUNNAISTETTU_VALINTA(A, p, q – 1, i) 9 ELSE RETURN SATUNNAISTETTU_VALINTA(A, q + 1, r, i – k) • Toiminta-ajatus: tutkitaan, mikä on syöteparametri i:n arvo suhteessa pivot-alkion lopulliseen sijoituspaikkaan q. 1) jos i = k, saadaan vastaus heti palauttamalla A[q]:n arvo 2) jos i < k, jatketaan etsimällä järjestyksessä i. alkiota nyt ensimmäisestä osavektorista 3) jos i > k, jatketaan etsimällä järjestyksessä i – k. alkiota jälkimmäisestä osavektorista Tapauksessa 3 on jo ehditty ohittaa syötevektorin alusta k alkiota pudottamalla ensimmäinen osavektori ja pivot-alkio jatkotarkastelujen ulkopuolelle! 9.2 Valinta keskimäärin lineaarisessa ajassa 9.2 Valinta keskimäärin lineaarisessa ajassa 9.2 Valinta keskimäärin lineaarisessa ajassa 9.2 Valinta keskimäärin lineaarisessa ajassa = 2c/n (½(n2 – n) – ½(⅟₄n2 – 3n/2 + 2)) + an = c/n (¾n2 + ½n – 2) + an = c(¾n + ½ – 2/n) + an ≤ ¾cn + ½c + an = cn – (⅟₄cn – ½c – an) Jotta termi cn kelpaisi suoritusajalle ylärajaksi, pitää nyt vielä osoittaa, että ⅟₄cn – ½c – an ≥ 0, kun n on tarpeeksi iso. Lisätään yhtälöön puolittain vakio c/2, niin saadaan: ⅟₄cn – an ≥ ½c ⇔ n(⅟₄c – a) ≥ ½c Olettaen, että vakio c on valittu siten, että c > 4a, voidaan jakaa puolittain termillä ⅟₄c – a, joka on nyt > 0: ⇒ n ≥ ½c / (⅟₄c – a) = 2c / (c – 4a) /* Lavennetaan nelosella. */ Kun n > 2c / (c – 4a), niin tällöin on voimassa T(n) ≤ cn. Tämän lisäksi oletetaan, että T(n) = Θ(1), kun n < 2c / (c – 4a) • Äskeisen analyysin perusteella i. alkio voidaan siten löytää keskimäärin lineaarisessa ajassa. 10 Perustietorakenteet 10.1 Pinot ja jonot • • Pino on tietorakenne, jonne alkioita voi lisätä ainoastaan päällimmäiseksi, ja ainoastaan viimeksi pinoon tuotuun alkioon päästään tarkasteluhetkellä käsiksi. Viimeksi pinoon tuotu alkio poistetaan ensimmäiseksi. Lisäys- ja poisto-operaatiot kohdistuvat pinossa samaan kohtaan. Analogia reaalimaailmasta: bussikuskin tai torimyyjän siilomainen kolikkolipas tietojenkäsittelystä: rekursiopino pinon huippu • • • 6 10 5 7 13 jonon alku ja loppu 13 7 5 10 6 Jono on puolestaan tietorakenne, jonne lisääminen tapahtuu loppuun ja jossa poisto kohdistuu aina alkuun. Kauimmin jonossa ollut poistetaan ensiksi Analogia reaalimaailmasta: kaupan kassajonon eteneminen (ilman etuilua … !) Siitä huolimatta, että pino ja jono ovat luonteeltaan dynaamisia rakenteita – s. o. niiden koot vaihtelevat herkästi, ne voidaan kuitenkin hyvin toteuttaa staattisella tietorakenteella: vektorilla Vektorille pitää kuitenkin varata tarpeeksi paljon muistia, ettei pino/jono täyty vastoin ennakko-odotuksia. Seuraavassa tullaan esittelemään pinoja ja jonoja käsittelevät algoritmit siten, että taustalla olevana perustietorakenteena on käytetty juuri vektoria. 10.1 Pinot ja jonot ENSIN PINOISTA … • Pino toteutetaan seuraavassa vektorin S[1..n] avulla. • Pinolla S on olemassa attribuutti huippu[S]: osoittaa pinoon viimeksi viedyn alkion indeksin pinovektorissa. • huippu[S] = 0, jos pinossa S ei ole yhtään alkiota (eli se on tyhjä) • Pinoon S ovat tallennettuina alkiot S[1..huippu[S]]. • Paikasta S[1] löytyy pinon pohjimmainen alkio. • Vastaavasti paikasta S[huippu[S]] löytyy pinon päällimmäisin alkio. • Seuraavassa pienoinen esimerkki pinovektorista: S= 13 7 5 10 6 huippu[S] • Seuraavalla algoritmilla voidaan testata, onko pino tyhjä. PINO_TYHJÄ?(S) 1 IF huippu[S] = 0 2 THEN RETURN tosi 3 ELSE RETURN epätosi • Kyseinen algoritmi toimii vakioajassa Ο(1). 10.1 Pinot ja jonot • Seuraava algoritmi lisää alkion pinoon: LISÄÄ_PINOON(S, x) 1 huippu[S] := huippu[S] + 1 2 S[huippu[S]] := x • Uusi alkio asetetaan pinoon siirtämällä huipun sijaintia yhdellä eteenpäin ja asettamalla alkio päällimmäiseksi • Myös tämä algoritmi toimii vakioajassa Ο(1). • Alkion poistaminen pinosta tapahtuu puolestaan seuraavasti: POISTA_PINOSTA(S) 1 IF PINO_TYHJÄ?(S) 2 THEN virheilmoitus ”Pinon alivuoto” 3 ELSE huippu[S] := huippu[S] – 1 4 RETURN S[huippu[S] + 1] • Pinosta poistetaan selvästikin päällimmäisin alkio. • Poistamisyritys tyhjästä pinosta täytyy estää alivuodon välttämiseksi. • Tämäkin pinoalgoritmi toimii vakioajassa Ο(1). 10.1 Pinot ja jonot … JA SITTEN JONOISTA • • • • • • • • Samoin kuin pinot edellä, myös jonot toteutetaan seuraavassa vektorin avulla. Jonovektori Q on indeksoitu välille 1..n. Jonolla Q on olemassa seuraavat kaksi attribuuttia: alku[Q] osoittaa jonoon ensimmäiseksi viedyn alkion indeksin jonovektorissa (ellei ole kyseessä tyhjä jono). loppu[Q] osoittaa jonon ensimmäisen vapaan paikan, jonne alkion lisääminen tapahtuu. Mikäli alku[Q] = loppu[Q], tällöin jono on tyhjä. Lähtötilanteessa alku[Q] = loppu[Q] = 1. Jonon alkiot sijaitsevat paikoissa alku[Q], alku[Q] + 1, …, loppu[Q] – 1. mikäli loppupään indeksit ovat jostain indeksiarvosta lähtien > n, pitää tätä seuraavien alkioiden sijaintipaikan määräämiseksi ottaa jakojäännnösoperaatio n:n suhteen (indeksi modulo n), sillä jono voi jatkua ”syklisesti” indeksistä 1. vektorin alkukohta siirtyy yhdellä eteenpäin, kun jonosta poistetaan alkio, paitsi silloin, kun alku[Q] = n. Kun kyseinen alkio aikanaan poistetaan, asetetaan alku[Q] := 1. on aivan mahdollista, että alku[Q] > loppu[Q]: tällöin jonon häntä jatkuu vektorin alusta. Jonoon voidaan sijoittaa maksimissaan n – 1 kappaletta alkioita. Indeksiin loppu[Q] ei koskaan sijoiteta mitään, jotta pystyttäisiin erottamaan toisistaan tilanteet, milloin jono on tyhjä ja milloin täysi. 10.1 Pinot ja jonot • Esimerkki tilanteesta, jossa jonoa joudutaan jatkamaan vektorin alusta: Tilanne ennen alkioiden 11 ja 19 lisäämistä silloin, kun jonon alku sijaitsee paikassa 7 ja loppu paikassa 11: alku[Q] loppu[Q] Q= 1 2 3 4 5 6 13 7 5 10 7 8 9 10 6 11 12 Alkio 11 voidaan sijoittaa nyt vektorin loppuun, mutta loppukohta siirtyy nyt indeksiin 1, … loppu[Q] alku[Q] Q= 13 7 5 10 6 11 1 2 3 4 5 6 7 … jonne alkio 19 sijoitetaan. 19 8 9 10 11 12 alku[Q] 13 7 5 10 loppu[Q] • Seuraavassa esitetään neljä algoritmia jonojen käsittelyä varten. 6 11 10.1 Pinot ja jonot • Seuraava algoritmi testaa, onko jono jo täyttynyt: JONO_TÄYSI?(Q) 1 IF alku[Q] = (loppu[Q] + 1) mod n 2 THEN RETURN tosi 3 ELSE RETURN epätosi • Algoritmi toimii vakioajassa Ο(1). • Alkion lisääminen jonoon tapahtuu puolestaan seuraavasti: LISÄÄ_JONOON(Q, x) 1 IF JONO_TÄYSI?(Q) 2 THEN virheilmoitus ”Jonon ylivuoto” 3 ELSE Q[loppu[Q]] := x 4 IF loppu[Q] = pituus[Q] 5 THEN loppu[Q] := 1 6 ELSE loppu[Q] := loppu[Q] + 1 • Alkion lisääminen jonoon toimii selvästikin vakioajassa Ο(1). • Algoritmi huomioi mahdollisen tarpeen jatkaa jonoa vektorin Q alusta. • Lisäksi jo täynnä olevaan jonoon tapahtuva alkion lisäämisyritys estetään. 10.1 Pinot ja jonot • Seuraavaksi esitellään jonon tyhjyyden testaamisalgoritmi: JONO_TYHJÄ?(Q) 1 IF alku[Q] = loppu[Q] 2 THEN RETURN tosi 3 ELSE RETURN epätosi • Algoritmi toimii vakioajassa Ο(1). • Lopuksi esitellään vielä, miten alkion poistaminen jonosta tapahtuu: POISTA_JONOSTA(Q) 1 IF JONO_TYHJÄ?(S) 2 THEN virheilmoitus ”Jonon alivuoto” 3 ELSE x := Q[alku[Q]] 4 IF alku[Q] = pituus[Q] 5 THEN alku[Q] := 1 6 ELSE alku[Q] := alku[Q] + 1 7 RETURN x • Alkion poistaminenkin jonosta onnistuu vakioajassa Ο(1). • Algoritmi estää poistamisyrityksen tyhjästä jonosta. • Lisäksi huomioidaan, pitääkö jonon alkukohta siirtää vektorin ensimmäiseen positioon. 10.2 Linkitetyt listat • Linkitetyt listat soveltuvat hyvin ns. dynaamisten joukkojen esittämiseen. Tällaisen joukon koko voi kasvaa tai pienentyä, tai sen ominaisuudet voivat muuttua algoritmin suorituksen ollessa käynnissä. • Tyypillisiä listoille ovat ns. sanakirja- eli hakemisto-operaatiot: uuden tietueen lisääminen olemassa olevan tietueen poistaminen tallennettuihin tietoihin kohdistuvat kyselyoperaatiot • Esimerkkejä: puhelinluettelo, sanakirja, kirjaston tietokanta yms. • Dynaamisen joukon alkiot ovat objekteja, joilla on 1) avain- eli tunnistekenttä (objektin identifioiva tieto) 2) satelliittidataa (muuta tallennettua tietoa avainarvon ohella) 3) osoitinkenttiä (viittauksia edeltäjään/seuraajaan – yleisesti: toisiin objekteihin) • Koneen keskusmuistissa jokaisella muistipaikalla on yksikäsitteinen osoite, jonka perusteella päästään käsiksi muistipaikkaan tallennettuun tietoon. • Osoitintietoa sisältävä muistipaikka sisältää viittauksen siihen muistipaikkaan, mistä varsinaiset tiedot löytyvät. • Osoitintiedon käsittelemisessä sallittavat operaatiot vaihtelevat ohjelmointikielittäin. Esimerkiksi Javassa ja Pythonissa osoittimien käsittely on rajoitetumpaa kuin C:ssä. 10.2 Linkitetyt listat • • • • • • Linkitetty lista on rakenne, johon voidaan sijoittaa samantyyppisiä tietoja peräkkäin. tässä ominaisuudessaan linkitetty lista muistuttaa vektoria MUTTA: listan mielivaltaiseen eli i. alkioon ei päästä suoraan käsiksi, vaan sinne on edettävä selaamalla tätä ennen sijaitsevat i – 1 alkiota läpi, kun taas taulukosta voidaan osoittaa milloin tahansa mitä muistipaikkaa tahansa (esimerkiksi x := A[i]). Jokainen listan alkioista sisältää ainakin yhden tulevan linkin, jota pitkin alkio on saavutettavissa. Viimeistä alkiota lukuun ottamatta alkioilla on myös seuraaja. Viittauksia merkitään monisteessa nuolilla (). Linkitetylle listalle on tyypillistä dynaamisuus, eli sen koko voi muuttua tarpeen niin vaatiessa. Uuden alkion lisäys tai vanhan poistaminen eivät aiheuta paljoa työtä, sillä muutokset ovat hyvin paikallisia. 1) Yhteen suuntaan linkitetty lista • • • • • Listan L kukin yksittäinen alkio x sisältää attribuutit: avain[x]: ilmaisee lista-alkioon x tallennettu avainarvon seuraava[x]: osoittaa x:ää seuraavaan alkioon Tyhjää osoitinta kuvaa algoritmeissa merkintä NIL, esimerkeissä kauttaviiva (/) Jos seuraava[x] = NIL, on x listan viimeinen alkio Koko listalla L on lisäksi olemassa attribuutti alku[L], joka viittaa listan L ensimmäiseen alkioon. Seuraavassa esimerkki yhteen suuntaan linkitetystä listasta: avain[x] alku[L] x seuraava[x] 14 3 7 18 / 10.2 Linkitetyt listat 2) Kahteen suuntaan linkitetty lista • Kuten yhteen suuntaan linkitetty lista, mutta nyt listan kukin yksittäinen alkio x sisältää lisäksi attribuutin: edellinen[x]: osoittaa x:ää edeltävään alkioon • Jos edellinen[x] = NIL, on x listan ensimmäinen alkio • Seuraavassa esimerkki kahteen suuntaan linkitetystä listasta L: z / 14 3 7 18 / alku[L] edellinen[z] avain[z] seuraava[z] • Kannattaa huomioida, että linkitetyt listat ovat usein järjestämättömiä. • Seuraavaksi lähdetään esittelemään linkitetyille listoille sovellettavia operaatioita. Aloitetaan alkion hakemisella linkitetystä listasta. HAE_LISTASTA(L, k) 1 x := alku[L] 2 WHILE x ≠ NIL AND avain[x] ≠ k DO 3 x := seuraava[x] 4 RETURN x 10.2 Linkitetyt listat • Algoritmi HAE_LISTASTA palauttaa viittauksen ensimmäiseen sellaiseen lista-alkioon x, jolle on voimassa avain[x] = k. • Ellei etsittyä avainta k löydy listasta L, paluuarvoksi tulee tyhjä osoitin NIL. • Algoritmin analyysi: Pahimmassa tapauksessa suoritusaika on Θ(n). Pahin tapaus esiintyy silloin, kun haettu alkio esiintyy yksistään listan viimeisenä alkiona tai sitä ei löydy listasta laisinkaan. Keskimääräisessä tapauksessa noin puolet alkioista on käytävä läpi, joten tällöinkin kompleksisuus on suuruusluokkaa Θ(n). • Tutkitaan seuraavaksi alkion lisäämistä kahteen suuntaan linkitettyyn listaan, kun lisäys tapahtuu listan alkuun. • Lisättävällä alkiolla on olemassa kaikki lista-alkion attribuuttikentät (edellinen, avain, seuraava). LISÄÄ_LISTAAN(L, x) 1 seuraava[x] := alku[L] 2 IF alku[L] ≠ NIL /* Lista L ei alun perin ollut tyhjä. */ 3 THEN edellinen[alku[L]] := x 4 alku[L] := x 5 edellinen[x] := NIL • Algoritmin kompleksisuus on Ο(1): lisäys tehdään aina listan alkuun (listan pituudella ei väliä). • Esitetään luennolla esimerkki, kun edellisen kalvon kahteen suuntaan linkitettyyn listaan, jossa avainarvoina ovat 14, 3, 7 ja 18, lisätään alkio 22. 10.2 Linkitetyt listat • Seuraavaksi esiteltävä algoritmi POISTA_LISTASTA poistaa listan L solmualkion x, johon oletetaan olevan suoraan osoitin käytettävissä (alkiota ei tarvitse enää erikseen lähteä hakemaan). • • • • POISTA_LISTASTA(L, x) 1 IF edellinen[x] ≠ NIL /* Ei olla poistamassa listan L ensimmäistä alkiota. */ 2 THEN seuraava[edellinen[x]] := seuraava[x] 3 ELSE alku[L] := seuraava[x] 4 IF seuraava[x] ≠ NIL /* Poistettava alkio ei ole listan viimeinen alkio. */ 5 THEN edellinen[seuraava[x]] := edellinen[x] Algoritmin suoritusaika on vakio eli O(1). Ellei suoraa osoitinta poistettavaan alkioon x kuitenkaan ole käytettävissä, tulee algoritmin suoritusajaksi O(n), eli suoritusaikaa dominoi alkion x etsintään kuluva aika. Esitetään luennolla esimerkki, jossa alkiot 22, 14, 3, 7 ja 8 sisältävästä listasta poistetaan solmu, joka sisältää avainarvon 7. Kuten edellä havaittiin, listasta alkiota poistettaessa pitää huomioida linkkien viittaamien alkioiden olemassaolo (kts. rivit 1 ja 4). ellei näitä rajatestejä tarvitsisi tehdä, algoritmi yksinkertaistuisi seuraavanlaiseksi: POISTA_LISTASTA_U(L, x) 1 seuraava[edellinen[x]] := seuraava[x] 2 edellinen[seuraava[x]] := edellinen[x] 10.2 Linkitetyt listat • Lyhennettyä versiota päästäisiin käyttämään, mikäli listaan asetetaan alkuun ns. pysäytys- eli otsakealkio, josta käytetään tunnistetta nil[L]. • Pysäytysalkio on muuten aivan lista-alkion kaltainen, mutta sen avainkenttää ei tarvita mihinkään – vain osoitinkentät ovat tarpeen. • Pysäytysalkion tehtävänä on tunnistaa listan päättyminen. • Viittaus seuraava[nil[L]] osoittaa listan ensimmäiseen alkioon. • Vastaavasti viittaus edellinen[nil[L]] osoittaa listan viimeiseen alkioon. listasta muodostuu kaksi silmukkaa. • Koska seuraava[nil[L]] osoittaa listan L alkuun, ei myöskään erillistä attribuuttia alku[L] tarvita. • Tyhjä lista koostuu pelkästä pysäytysalkiosta nil[L]. Tyhjä lista pysäytysalkion avulla esitettynä: nil[L] ? Esimerkki ei-tyhjästä kahteen suuntaan linkitetystä listasta pysäytysalkiolla: nil[L] ? 14 3 7 18 10.2 Linkitetyt listat • Seuraavassa vielä uusitut haku- ja lisäysalgoritmit, jos listan pysäytysalkio on käytettävissä (poistoalgoritmi esitettiinkin jo edellä): HAE_LISTASTA_U(L, k) 1 x := seuraava[nil[L]] 2 WHILE x ≠ nil[L] AND avain[x] ≠ k DO 3 x := seuraava[x] 4 RETURN x LISÄÄ_LISTAAN_U(L, x) 1 seuraava[x] := seuraava[nil[L]] 2 edellinen[seuraava[nil[L]]] := x 3 seuraava[nil[L]] := x 4 edellinen[x] := nil[L] • Pysäytysalkion käyttöönotto jonkin verran yksinkertaistaa lista-algoritmeja. 10.3 Juurrettujen puiden esittäminen • Matemaattisesti määriteltynä puu on yhdistetty, syklitön graafi. Jokainen puun solmuista on saavutettavissa Siirtymällä eteenpäin seuraajalinkkiä pitkin ei päästä enää takaisin kuin isäosoittimia pitkin. • Ei-tyhjällä puulla P on yksikäsitteinen juuri. Juuri on solmu josta alaspäin puu kasvaa. • Binääripuulla tarkoitetaan puuta, jonka kullakin solmulla on enintään kaksi lasta. • Yksittäinen binääripuun solmu x on kuvaukseltaan seuraavanlainen eli sisältää seuraavat attribuutit: avain[x]: sisältää solmuun x tallennetun avainarvon vasen[x]: sisältää osoittimen x:n vasempaan lapsisolmuun oikea[x]: sisältää osoittimen x:n oikeaan lapsisolmuun vanhempi[x]: sisältää osoittimen x:n isäsolmuun • Seuraavassa on nähtävillä hahmotelma binääripuun solmun rakenteesta: isäosoitin avainarvo osoitin vasempaan lapseen osoitin oikeaan lapseen 10.3 Juurrettujen puiden esittäminen • Koko puulla on olemassa attribuutti juuri[P], joka on osoitin puun juurisolmuun. • Juuri on solmuista ainoa, jonka isäosoitin on puuttuva eli arvoltaan NIL. Toisin sanoen, vanhempi[juuri[P]] = NIL NIL juuri[P] • Erityisesti, jos juuri[P] = NIL, on kyseessä tyhjä puu. • Seuraavalla sivulla on esitettynä esimerkki binääripuusta. • Puuttuvaa osoitinta merkitään rakennekuvissa kauttaviivalla (/). 10.3 Juurrettujen puiden esittäminen • Seuraavassa pieni esimerkki binääripuun P esittämisestä (Huom! Puu on järjestämätön). / juuri[P] 19 3 41 / 31 25 28 / / / 51 29 6 / / / / / / 10.3 Juurrettujen puiden esittäminen • Yleisessä puu-tietorakenteessa solmulla voi olla mielivaltainen määrä lapsisolmuja. • Tällaisessa vapaassa (ei välttämättä binääri-)puussa solmulla on olemassa seuraavat attribuutit: avain[x]: sisältää solmuun x liittyvän avainarvon vasen_lapsi[x]: sisältää osoittimen solmun x vasemmanpuoleisimpaan lapsisolmuun oikea_veli[x]: sisältää osoittimen solmun x oikeanpuoleiseen velisolmuun vanhempi[x]: sisältää osoittimen solmun x isäsolmuun • Vasen lapsiosoitin on NIL silloin, kun kyseessä on lehtisolmu • Itse puulla on lisäksi attribuutti juuri[P], joka sisältää osoittimen puun juurisolmuun. 10.3 Juurrettujen puiden esittäminen • Seuraavassa vielä pieni esimerkki yleisen puun esittämisestä: / 3 juuri[P] / 45 25 / / 76 51 29 / / / / 11 Hajautustaulut (Hash-taulut) • Hajautustauluja käytetään (tieto)hakemistojen toteuttamiseen • Yksittäisellä säilöttävällä tietoalkiolla eli objektilla on: 1) tunnistearvo avain[x] 2) tämän lisäksi muita mahdollisia tietokenttiä eli attribuutteja – ts. satelliittidataa • Tietoalkioille pitää pystyä suorittamaan seuraavia operaatioita uuden alkion lisääminen hakemistoon hakemistoon kohdistuvia kyselyitä olemassa olevan alkion poistaminen • Kyseiset operaatiot on ovat nopeita, sillä ne voidaan toteuttaa keskimäärin vakioajassa! 11.1 Suorasaantitaulut • Seuraavassa oletetaan, että 1) Tallennettavat avainarvot kuuluvat joukkoon U = {0, 1, 2, …, m – 1}, missä m ei saa olla ”kohtuuttoman iso”. Toisin sanoen, avainten vaihteluväli ei saa olla ”liian pitkä”. 2) Kaikilla tallennettavilla arvoilla on eri avain keskenään (ei sallita duplikaatteja avaimelle). • Mikäli nämä vaatimukset täyttyvät, voidaan käyttää hakurakenteena suorasaantitaulua T, joka on indeksoitu välille 0..m – 1. 11.1 Suorasaantitaulut • Joukon U kaikille mahdollisille alkioille 1..m – 1 muodostettu suorasaantitaulu T on seuraavanlainen: objekteista koostuva taulukko taulun paikassa T[i] on osoitin objektiin, joka sisältää avainarvon i objekti voi sisältää myös satelliittidataa mikäli tietty avainarvo i ei tarkasteluhetkellä kuulu U:n alijoukkona olevaan avainten joukkoon K, on T[i] tällöin NIL. • Esimerkki: Olkoon m = 12, jolloin U = {0, 1, 2, …, 11}, ja lisäksi K = {0, 1, 4, 7, 9, 10}. Tällöin suorasaantitaulu T on indeksoitu välille 0..11 ja näyttää seuraavanlaiselta: 0 1 T= 0 1 2 3 / / 4 4 5 6 / / 7 8 9 10 / 7 11 / 9 10 11.1 Suorasaantitaulut • Suorasaantitauluihin voi kohdistaa seuraavia operaatioita: 1) Tietyn avaimen sisältävän alkion etsiminen taulusta ETSI_SUORASAANTITAULUSTA(T, k) RETURN T[k] 2) Tietyn avaimen sisältävän alkion lisääminen tauluun LISÄÄ_SUORASAANTITAULUUN(T, x) T[avain[x]] := x 3) Taulussa olevan solmualkion x poistaminen sen avainarvon perusteella POISTA_SUORASAANTITAULUSTA(T, x) T[avain[x]] := NIL • Kaikki edellä esitetyt operaatiot ovat vakioaikaisia ja siten hyvin nopeita. 11.2 Hajautustaulut • • • Kuten edellä todettiin, kaikki suorasaantitauluihin kohdistuvat operaatiot ovat vakioaikaisia. Pulma syntyy kuitenkin silloin, kun joukon U koko alkaa kasvaa voimakkaasti. tarvitaan enemmän ja enemmän tilaa suorasaantitaulun T tallentamiseksi muistiin lisäksi suuri osa varatusta tilasta on käytännössä hyödytöntä, jos taulussa on kuitenkin vain vähän avaimia taulun kokoon nähden, eli taulun täyttösuhde on pieni. Pulman ratkaisuehdotus: yritetään suhteuttaa taulun T koko joukon K kokoon eli käytössä olevien avainten määrään koko sallitun arvoalueen pituuden asemesta. Tämä voidaan toteuttaa määrittelemällä ns. hajautus- (eli hash-)funktio h siten, että se kuvaa kaikki joukon U mahdolliset avaimet käytössä olevien avainten joukon K alkioiden määrän mittaiselle vaihteluvälille seuraavasti: h: U {0, 1, 2, …, |K| ̶ 1}, missä |K| edustaa avainten tämänhetkistä lukumäärää • • Siten alkio, jonka alkuperäinen avainarvo on k, tallentuu taulukossa T paikkaan h(k). Määritellään vastedes, että hajautustaulu T indeksoidaan välille 0..m ̶ 1, missä m = |K|, eli tämä määrittely kumoaa termin m edellä käytetyn merkityksen suurimpana sallittuna alkuperäisenä avaimena + 1. m edustaa kuitenkin edelleen taulun T kokoa. Syntyy kuitenkin uusi ongelma: koska funktio h on käytännössä aina kutistava kuvaus, käy ennen pitkää niin, että funktio h kuvaa kaksi tai useampia U:hun kuuluvia alkioehdokkaita samaan osoitteeseen, eli h(k1) = h(k2) ainakin joillakin k1 ja k2, kun k1 ≠ k2. syntyy osoitetörmäys. 11.2 Hajautustaulut • Mikäli osoitetörmäykset joudutaan hyväksymään, pitää löytää jokin menetelmä niiden hallitsemiseen tietoja menettämättä. ratkaisu ei varmaankaan liene paikassa T[i] olevan aikaisemman alkion tuhoaminen …! • Tarkastellaan seuraavaksi vaihtoehtoja, miten osoitetörmäysongelma voidaan ratkaista. • 1) Ketjutus: muodostetaan samaan indeksipaikkaan päätyneistä alkioista lista. Hajautustaulun paikassa T[i] on osoitin ensimmäiseen sellaiseen alkioon, jonka hajautusfunktio h on kuvannut paikkaan i – toisin sanoen h(avain[x]) = i. Mikäli funktion h valinta on onnistunut, ja avainten jakautuma lähtöjoukossa on tasainen, T:n jokaiseen positioon tulee vain vähän alkioita keskimäärin. Toiminta-ajatus on käytännössä ihan sama kuin nippulajittelussakin. Seuraavassa pieni esimerkki: 0 0 1 1 2 3 / / 4 5 6 / / 7 28 7 4 31 8 9 10 11 / / / / 11.2 Hajautustaulut • Ketjutusta käyttäviin hajautustauluihin voi kohdistaa seuraavia operaatioita: 1) Tietyn avaimen sisältävän alkion etsiminen taulusta ETSI_KETJUTETUSTA_HAJAUTUSTAULUSTA(T, k) /* Etsii listasta T[h(k)] alkiota, jolla on avaimena k. */ 2) Tietyn avaimen sisältävän alkion lisääminen tauluun LISÄÄ_KETJUTETTUUN_HAJAUTUSTAULUUN(T, x) /* Lisää alkion x listan T[h(avain[x])] alkuun. */ 3) Taulussa olevan solmualkion x poistaminen sen avainarvon perusteella POISTA_KETJUTETUSTA_HAJAUTUSTAULUSTA(T, x) /* Poistaa lista-alkion x listasta T[h(avain[x])]. */ • Lisäysoperaation kustannus edelleen vakioaikainen kuten suorasaantitauluissakin. • Sen sijaan alkion etsinnän ja poiston kustannus riippuu paikasta T[h(k)] (tai T[h(avain[x])) alkavan listan pituudesta. 11.2 Hajautustaulut • • • • • • Seuraavassa muutamia käsitteitä ja määritelmiä n = hajautustauluun T tallennettujen alkioiden lukumäärä m = taulun T koko taulun täyttösuhde α määritellään seuraavasti: α = n / m Oletetaan seuraavaksi, että kyseessä on ns. yksinkertainen tasainen hajautus. Tällöin hajautusfunktio jakaa kutakin arvoa väliltä 0, 1, 2, …, m – 1 samalla todennäköisyydellä 1 / m. Kannattaa huomioida, että täyttösuhde α on sama kuin kunkin listan keskimääräinen pituus. Tarkastellaan seuraavassa erikseen onnistuneen ja epäonnistuneen haun suoritusaikaa Onnistunut haku: etsitään avainta, joka on tallennettuna hajautustauluun T. Epäonnistunut haku: etsitään avainta, jota ei esiinny kyseisessä hajautustaulussa. Seuraavassa oletetaan edelleen, että arvon h(k) laskeminen tapahtuu vakioajassa eli sen kustannus on Θ(1). Lause 11.1: Epäonnistuneen haun kustannus on keskimäärin Θ(1 + α). Todistus: Etsitään alkiota, jonka avaimena esiintyy k. Hajautusfunktion arvon h(k) laskemiseen kuluu aikaa Θ(1). Paikasta T[h(k)] alkava lista joudutaan käymään kokonaan läpi, sillä ollaan etsimässä alkiota, jota listassa ei esiinny. Siten työmääräksi saadaan yhteensä Θ(1) + Θ(α) = Θ(1 + α). 11.2 Hajautustaulut • Lause 11.2: Myös onnistuneen haun kustannus on keskimäärin Θ(1 + α). Todistus: Etsitään alkiota, jonka avaimena esiintyy k. Avain voi olla mikä tahansa taulussa olevista n avaimesta. Huom! Uuden alkion lisäys tapahtuu aina listan alkuun. Lisäksi oletetaan, että minkä tahansa hajautustauluun tallennetun avaimen etsiminen on yhtä todennäköistä. Etsitään sellaista avainta k, joka on tallennettu tauluun i:ntenä. Tällöin paikasta T[h(k)] alkavassa listassa on keskimäärin (n – i)/m alkiota, jotka sijaitsevat ennen etsittävää alkiota (nämä alkiot on lisätty kyseiseen listaan tarkasteltavan eli i. alkion jälkeen). Lisäksi vielä etsittävä i. alkio itse on tutkittava. Lasketaan siten hakuaikojen keskiarvo kaikkien mahdollisten i:n valintojen ylitse. Saadaan: ) = (1/n) ⋅ ((1 + (n – 1)/m) + (1 + (n – 2)/m) + (1 + 1/m) + (1 + 0)) = (1/n) ⋅ ((n + (1/m))((n – 1) + (n – 2) + … + 2 + 1) = (1/n) ⋅ (n + (1/m)½n(n – 1)) /* (1/n) ja n:t kumoutuvat. */ = 1 + (1/(2m))(n – 1) = 1 + n/(2m) – 1/(2m) /* Sijoitus: n/m = α. */ = 1 + α/2 – α/(2n) /* Viimeinen termi lavennettiin luvulla 2n ≠ 0. */ Kun n ≥ 2, saadaan: 1 + α/2 – α/(2n) ≥ 1 + α/2 – α/4 = ¼(1 + α) Toisaalta: 1 + α/2 – α/(2n) ≤ 1 + α/2 ≤ 1 + α (1/n) ∑(1 + Siten 1 + α/2 – α/(2n) = Θ(1 + α). Lisäksi hajautusfunktion arvon h(k) laskeminen vie vakioajan. Siten onnistuneen haun kokonaissuoritusaika on hajautusfunktion arvon laskenta mukaan luettuna Θ(1) + Θ(1 + α) = Θ(1 + α). 11.2 Hajautustaulut • Edellä esitettyjen analyysien perusteella voidaan todeta, että ketjutukseen perustuvaa osoitetörmäysten hallintaa käyttämällä kaikki hakemisto-operaatiot saadaan tehtyä keskimäärin vakioajassa (lisäyksen kompleksisuus on aina vakioaikainen, sillä uusi alkio lisätään aina paikasta T[h(avain[x])] alkavan listan alkuun). • Jos esimerkiksi n ≈ 2700 ja m ≈ 900, saadaan täyttösuhteeksi α = m / n = 2700 / 900 = 3. Hajautustauluun muodostuvat listat ovat keskimäärin kolmen mittaisia. 11.3 Hajautusfunktioista • • • • Hajautusfunktion tärkeimpänä ominaisuutena voidaan pitää, että se toteuttaa yksinkertaisen tasaisen hajautuksen periaatteen. Tällöin hajautusfunktio antaa talletettavien alkioiden avaimille samalla todennäköisyydellä minkä tahansa arvon väliltä 0..m ̶ 1. Talletettavan avaimen sijoittamisen todennäköisyys paikkaan T[i] ei ole riippuvainen siitä, mihin aikaisemmin tauluun tallennetut avaimet ovat päätyneet. Kannattaa huomioida, että hajautusfunktion toimivuuden ihanteellisuutta ei usein pystytä tarpeeksi luotettavasti mittaamaan saati todistamaan. Esimerkki hajautusfunktiosta: Oletetaan, että talletettavat avaimet k ovat täysin satunnaisesti valittuja reaalilukuja väliltä {0 ≤ k < 1}. Valitaan nyt hajautusfunktioksi h(k) = km Tällöin saadaan aikaan selvästikin yksinkertainen tasainen hajautus 11.3 Hajautusfunktioista • Esimerkiksi merkkijonot (sanat) voitaisiin tulkita ”128-järjestelmän” luvuiksi. 7-bittisessä ASCII-koodissa eri merkit kuvataan numeroarvoiksi 0..127 • Mikäli 7-bitin ASCII-koodin mukaisista merkeistä koostuvat sanat haluttaisiin muuntaa 10-järjestelmän luvuiksi, voidaan koodaus suorittaa merkeittäin. • Esimerkki: Sanan ”Hauki” koodaaminen merkeittäin 10-järjestelmään: Tarvittavat ASCII-merkkien koodiarvot: ’H’ = 72, ’a’ = 97, ’u’ = 117, ’k’ = 107 ja ’i’ = 105 Siten sanaa ”Hauki” vastaisi 10-järjestelmän kokonaisluku k k = (72 * 1284) + (97 * 1283) + (117 * 1282) + (107 * 1281) + (105 * 1280) = 19 327 352 832 + 203 423 744 + 1 916 928 + 13 696 + 105 = 19 532 707 305 11.3.1 Jakojäännösmenetelmä • Jakojäännösmenetelmässä hajautusfunktio on muotoa: h(k) = k mod m • Esimerkki: Jos m = 12 ja k = 59 h(k) = 11, sillä 59 = 4 * 12 + 11. • Jakojäännösmenetelmää käyttämällä saadaan aikaan tasainen hajautus, jos talletettavat avaimet ovat jakautuneet tasaisesti. 11.3.2 Kertolaskumenetelmä 11.4 Avoin osoitteenanto • Edellä tarkasteltiin osoitetörmäysten hallitsemista ketjutusta käyttämällä, jolloin tallennettavat objektit sijoitettiin hajautustaulun T paikoista 0..m – 1 alkaviin listoihin. • Seuraavaksi selvitetään avainten (yhtä lailla koko objektien) tallentamista itse tauluun T. Mikäli taulun johonkin indeksiin ei ole sijoitettuna yhtään objektia, paikassa on NIL-osoitin. • Jos samaan osoitteeseen yritetään tallentaa useampia kuin yksi alkio, eli ajaudutaan osoitetörmäykseen, joudutaan etsimään vaihtoehtoista paikkaa uuden alkion lisäämiseksi. Sille, miten hakua uuden paikan tallennuspaikan löytämiseksi jatketaan, on olemassa vaihtoehtoisia menetelmiä. Samaa menettelyä sovelletaan myös avainta etsittäessä tai alkiota poistettaessa. • Avointa osoitteenantoa käytettäessä on vaatimuksena, että α ≤ 1. Muutoin kaikki alkiot eivät mahdu käytössä olevaan hajautustauluun. • Jatko-osoitteen hakumenettely: Asetetaan hajautusfunktiolle toinen parametri i, joka ilmaisee jatko-osoitteen järjestysnumeron: 0, 1, 2, …, m – 1. Hajautusfunktio on siis kuvaus laillisten avainarvojen joukon U ja kokeilun järjestysnumeroiden tulojoukolta hajautustaulun indekseille, eli se on muotoa h: U x {0, 1, 2, …, m – 1} {0, 1, 2, …, m – 1} Hajautustaulua T tutkitaan siten järjestyksessä h(k, 0), h(k, 1), h(k, 2), …, h(k, m – 1), joka on yksi taulun T indekseistä koostuvan jonon (0, 1, 2, …, m – 1) permutaatioista. 11.4 Avoin osoitteenanto • Seuraavaksi esitellään avointa osoitteenantoa käyttävää hajautustaulua käsitteleviä algoritmeja. Tarkastellaan ensiksi alkion lisäämistä tauluun T … LISÄÄ_HAJAUTUSTAULUUN(T, k) 1 i := 0 2 REPEAT 3 j := h(k, i) 4 IF T[j] = NIL 5 THEN 6 T[j] := k 7 RETURN j 8 ELSE 9 i := i + 1 10 UNTIL i = m 11 Virheilmoitus ”Hajautustaulu on täynnä – ei voida lisätä.” Algoritmi sijoittaa avaimen k tauluun T ensimmäiseen vapaaseen paikkaan, joka tulee hajautusfunktion palauttamana vastaan matkan varrella. Ellei yhtään paikkaa ole tyhjänä, tulostetaan asiasta kertova virheilmoitus. 11.4 Avoin osoitteenanto • ... ja sitten avaimen k etsimistä taulusta T. ETSI_HAJAUTUSTAULUSTA(T, k) 1 i := 0 2 REPEAT 3 j := h(k, i) 4 IF T[j] = k 5 THEN 6 RETURN j 7 i := i + 1 8 UNTIL T[j] = NIL OR i = m 9 RETURN NIL Algoritmi etsii avainta k taulusta T hajautusfunktion arvojen määräämässä kokeilujärjestyksessä. Suoritus päättyy joko 1) Avaimen löytymiseen jostakin indeksistä kokeilulla i. Jos i > 0 kaikki edellä tutkitut taulun positiot ovat olleet ei-tyhjiä. 2) Osuttaessa vektorin T johonkin tyhjään positioon. Tällöin tiedetään haun epäonnistuneen, sillä avaimen k talletuspaikan etsintäketju on jo katkennut. 3) Laskurin i saavuttaessa arvon m: taulu T täynnä, mutta etsittyä avainta ei löytynyt. 11.4 Avoin osoitteenanto • • • Alkioiden poistaminen osoittautuu lisäystä ja hakua selvästi hankalammaksi. Pitää taata, ettei avaimen tallennus-/hakuketju pääse katkeamaan. siten poistettavan alkion korvaaminen arvolla NIL ei tule kyseeseen Ratkaisuehdotus: käytetään erityistä arvoa TUHOTTU arvon NIL asemesta, kun poistetaan alkioita. Tällöin pitää myös lisäysalgoritmia muuttaa sellaiseksi, että se tallettaa uuden avaimen kohdatessaan joko tyhjän indeksipaikan tai vaihtoehtoisesti tuhotuksi merkityn alkion. Sen sijaan hakualgoritmille ei tarvitse tehdä mitään, sillä se ei reagoi mitenkään tuhottuihin arvoihin, vaan tunnistaa niiden olevan erisuuria kuin etsitty avain. Uusi pulma: Taulun täyttösuhde ei välttämättä pidä enää paikkaansa, sillä tuhotuiksi merkityt arvot pidentävät hakuketjuja. TASAINEN HAJAUTUS • • • • Hajautuksen sanotaan olevan tasainen, jos jokaiselle avaimelle kaikki m! erilaista lukujonon (0, 1, 2, …, m – 1) permutaatiota ovat yhtä todennäköisiä tutkittavia osoitejonoja. Kannattaa huomioida, että edellä tarkasteltu yksinkertainen tasainen hajautus merkitsi sitä, että mikä tahansa arvo 0, 1, 2, …, m – 1 on yhtä todennäköinen hajautusfunktion arvo (yhteen kertaan määrättynä). Sen sijaan tasaisessa hajautuksessa oletetaan, että kukin permutaatio – joita on m! kappaletta – on yhtä todennäköinen tutkittavien osoitteiden järjestys. Tasainen hajautus on lähes mahdoton toteuttaa käytännössä. Esiteltävissä menetelmissä tämä ei toteudu. Tällä kurssilla tarkasteltavalla parhaalla menetelmällä on vain m2 erilaista osoitejonoa (kaksoishajautus) 11.4 Avoin osoitteenanto LINEAARINEN JATKO-OSOITTEEN LASKUMENETTELY • Oletetaan, että h’: U {0, 1, 2, …, m – 1} on jokin hajautusfunktio, ja määritellään h(k, i) = (h’(k) + i) mod m • Idea: osoitteiden kokeilujärjestys hakua, tallennusta ja poistoa varten on T[h’(k)], T[h’(k) + 1], T[h’(k) + 2], …, T[m – 1], T[0], T[1], …, T[h’(k) – 1] • • • • Tässä menetelmässä h’(k) eli ensimmäinen hajautustaulun tutkimiskohta määrää täysin jatko-osoitejonon! Jos vaikkapa ensiksi kokeillaan paikkaa 6, yritetään tämän jälkeen tarvittaessa aina heti seuraavasta paikasta 7 (tai vaihtoehtoisesti paikasta 0, jos m = 7). Mahdollisia jatko-osoitejonoja vain m kappaletta. Kyseessä ei siis ole tasainen hajautus. Menetelmän etu: yksinkertainen toteuttaa. Menetelmän haitta: hajautustaulun ryvästyminen eli klusteroituminen Hajautustauluun syntyy helposti pitkiä varattujen lokeroiden jonoja ⇒ hakuajat pitenevät 11.4 Avoin osoitteenanto KAKSOISHAJAUTUS • Kaksoishajautus on suositeltava menetelmä, jolla saadaan aikaan lähes satunnainen permutaatio h(k, i) = (h1(k) + i ⋅ h2(k)) mod m, missä h1 ja h2 ovat joitain hajautusfunktioita • • • Idea: Tutkitaan ensimmäisellä kierroksella paikasta T[h1(k)]. Seuraavat osoitteet sijaitsevat puolestaan h2:n etäisyydellä toisistaan. Siten h2 esittää hypyn pituutta etsittäessä paikkaa avaimelle k. Erilaisia osoitejonoja muodostuu m2 kappaletta. Jotta koko hajautustaulu tulisi tarpeen vaatiessa käytyä läpi, on oltava voimassa syt(m, h2(k)) = 1. • Muutoin yritettäisiin palata liian aikaisin takaisin jo kertaalleen tutkittuihin osoitteisiin samalla, kun toisaalla taulussa on vielä tutkimattomia paikkoja. Esimerkki: Oletetaan, että m = 13, k = 14, h1(k) = k mod 13 ja h2(k)) = 1 + (k mod 11) Nyt k = 14 ≡ 1 (mod 13) ja k = 14 ≡ 3 (mod 11) Täten h1(14) = 1 ja h2(14) = 1 + 3 = 4. Tallennettaessa avainta k = 14 testataan ensiksi paikkaa 1. Jos se on varattu, jatketaan kokeiluja tästä eteenpäin seuraavassa järjestyksessä: 5, 9, 0, 4, 8, 12, 3, 7, 11, 2, 6, 10. Ellei avainta 14 löydetä viimeistään paikasta 10 (tai tallennettaessa kyseistä avainta ei siihen mennessä ole kohdattu tyhjää paikkaa tai tuhotuksi merkittyä arvoa), haku (lisäys) epäonnistui. 11.4 Avoin osoitteenanto 11.4 Avoin osoitteenanto • • Tarkastellaan toisena esimerkkinä Ässä-arvan voitonjakoa. Yhden arvan hinta on 4 euroa. Voiton suuruus • • Arpojen lukumäärä 100 000 € 5 2 000 € 40 1 000 € 100 500 € 2 000 30 € 30 000 20 € 10 000 10 € 60 000 7€ 300 000 4€ 340 000 Voittojen yhteissumma: 6 840 000 Voittavia arpoja yhteensä: 742 145 Arpoja on painettu yhteensä 3 000 000 kappaletta, joista 742 145 sisältää jonkin voiton. Minkä tahansa voiton osumisen todennäköisyys on siten 742 145/3 000 000 ≈ 0.247 Hieman harvempi kuin joka 4. arpa voittaa (tai antaa ainakin panoksen takaisin). 11.4 Avoin osoitteenanto • Merkitään satunnaismuuttujaa X ilmaisemaan nyt voiton suuruutta euroissa. • Esimerkiksi P(X = 100 000) = 5 / 3 000 000 ≈ 0.0000016 P(X = 7) = 300 / 3 000 000 = 0.1 • Voiton odotusarvoksi E(X) saadaan siten • • • • E(X) = 100 000 € ⋅ (5 / 3 000 000) + 2 000 € ⋅ (40 / 3 000 000) + 1 000 € ⋅ (100 / 3 000 000) + 500 € ⋅ (2 000 / 3 000 000) + 30 € ⋅ (30 000 / 3 000 000) + 20 € ⋅ (10 / 3 000 000) + 10 € ⋅ (60 000 / 3 000 000) + 7 € ⋅ (300 000 / 3 000 000) + 4 € ⋅ (340 000 / 3 000 000) = 6 840 000 € / 3 000 000 = 2.28 € Odotettavissa olevan voiton suuruus on siis 2.28 euroa. MUTTA: yhden arvan ostamiseen tarvitaan 4 euroa. Siten odotettavissa oleva ”voitto” on itse asiassa 2.28 € – 4 € = -1.72 €. Odotettavissa onkin loppujen lopuksi 1.72 euron tappio! Lemma: Olkoon X jokin satunnaismuuttuja, joka voi saada arvoja 1, 2, 3, … . Tällöin E(X) = ∑ ( ≥ ). Todistus: Määritelmän perusteella E(X) = ∑ ( = ) = ∑ (( ≥ ) – ( ≥ + 1)) = (1 ⋅ P(X ≥ 1) – 1 ⋅ P(X ≥ 2)) + (2 ⋅ P(X ≥ 2) – 2 ⋅ P(X ≥ 3)) + (3 ⋅ P(X ≥ 3) – 3 ⋅ P(X ≥ 4)) + … = ∑ ( ≥ ). 11.4 Avoin osoitteenanto • Lause: Avointa osoitteenantoa käyttävässä hajautustaulussa α = m/n < 1. Tällöin epäonnistuneessa haussa tarvitaan keskimääräisessä tapauksessa enintään 1/(1 – α) kokeilua edellyttäen, että hajautus on tasainen. Todistus: Olkoon X satunnaismuuttuja, joka kuvaa tarvittavien kokeilujen lukumäärää (joudutaan tekemään väkisin ainakin yksi kokeilu). Nyt P(X ≥ 1) = 1; P(X ≥ 2) = n/m; P(X ≥ 3) = (n/m) ⋅ ((n – 1)/(m – 1)). Yleisesti P(X ≥ i) = 1 ⋅ (n/m) ⋅ ((n – 1)/(m – 1)) ⋅ ((n – 2)(m – 2)) ⋅ … ⋅ ((n – i + 2)/(m – i + 2)) ≤ (n/m)i – 1 = αi – 1 /* Edellisellä rivillä on alleviivattuja tulotermejä i – 1 kpl. */ Todistuksessa kannattaa huomioida, että (n – 1)/(m – 1) ≤ n/m ⇔ m(n – 1) ≤ n(m – 1) ⇔ mn – m ≤ nm – n ⇔ -m ≤ -n ⇔ n ≤ m. Käyttämällä hyväksi edellä oikeaksi osoitettua lemmaa saadaan: ∑ E(X) = ∑ = α = 1/(1 – α). ( = ) = ∑ ( ≥ ) ≤ ∑ α Huom! Viimeinen yhtäsuuruus nähdään suorittamalla jakolasku 1/(1 – α) = 1 + α + α2 + α3 + α4 + α5 + … Seuraus: Alkion lisääminen tauluun vaatii keskimäärin 1/(1 – α) kokeilua. 11.4 Avoin osoitteenanto 12 Binäärinen hakupuu 12.1 Mikä on binäärinen hakupuu? • Binäärinen hakupuu on tietynlainen binääripuu sille on määritelty ainakin seuraavat operaatiot: 1) avainarvon haku 2) alkion lisääminen 3) alkion poistaminen 4) minimin etsintä 5) maksimin etsintä 6) alkion edeltäjän määrääminen 7) alkion seuraajan määrääminen • Binääripuun solmualkiolla x on olemassa seuraavat attribuutit: avain[x]: esittää solmualkion x avainkentän arvoa vasen[x]: sisältää osoittimen solmun x vasempaan lapsisolmuun oikea[x]: sisältää osoittimen solmun x oikeaan lapsisolmuun vanhempi[x]: sisältää osoittimen solmun x isäsolmuun jos x on puun juuri, on isäosoitin arvoltaan NIL • Lisäksi itse binääripuulle P on määritelty attribuutti: juuri[P]: sisältää osoittimen puun P juuressa sijaitsevaan solmualkioon jos juuri[P] = NIL, kyseessä on tyhjä puu 12.1 Mikä on binäärinen hakupuu? • Binäärinen hakupuu -ominaisuus: Oletetaan, että x on jokin binääriseen hakupuuhun P kuuluva solmu jos jokin toinen puun P solmu y kuuluu x:n vasempaan alipuuhun, niin tällöin on voimassa avain[y] ≤ avain[x] vastaavasti, jos y kuuluu x:n oikeaan alipuuhun, niin avain[y] ≥ avain[x] Esimerkki binäärisestä hakupuusta: 24 16 7 51 19 29 43 12.1 Mikä on binäärinen hakupuu? • Tarkastellaan seuraavassa binääripuuta, jonka korkeus on h: Ensimmäiselle (ylimmälle) tasolle mahtuu 1 = 20 alkiota Toiselle tasolla mahtuu 2 = 21 alkiota Kolmannelle tasolle mahtuu 4 = 22 alkiota … Alimmalle tasolle (lehtiin) mahtuu 2h alkiota • Siten täyteen h-korkuiseen puuhun mahtuu alkioita: n = 20 + 21 + 22 + … + 2h = ∑ 2 = 2h + 1 – 1 kappaletta. • Tällöin 2h + 1 = n + 1 ja h + 1 = log2(n + 1). • Siten täyden binäärisen hakupuun korkeus h ≤ log2n, sillä h = log2(n + 1) – 1 ≤ log2(n + n) – 1 /* Silloin, kun n ≥ 1 */ = log2 (2n) – 1 = log2n + log22 – 1 = log2n + 1 – 1 = log2n. 12.1 Mikä on binäärinen hakupuu? • Seuraavassa esitellään tärkeimmät tavat binäärisen hakupuun solmujen läpikäymiseksi. Oletetaan, että puun solmujen avainarvot halutaan tulostaa. 1) Välijärjestyskulku (solmu käsitellään heti, kun sen koko vasen alipuu on käsitelty, ja ennen oikeaan alipuuhun siirtymistä) Kulje vasen alipuu välijärjestyksessä Vieraile juuressa Kulje oikea alipuu välijärjestyksessä VÄLIJÄRJESTYSKULKU(x) 1 IF x ≠ NIL 2 THEN VÄLIJÄRJESTYSKULKU(vasen[x]) 3 Tulosta(avain[x]]) 4 VÄLIJÄRJESTYSKULKU(oikea[x]) • Välijärjestyksen mukaisesti etenemällä saadaan listattua binäärisen hakupuun solmut kasvavassa (ei-vähenevässä) järjestyksessä. 12.1 Mikä on binäärinen hakupuu? 2) Esijärjestyskulku (solmu käsitellään heti, kun siihen saavutaan ensimmäistä kertaa) Vieraile juuressa Kulje vasen alipuu esijärjestyksessä Kulje oikea alipuu esijärjestyksessä 3) Loppujärjestyskulku (solmu käsitellään vasta, kun sen molemmat alipuut on jo käsitelty) Kulje vasen alipuu loppujärjestyksessä Kulje oikea alipuu loppujärjestyksessä Vieraile juuressa Kohtien 2) ja 3) pseudokoodit voidaan muodostaa helposti kohdan 1) perusteella. Kannattaa huomioida, että triviaalina tapauksena pidetään sitä, kun osoitin x saa arvon NIL. • Lause 12.1: Jos x on n-solmuisen binääripuun juuri, niin kutsun VÄLIJÄRJESTYSKULKU(x) suorittaminen vie ajan Θ(n). Todistus: Välijärjestyskulun suoritusaikaa kuvaa rekursioyhtälö T(n) = T(k) + T(n – k – 1) + d, missä k on vasemman alipuun koko ja d on jokin vakio, joka kuvaa yhden VÄLIJÄRJESTYSKULKU-algoritmin kutsun suorittamiseen menevää aikaa ilman rekursiivisia kutsuja (ts. d kuvaa yksittäisen solmun käsittelyyn kuluvaa aikaa). 12.1 Mikä on binäärinen hakupuu? Induktiolla pystytään helposti osoittamaan, että T(n) = Ο(n): T(n) = T(k) + T(n – k – 1) + d ≤ c1k + c1(n – k – 1) + d = c1n – (c1 – d) ≤ c1n /* Silloin, kun c1 ≥ d */ Toisaalta on myös selvää, että T(n) ≥ c2n jollain vakiolla c2, sillä jokaisessa solmussa joudutaan vierailemaan kertaalleen. Täten T(n) = Θ(n). 12.2 Kyselyt • Seuraavassa tarkastellaan binäärisiin hakupuihin kohdistuvia kyselyalgoritmeja. • Rekursiivisessa hakualgoritmissa muuttuja x toimii osoittimena puun juureen, ja k on etsittävä avain. HAE_BINÄÄRISESTÄ_HAKUPUUSTA(x, k) 1 IF x = NIL OR k = avain[x] 2 THEN RETURN x 3 IF k < avain[x] 4 THEN RETURN HAE_BINÄÄRISESTÄ_HAKUPUUSTA(vasen[x], k) 5 ELSE RETURN HAE_BINÄÄRISESTÄ_HAKUPUUSTA(oikea[x], k) 12.2 Kyselyt • • • Hakualgoritmi voidaan helposti muuntaa iteratiiviseksi: HAE_BINÄÄRISESTÄ_HAKUPUUSTA_ITER(x, k) 1 WHILE x ≠ NIL AND k ≠ avain[x] DO 2 IF k < avain[x] 3 THEN x := vasen[x] 4 ELSE x := oikea[x] 5 RETURN x Ratkaisutavasta riippumatta haun kustannus on Ο(h), missä h on puun korkeus Puussa tarkastellaan joka tasolla yhtä alkiota Parhaassa tapauksessa pelkän juurisolmun tutkiminen riittää (puu tyhjä, tai etsitty alkio löytyy heti juuresta) Pahimmassa tapauksessa kuljetaan jokin polku juuresta aina kaukaisimpaan lehteen asti Minimin etsintä: Ensimmäisessä kutsussa syöteparametrina annetaan osoitin juuri[P]. BINÄÄRISEN_HAKUPUUN_MINIMI(x) 1 IF x ≠ NIL DO 2 WHILE vasen[x] ≠ NIL DO 3 x := vasen[x] 4 RETURN x 5 ELSE virheilmoitus ”Puu tyhjä: minimiä ei ole määritelty.” Algoritmi etsii puusta kaikkein vasemmanpuoleisimman alkion (edetään vasemmalle niin kauan kuin pystytään). 12.2 Kyselyt • Maksimin etsintä: BINÄÄRISEN_HAKUPUUN_MAKSIMI(x) 1 IF x ≠ NIL DO 2 WHILE oikea[x] ≠ NIL DO 3 x := oikea[x] 4 RETURN x 5 ELSE virheilmoitus ”Puu tyhjä: maksimia ei ole määritelty.” Algoritmi etsii puusta kaikkein oikeanpuoleisimman alkion (edetään oikealle niin kauan kuin pystytään). • Edellä esitettyjen minimin- ja maksiminhakualgoritmien kompleksisuus on Ο(h), sillä niissä käydään tarkalleen yksi polku juuresta vasemmalle (tai oikealle) niin pitkälle, kunnes polku päättyy eli vasenta (oikeaa) lapsisolmua ei enää ole olemassa. • Pahimmassa tapauksessa joudutaan etenemään juurisolmusta kaukaisimpaan lehteen asti. • Seuraajan ja edeltäjän etsintä: Seuraajan määritelmä: Jos puun alkiot lajiteltaisiin avainten mukaan ei-vähenevään suuruusjärjestykseen, solmun x seuraaja olisi solmu, joka sisältäisi x:n avainarvoa lähinnä suuremman (tai yhtä suuren) arvon. 12.2 Kyselyt • Solmun x seuraajan hakemisen idea: Jos solmun x oikea alipuu ei ole tyhjä, niin x:n seuraaja on sen oikean alipuun pienin alkio, sillä kaikki x:ää suuremmat sijaitsevat sen oikeassa alipuussa, ja valitaan niistä se, jonka avainarvo on pienin. Jos x:n oikea alipuu on kuitenkin tyhjä, joudutaan puussa nousemaan taso kerrallaan ylöspäin, kunnes kohdataan ensimmäinen solmu y, jonka vasempaan alipuuhun x kuuluu. Kyseisessä alkiossa y on nyt avainarvo, joka on lähinnä avain[x]:ää suurempi, joten y on siten x:n seuraaja. Ellei tällaista alkiota y ole olemassa, seuraajaa ei ole määritelty, vaan tuolloin solmussa x on puun arvoltaan suurin avain, jolloin palautetaan tyhjä osoitin NIL. SEURAAJA_BINÄÄRISESSÄ_HAKUPUUSSA(x) 1 IF oikea[x] ≠ NIL DO 2 THEN RETURN BINÄÄRISEN_HAKUPUUN_MINIMI(oikea[x]) 3 y := vanhempi[x] 4 WHILE y ≠ NIL AND x = oikea[y] DO 5 x := y 6 y := vanhempi[y] 7 RETURN x Algoritmissa edetään solmusta x jotain polkua aina yhteen suuntaan joko alas- tai ylöspäin. ⇒ Aikakompleksisuudeksi saadaan siten Ο(h). • Solmun x edeltäjä määräytyy puolestaan seuraavasti: Jos solmulla x on vasen lapsi, se on solmun edeltäjä. Ellei x:llä ole vasenta lasta, joudutaan puussa nousemaan ylöspäin niin kauan, kunnes kohdataan ensimmäinen solmu y, jonka oikeaan alipuuhun x kuuluu. Kyseisessä alkiossa on nyt avainarvo, joka on lähinnä pienempi kuin x:n avainarvo. Ellei tällaista alkiota y ole olemassa, edeltäjää ei ole määritelty, vaan tuolloin solmussa x sijaitsee arvoltaan puun pienin avain. Tuolloin palautetaan tyhjä osoitin NIL. 12.3 Lisäys ja poisto • • • Tarkastellaan ensiksi alkion lisäämistä binääriseen hakupuuhun. Puuhun lisätään sellainen solmualkio z, jolle on voimassa: avain[z] = v (jokin avaimen tyypin mukainen arvo) vasen[z] = NIL oikea[z] = NIL Lisäysalgoritmin toiminta-ajatus: Etsitään sellainen solmu y, jonka lapseksi lisättävä solmu z avainarvonsa puolesta kelpaa ja lisätään z oikealle paikalleen y:n joko vasemmaksi tai oikeaksi pojaksi. LISÄÄ_BINÄÄRISEEN_HAKUPUUHUN(P, z) 1 y := NIL 2 x := juuri[P] 3 WHILE x ≠ NIL DO 4 y := x 5 IF avain[z] < avain[x] 6 THEN x := vasen[x] 7 ELSE x := oikea[x] 8 vanhempi[z] := y 9 IF y = NIL 10 THEN juuri[P] = z /* Lisäys tapahtuu tyhjään puuhun. */ 11 ELSE IF avain[z] < avain[y] 12 THEN vasen[y] := z 13 ELSE oikea[y] := z Algoritmissa edetään juuresta jokin polku alaspäin. ⇒ Aikakompleksisuudeksi saadaan siten Ο(h). 12.3 Lisäys ja poisto • Seuraavaksi esitetään, miten tapahtuu solmualkion z poistaminen binäärisestä hakupuusta. • Poistossa joudutaan huomioimaan kolme eri tapausta: 1) z on lehtisolmu: z poistetaan pelkästään isäsolmun viittauksia päivittämällä 2) z:lla on vain yksi lapsi: z ohitetaan päivittämällä sen ainoan lapsisolmun ja isäsolmun linkkejä 3) z:lla on molemmat lapset: z:n seuraajalla y ei ole tällöin vasenta lasta (minkä tähden?) z:n seuraaja y on seuraajan määritelmän perusteella nyt selvästikin z:n oikean alipuun minimi, koska z:lla on molemmat lapset talletetaan seuraajan y avainarvo (ja mahdollinen satelliittidata) ja poistetaan seuraajaalkio y kopioidaan seuraajan tiedot poistettavan alkion z tietojen päälle. 12.3 Lisäys ja poisto • Solmun poistamisalgoritmi on esitetty seuraavassa: POISTA_BINÄÄRISESTÄ_HAKUPUUSTA(P, z) 1 IF vasen[z] = NIL OR oikea[z] = NIL 2 THEN y := z 3 ELSE y := SEURAAJA_BINÄÄRISESSÄ_HAKUPUUSSA(z) 4 IF vasen[y] ≠ NIL 5 THEN x := vasen[y] 6 ELSE x := oikea[y] 7 IF x ≠ NIL 8 THEN vanhempi[x] := vanhempi[y] 9 IF vanhempi[y] = NIL 10 THEN juuri[P] = x 11 ELSE IF y = vasen[vanhempi[y]] 12 THEN vasen[vanhempi[y]] := x 13 ELSE oikea[vanhempi[y]] := x 14 IF y ≠ z 15 THEN avain[z] := avain[y] 16 kopioidaan samalla myös y:n mahdollinen satelliittidata solmuun z 17 RETURN y 12.3 Lisäys ja poisto • Miten voidaan arvioida kustannustermin Ο(h) suuruutta? Kuten jo edellä todettiin, täyden binäärisen hakupuun korkeus h ≤ log2n. Pulmallista on, että usein on mahdotonta aavista etukäteen, millaiseksi puun rakenne kehittyy. Varoittava esimerkki: tallennetaan alkiot 11, 15, 24, 29, 39, 71 ja 93 mainitussa järjestyksessä binääriseen hakupuuhun. Saadaan seuraavanlainen hakupuu (syöttöjärjestys määrää yksikäsitteisesti sen, miltä puu tulee näyttämään lisäyksen jälkeen): 11 juuri[P] NIL 15 NIL 24 29 NIL 39 NIL 71 NIL NIL 93 NIL NIL • Hakupuusta muodostuukin juuresta oikealle aukeava lista, jolloin Ο(h) = Ο(n)! Puu pitäisi pystyä jollain tavoin tasapainottamaan, jotteivät operaatioiden suoritusajat heikkenisi lineaarisiksi logaritmisista, jollaiset ne olisivat täydelle puulle. 12.3 Lisäys ja poisto • • • • Algoritmin toimintaperiaate on seuraavanlainen: Riveillä 1 – 3 määräytyy solmu, joka poistetaan puusta fyysisesti Jos poistettavaksi tarkoitetulta solmulta z puuttuu ainakin toinen lapsista, kopioidaan osoitin y osoittamaan solmuun z. Jos z:lla on molemmat lapset olemassa, merkitään y osoittamaan z:n seuraajaa, jolla ei tässä tilanteessa voi olla vasenta lasta. Riveillä 4 – 6 asetetaan x osoittamaan siihen y:n lapseen, joka ei ole NIL (nyt jompikumpi y:n lapsista puuttuu väkisin). Mikäli tällaista solmua ei ole olemassa, tehdään x:stä solmun y oikea lapsi. Riveillä 7 – 13 alkio y poistetaan solmujen vanhempi[y] ja x osoittimia päivittämällä Rivejä 14 – 16 tarvitaan, jos poistettavaksi alkioksi määräytyi y:n seuraaja. Tällöin y:n tietokenttien sisältö kopioidaan solmuun z. Rivillä 17 palautetaan vielä osoitin fyysisesti poistettavaan solmuun y, jotta sille dynaamisesti varattu muistitila voidaan vapauttaa. Alkion poistamisen kompleksisuus on sama kuin lisäämisen, eli Ο(h). Lause 12.2: Operaatioiden haku, minimin etsintä, maksimin etsintä, seuraajan hakeminen ja edeltäjän hakeminen aikakompleksisuus on Ο(h), missä h on binäärisen hakupuun korkeus. joudutaan pahimmassa tapauksessa kulkemaan juuresta kaukaisimpaan lehteen tai päinvastoin. Lause 12.3: Myös operaatioiden lisäys ja poisto aikakompleksisuus on samoin Ο(h). haun osuuden kustannus samatkuin edellä, ja linkkien päivitykset ovat vakioaikaisia
© Copyright 2024