Tietorakenteet ja algoritmit I

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/2n/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 3n/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, …, 2n/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 ≤ 3n/2
2) Jos n on puolestaan pariton, niin vertailuja tehdään:
n/2 + (n/2 – 1) + (n/2 – 1) + 2 = 3n/2
• Kummassakin tapauksessa joudutaan tekemään enintään 3n/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