Tietorakenteet ja algoritmit I TRAI 29.8.2011/SJ Luentomuistiinpanoja



Samankaltaiset tiedostot
Tietorakenteet ja algoritmit I TRAI /SJ Luentomuistiinpanoja

Algoritmit 1. Luento 2 Ke Timo Männikkö

f(n) = Ω(g(n)) jos ja vain jos g(n) = O(f(n))

Algoritmit 2. Luento 1 Ti Timo Männikkö

Tietorakenteet ja algoritmit - syksy

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

3. Laskennan vaativuusteoriaa

Algoritmit 1. Luento 3 Ti Timo Männikkö

Algoritmit 1. Luento 1 Ti Timo Männikkö

Algoritmit 2. Luento 14 Ke Timo Männikkö

4 Tehokkuus ja algoritmien suunnittelu

Ohjelmoinnin perusteet Y Python

Algoritmit 1. Luento 10 Ke Timo Männikkö

58131 Tietorakenteet ja algoritmit (kevät 2014) Uusinta- ja erilliskoe, , vastauksia

1.4 Funktioiden kertaluokat

1. (a) Seuraava algoritmi tutkii, onko jokin luku taulukossa monta kertaa:

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

Diskreetin matematiikan perusteet Laskuharjoitus 2 / vko 9

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

Verkon värittämistä hajautetuilla algoritmeilla

Algoritmit 1. Demot Timo Männikkö

Algoritmit 2. Luento 8 To Timo Männikkö

Algoritmit 1. Luento 10 Ke Timo Männikkö

Algoritmit 2. Luento 7 Ti Timo Männikkö

Luku 6. Dynaaminen ohjelmointi. 6.1 Funktion muisti

811312A Tietorakenteet ja algoritmit, , Harjoitus 3, Ratkaisu

A TIETORAKENTEET JA ALGORITMIT

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

Karteesinen tulo. Olkoot A = {1, 2, 3, 5} ja B = {a, b, c}. Näiden karteesista tuloa A B voidaan havainnollistaa kuvalla 1 / 21

Algoritmit 1. Luento 13 Ti Timo Männikkö

Tietotekniikan valintakoe

Algoritmit 1. Luento 14 Ke Timo Männikkö

Algoritmit 1. Demot Timo Männikkö

Algoritmi on periaatteellisella tasolla seuraava:

Nopea kertolasku, Karatsuban algoritmi

Luku 8. Aluekyselyt. 8.1 Summataulukko

Algoritmit 2. Luento 2 To Timo Männikkö

Algoritmit 2. Luento 2 Ke Timo Männikkö

T Syksy 2004 Logiikka tietotekniikassa: perusteet Laskuharjoitus 7 (opetusmoniste, kappaleet )

Reaalifunktioista 1 / 17. Reaalifunktioista

Tietorakenteet ja algoritmit

7.4 Sormenjälkitekniikka

Ohjelmoinnin perusteet Y Python

A ja B pelaavat sarjan pelejä. Sarjan voittaja on se, joka ensin voittaa n peliä.

Tutkimusmenetelmät-kurssi, s-2004

58131 Tietorakenteet ja algoritmit Uusinta- ja erilliskoe ratkaisuja (Jyrki Kivinen)

12. Javan toistorakenteet 12.1

811312A Tietorakenteet ja algoritmit I Johdanto

58131 Tietorakenteet ja algoritmit (syksy 2015) Toinen välikoe, malliratkaisut

Yhtälönratkaisusta. Johanna Rämö, Helsingin yliopisto. 22. syyskuuta 2014

Matematiikan tukikurssi, kurssikerta 2

Tietorakenteet, laskuharjoitus 2,

Esimerkkejä vaativuusluokista

Algoritmit 2. Luento 13 Ti Timo Männikkö

Määrittelydokumentti

Matematiikan tukikurssi

2.1. Tehtävänä on osoittaa induktiolla, että kaikille n N pätee n = 1 n(n + 1). (1)

Algoritmit 1. Luento 11 Ti Timo Männikkö

Tietorakenteet, laskuharjoitus 7, ratkaisuja

Tietorakenteet, laskuharjoitus 3, ratkaisuja

Datatähti 2019 loppu

Matematiikan tukikurssi, kurssikerta 3

Johdatus lukuteoriaan Harjoitus 2 syksy 2008 Eemeli Blåsten. Ratkaisuehdotelma

Diskreetin matematiikan perusteet Laskuharjoitus 1 / vko 8

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

Johnson, A Theoretician's Guide to the Experimental Analysis of Algorithms.

58131 Tietorakenteet ja algoritmit (syksy 2015)

12. Javan toistorakenteet 12.1

3.3 Paraabeli toisen asteen polynomifunktion kuvaajana. Toisen asteen epäyhtälö

Harjoitustyön testaus. Juha Taina

Vasen johto S AB ab ab esittää jäsennyspuun kasvattamista vasemmalta alkaen:

811312A Tietorakenteet ja algoritmit , Harjoitus 2 ratkaisu

Hahmon etsiminen syotteesta (johdatteleva esimerkki)

(p j b (i, j) + p i b (j, i)) (p j b (i, j) + p i (1 b (i, j)) p i. tähän. Palaamme sanakirjaongelmaan vielä tasoitetun analyysin yhteydessä.

Valitaan alkio x 1 A B ja merkitään A 1 = A { x 1 }. Perinnöllisyyden nojalla A 1 I.

1. Osoita, että joukon X osajoukoille A ja B on voimassa toinen ns. de Morganin laki (A B) = A B.

Joukot. Georg Cantor ( )

Tietorakenteet ja algoritmit II

1 Kannat ja kannanvaihto

Algoritmit 1. Luento 12 Ti Timo Männikkö

Algoritmit 2. Luento 3 Ti Timo Männikkö

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

Tutoriaaliläsnäoloista

Oikeasta tosi-epätosi -väittämästä saa pisteen, ja hyvästä perustelusta toisen.

11. Javan toistorakenteet 11.1

Ohjelmoinnin perusteet Y Python

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

1.4 Funktion jatkuvuus

Approbatur 3, demo 1, ratkaisut A sanoo: Vähintään yksi meistä on retku. Tehtävänä on päätellä, mitä tyyppiä A ja B ovat.

useampi ns. avain (tai vertailuavain) esim. opiskelijaa kuvaavassa alkiossa vaikkapa opintopistemäärä tai opiskelijanumero

Rekursiolause. Laskennan teorian opintopiiri. Sebastian Björkqvist. 23. helmikuuta Tiivistelmä

TIE Tietorakenteet ja algoritmit 1. TIE Tietorakenteet ja algoritmit

= 5! 2 2!3! = = 10. Edelleen tästä joukosta voidaan valita kolme särmää yhteensä = 10! 3 3!7! = = 120

AVL-puut. eräs tapa tasapainottaa binäärihakupuu siten, että korkeus on O(log n) kun puussa on n avainta

Algoritmit 2. Luento 13 Ti Timo Männikkö

A TIETORAKENTEET JA ALGORITMIT

isomeerejä yhteensä yhdeksän kappaletta.

Tietorakenteet ja algoritmit

10. Painotetut graafit

Transkriptio:

Tietorakenteet ja algoritmit I Luentomuistiinpanoja Simo Juvaste Asko Niemeläinen Itä-Suomen yliopisto Tietojenkäsittelytiede

Alkusanat Tämä moniste perustuu valtaosaltaan aiemman Tietorakenteet ja algoritmit -kurssin luentomonisteeseen, joka taas perustui valtaosaltaan Askon mainioon tekstiin. Edellisen version kantava ajatus oli käyttää toteuttamaamme tietrakennekirjastoa konkreettistamaan algoritmejamme ja mahdollistamaan esimerkkien ja harjoitusten suorittaminen tietokoneella. Nyt uudessa tutkintorakenteessa kurssi jakautuu kahteen osaan. Tässä ensimmäisessä osassa keskitytään aikavaativuuden analysointiin, perustietorakenteisiin, joihinkin järjestämisalgoritmeihin sekä perustietorakenteiden toteuttamiseen. Toiseen osaan jäävät aiemmin kurssiin kuuluneet verkkoalgoritmit, algoritmistrategiat, ulkoinen muisti sekä mahdollisuuksien mukaan hieman vaativammat algoritmit. Toinen merkittävä uudistus on ohjelmointikielen vaihtuminen proseduraalisista Pascalista ja C:stä oliopohjaiseen Java 1.5:een. Vaikka kieli ja sen kirjastot vaikeammalta tuntuvatkin, tuovat ne myös paljon hyvää tietorakenteiden käyttäjälle ja toteuttajalle. Itse tietorakenteet ja algoritmit ovat kuitenkin muuttumattomia työvälineen vaihtumisesta huolimatta Simo Juvaste 29.8.2011 Asko Niemeläisen alkuperäiset alkusanat Kokosin nyt käsillä olevat luentomuistiinpanot Joensuun yliopistossa syksyllä 1996 luennoimaani Tietorakenteiden ja algoritmien kurssia varten. Muistiinpanot pohjautuvat vuonna 1993 järjestämäni samannimisen kurssin luentoihin, jotka puolestaan noudattelivat pitkälti Alfred V. Ahon, John E. Hopcroftin ja Jeffrey D. Ullmanin ansiokasta oppikirjaa Data Structures and Algorithms (Addison-Wesley 1983). Kurssin laajuus oli vuonna 1993 vielä 56 luentotuntia, mutta nyttemmin kurssi on supistunut 40 luentotuntia käsittäväksi, minkä vuoksi jouduin karsimaan osan aiemmin järjestämäni kurssin asiasisällöstä. Muutenkaan nämä muistiinpanot eivät enää täysin noudattele mainitsemaani oppikirjaa, sillä käsittelen asiat oppikirjaan nähden eri järjestyksessä, osin eri tavoinkin. Radikaalein muutos aiempaan on se, että pyrin kuvailemaan abstraktit tietotyypit mieluummin käyttäjän kuin toteuttajan näkökulmasta. Lähestymistavan tarkoituksena on johdattaa kurssin kuulijat käyttämään abstrakteja tietotyyppejä liittymän kautta, toisin sanoen toteutusta tuntematta. Toteutuskysymyksiin paneudun vasta kurssin loppupuolella, silloinkin enimmäkseen kuvailevasti. Abstraktien tietotyyppien toteuttamisen ei näet mielestäni pitäisi Ohjelmoinnin peruskurssin jälkeen muodostua ongelmaksi, mikäli tietotyypin käyttäytyminen ja toteutusmallin keskeisimmät ideat ymmärretään. Käyttäjän näkökulman korostaminen on perusteltua myös siksi, että tietorakenne- ja algoritmikirjastojen käytön odotetaan lähivuosina merkittävästi kasvavan. Näiden kirjastojen myötä ohjelmoijat välttyvät samojen rakenteiden toistuvalta uudelleentoteuttamiselta, mutta joutuvat samalla sopeutumaan valmiina tarjolla olevien toteutusten määräämiin rajoituksiin. Tällaisen uuden ohjelmointikulttuurin omaksuminen ei käy hetkessä, vaan siihen on syytä ryhtyä sopeutumaan hyvissä ajoin. Painotan kurssilla algoritmien vaativuusanalyysiä, vaikka hyvin tiedänkin monien tietojenkäsittelytieteen opiskelijoiden aliarvioivan vaativuuden analysoinnin merkitystä. Algoritmiikan tutkimuksessa ei vaativuusanalyysiä voi välttää. Analysointitaitoa on myös helppo hyödyntää jopa hyvin yksinkertaisilta vaikuttavissa ohjelmointitehtävissä. Tahdon lausua kiitokseni kärsivällisille kuulijoilleni, jotka joutuivat keräämään nämä muistiinpanot osa osalta, joskus jopa sivu sivulta, muistiinpanojen valmistumisen myötä. Samoin kiitän perhettäni, joka on muistiinpanojen kirjoittamisen aikana tyytynyt toissijaiseen osaan. Kiitokset myös Tietojenkäsittelytieteen laitoksen kansliahenkilökunnalle sekä Yliopistopainolle tehokkaasta toiminnasta. Erityiset kiitokset ansaitsee vielä muistiinpanot tarkastanut FL Pirkko Voutilainen. Joensuussa 14. elokuuta 1997 Asko Niemeläinen

Sisällysluettelo Luku 1: Algoritmiikasta 1 1.1 Ongelmasta ohjelmaksi 1 1.2 Abstraktit tietotyypit 7 1.3 Suorituksen vaativuus 9 1.4 Suoritusajan laskeminen käytännössä 17 Luku 2: Abstraktit listatyypit 22 2.1 Lista 23 2.2 Pino 29 2.3 Jono 31 2.4 Pakka 32 2.5 Rengas 33 2.6 Taulukko 34 2.7 Yhteenveto 35 Luku 3: Puut 37 3.1 Puiden peruskäsitteistö 37 3.2 Puu abstraktina tietotyyppinä 42 3.3 Binääripuu 45 Luku 4: Joukot 48 4.1 Määritelmiä 48 4.2 Sanakirja 52 4.3 Relaatio ja kuvaus 53 4.4 Monilista 54 4.5 Prioriteettijono 55 4.6 Laukku 57 Luku 5: Verkot 58 Luku 6: Lajittelu 61 6.1 Sisäinen lajittelu 61 6.2 Yksinkertaisia lajittelualgoritmeja 62 6.3 Pikalajittelu (Quicksort) 63 6.4 Kasalajittelu 66 6.5 Lomituslajittelu 66 6.6 Kaukalolajittelu 66 Luku 7: Abstraktien tietotyyppien toteuttaminen 68 7.1 Kotelointi ja parametrointi 68 7.2 Listan toteuttaminen 71 7.3 Listan erikoistapaukset 81 7.4 Puiden toteuttaminen 82 Luku 8: Joukkojen toteuttaminen 85 8.1 Yksinkertaiset joukkomallit 85 8.2 Joukkojen erikoistapaukset 86 8.3 Etsintäpuut 90 8.4 Joukon läpikäynti 93 8.5 Verkot 94 Kirjallisuutta 96

Luku 1 Algoritmiikasta Annetun ongelman ratkaisevan tietokoneohjelman laatiminen on monivaiheinen prosessi, josta valitettavan usein huomataan ainoastaan lopputulos eli valmis ohjelma. Sana "ohjelmointi" luo helposti mielikuvan yksin ohjelmakoodin kirjoittamisesta jättäen huomiotta koko ohjelmointiprosessin kannalta merkittävämmät työvaiheet. Luonnehditaan tämän luvun aluksi näitä työvaiheita, minkä jälkeen esitellään kurssilla tarvittavat perustyövälineet. 1.1 Ongelmasta ohjelmaksi Ongelman ratkaisemiseksi on välttämätöntä ymmärtää, mikä ongelma onkaan tarkoitus ratkaista. Tehtävänmäärittely on monasti kovin ylimalkainen ja epätäsmällinen, joten aivan aluksi on paikallaan määritellä ongelma riittävän yksityiskohtaisesti, jottei ratkaisulle asetetuista vaatimuksista jää epäselvyyttä. Esimerkiksi päällisin puolin yksinkertaiselta näyttävä tehtävä "Laadi ohjelma, joka laskee lukujen summan" on tarkemmin katsoen kovin puutteellisesti määritelty. Määrittelystä ei käy ilmi, minkä tyyppisiä lukuja on tarkoitus summata, kuinka monta lukuja tulee olemaan, mistä summattavat luvut saadaan ja mitä lasketulle summalle tehdään. Tehtävän täsmällisempi määrittely voisi olla vaikkapa "Laadi ohjelma, joka laskee ja tulostaa näppäimistöltä syötettävien kokonaislukujen summan. Syötteet loppuvat, kun näppäillään luku nolla." Kun ongelma on täsmennetty, voidaan ryhtyä suunnittelemaan ratkaisuperiaatetta eli ratkaisualgoritmia. Algoritmi määritellään eri lähteissä toisiinsa nähden hieman eri tavoin. Tällä kurssilla algoritmi tarkoittaa äärellistä käskyjonoa, jonka jokaisella käskyllä on yksiymmärteinen merkitys ja jonka jokainen käsky voidaan suorittaa äärellisellä työmäärällä äärellisessä ajassa. Lisäksi vaaditaan, että algoritmin suorituksen tulee aina päättyä, toisin sanoen algoritmi ei saa koskaan jäädä ikuiseen suoritukseen, ei edes odottamattomia syötteitä saadessaan. Vaatimus suorituksen päättymisestä rajaa joitakin kelvollisia ohjelmia algoritmien joukon ulkopuolelle. Esimerkiksi seuraava Java-menetelmä ei kelpaa algoritmiksi: public static void infinite() { 1 while (true); 2 } 3 1

1. ALGORITMIIKASTA 2 Toisaalta algoritmien joukkokaan ei ole ohjelmien joukon osajoukko, sillä algoritmia ei yleensä esitetä millään ohjelmointikielellä, vaan käytetään enemmän luonnollista kieltä muistuttavaa pseudokieltä. Pseudokielinen algoritmi on yksittäistä ohjelmaa yleisempi, koska pseudokielen yksityiskohdat toteutetaan eri ohjelmointikielillä eri tavoin. Pseudokielen käyttäminen algoritmien esittämisessä on perusteltua siksi, ettei algoritmin suunnitteluvaiheessa välttämättä edes tiedetä, millä kielellä ja millaisessa ympäristössä algoritmi tullaan toteuttamaan. Algoritmin tulisikin aina olla suuremmitta vaikeuksitta toteutettavissa millä tahansa ohjelmointikielellä. Tämä vaatimus on kohtuullinen, sillä rakenteisen ohjelmoinnin perustekniikat peräkkäisyys, toisto ja valinta luontuvat sellaisinaan pseudokieleen ja riittävät minkä tahansa (peräkkäis)algoritmin esittämiseen. Itse asiassa algoritmi edustaa korkeampaa abstraktiotasoa kuin ohjelma. Tällä kurssilla esiteltävien välineiden ja tekniikoiden yhtenä motiivina onkin algoritmien samoin kuin tiedon esittämisen abstraktiotason kohottaminen. Koska esimerkit ja harjoitukset on tarkoitus pystyä myös kääntämään ja suorittamaan, on kuitenkin käytännöllisintä käyttää jotakin ohjelmointikieltä algoritminotaation pohjana. Tällä kurssilla algoritmit esitetään pääosin Java-kielellä, kuitenkin käyttäen abstraktimpaa algoritminotaatiota (pseudokieleltä) apuna selkeyttämään algoritmeja. Ennen kuin algoritmia ryhdytään toteuttamaan eli koodaamaan ohjelmointikielelle, on syytä vakuuttua algoritmin oikeellisuudesta ja tehokkuudesta. Oikeellisuusnäkökohtiin ei tällä kurssilla puututa syvällisesti. Sen sijaan algoritmien tehokkuuden analysointi on yksi tämän kurssin keskeisimmistä aihepiireistä. Jos suunniteltu algoritmi todetaan tehottomaksi, on ratkaistava, kannattaako yrittää löytää tehokkaampi algoritmi, jos sellaista ylipäätään on edes olemassa, vai riittääkö tehotonkin algoritmi ratkaisuksi käsillä olevaan ongelmaan. Algoritmia vastaavaa ohjelmakoodia tuotettaessa täytyy pseudokielen käskyt ja rakenteet muuntaa ohjelmointikieliseen muotoon. Matalan abstraktiotason algoritmin koodaaminen on suoraviivainen tehtävä, mutta mitä korkeammalta abstraktiotasolta lähdetään liikkeelle, sitä monimutkaisempiin kysymyksiin täytyy toteutettaessa löytää vastaukset. Myös tehokkuusnäkökohtia joudutaan usein pohtimaan vielä toteutusvaiheessakin silloin kun tehdään valintoja erilaisten toteutusvaihtoehtojen välillä. Toteutusvaiheessa voidaan soveltaa esimerkiksi asteittaista tarkentamista, jonka avulla ehkä hyvinkin laaja toteutusprosessi kyetään pitämään hallitusti koossa. Algoritmin suunnitteluvaihe on tärkeä myös koodausvaiheen onnistumisen kannalta. Mitä tarkempi algoritmin suunnitelma on, sitä paremmin se ohjaa toteutusta oikeaan suuntaan. Erityisesti tarkan algoritmin pitäisi varmistaa, ettei toteutukseen lipsahda tehottomia osatoteutuksia. Erityisesti käytettäessä valmiita tietorakenne- ja aliohjelmakirjastoja, kuten Java API:a, on vielä varmistuttava niiden operaatioiden toiminnasta ja aikavaativuuksista. Jos valmiiden kirjastojen toteutukset tai operaatiot eivät olekaan aivan yhteensopivia algoritmimme kanssa, voi aikavaativuuteen helposti lipsahtaa kertaluokan lisä. Tälläistä on usein vaikea havaita pienimuotoisessa testauksessa, jolloin tehottomuus voi jäädä ohjelmistoon piileväksi, ja se havaitaan vasta myöhemmin käytettäessä suurempia aineistoja. Tätä ongelmaa pyrimme tällä kurssilla välttämään paneutumalla tarkemmin kirjastojen toteutukseen, sekä jossain määrin kokeellisella aikavaativuuden testaamisella TRA2-kurssilla. Koodauksen valmistuttua on muodostunut ohjelma vielä testattava mahdollisten koodaus- ja muiden virheiden havaitsemiseksi. Jos ongelma on alkujaan määritelty täsmällisesti ja algoritmi todettu oikeelliseksi, ovat mahdolliset virheet syntyneet toteutusai-

1. ALGORITMIIKASTA 3 kana ja ne pystytään toivottavasti korjaamaan käymättä koko raskasta prosessia läpi uudelleen. Toki virheet voivat silti olla hankalasti korjattavia, joten huolellisuus on toteutusvaiheessakin välttämätöntä. Selkeän algoritmin selkeä toteutus johtaa selkeään ohjelmaan, johon ehkä myöhemmin tarvittavien muutostenkin tekeminen onnistuu kohtuullisella työmäärällä, mutta sekavan ohjelman vähäinenkin muuttaminen voi osoittautua hankalaksi tehtäväksi. Joskus on jopa järkevämpää aloittaa työ uudelleen alkutekijöistään. Käytännössä muutostarpeita ilmenee varsin usein, joten muutosten mahdollisuuteen on pyrittävä varautumaan jo algoritmin suunnittelu- ja toteutusvaiheissa. Muutostarpeet aiheutuvat esimerkiksi itse ongelman määrittelyn muuttumisesta tai tehokkuuden lisäämisen vaatimuksesta. Näistä jälkimmäiseen puolestaan voidaan yrittää vaikuttaa joko algoritmia tehostamalla tai toteuttamalla algoritmin kriittiset yksityiskohdat entistä tehokkaammalla tavalla. Kaikkiin muutoksiin on luonnollisesti mahdotonta varautua, mutta huolellisesti rakennetun ohjelman osittainen muuttaminen ei aiheuta koko rakennelman romahtamista. Esimerkki 1-1: Tarkastellaan aiheeseen johdattelevana tehtävänä liikennevalojen vaiheistuksen suunnittelevan ohjelman rakentamista. Ohjelman tarkoituksena on ryhmitellä risteyksessä sallitut ajoreitit siten, että samaan ryhmään kuuluvat ajoreitit eivät leikkaa toisiaan toisin sanoen samassa liikennevalojen vaiheessa sääntöjen mukaisia reittejä ajettaessa ei voi sattua yhteentörmäystä ja että ryhmiä on mahdollisimman vähän, jolloin tarvittavien vaiheiden määrä minimoituu. Ohjelma saa syötteenään mahdolliset ajoreitit ja ohjelma tulostaa reittien optimaalisen ryhmittelyn. Havainnollistetaan ongelmaa kuvan 1-1 esittämällä risteyksellä, jossa kadut A ja C ovat yksisuuntaisia, kadut B ja D puolestaan kaksisuuntaisia. Mahdollisia ajoreittejä on kaikkiaan seitsemän erilaista. Niistä vaikkapa reitit AB ja DC voidaan ajaa samanaikaisesti, mutta reittien AC ja DB yhtäaikainen käyttäminen aiheuttaa yhteentörmäyksen vaaran. A A D B D * B C C Kuva 1-1: Katujen risteys Kuvataan ongelma verkkona eli graafina, joka koostuu joukosta solmuja ja joukosta näitä solmuja yhdistäviä kaaria. Verkkojen käsitteistö esitellään tarkemmin luvussa 5 ja TRA2-kurssilla. Esittäkööt solmut ajoreittejä ja olkoon verkossa kaari kahden solmun välillä vain siinä tapauksessa, ettei näitä kahta reittiä voida ajaa samanaikaisesti. Kuvan 1-1 risteystä vastaava verkko nähdään kuvassa 1-2. Taulukko 1-3 esittää saman verkon toisessa muodossa, taulukkona, jossa ykköset ilmaisevat kaaren olemassaolon ja tyhjät alkiot kaaren puuttumisen. Näistä esitysmuodoista kuva 1-1

1. ALGORITMIIKASTA 4 AB AC AD BC BD DB DC Kuva 1-2: Yhteentörmäävien reittien verkko. on ilman muuta ihmiselle ymmärrettävin ja taulukko 1-3 puolestaan tietokoneelle ymmärrettävin. Kuvan 1-2 esitys ei ole paras mahdollinen kummallekaan, mutta ongelmaa verkkona tarkasteltaessa se antaa tyhjentävän kuvan tilanteesta. Taulukko 1-3: Verkon matriisiesitys. AB AC AD BC BD DB DC AB 1 1 AC 1 1 AD BC 1 1 BD 1 1 DB 1 1 DC Väritetään nyt verkon solmut niin, ettei minkään kaaren molemmissa päissä käytetä samaa väriä. Alkuperäinen ongelma on ratkaistu, kun löydetään pienin määrä värejä, jolla verkon kaikki solmut saadaan väritetyksi rikkomatta väritysehtoa. Tällöin keskenään samanvärisiä solmuja vastaavat ajoreitit voidaan ajaa yhtaikaa eli ne muodostavat yhden vaiheen. Ohjelman tuloste saadaan suoraan solmujen värien mukaisesta ryhmittelystä. AB AC AD BC BD DB DC Kuva 1-4: Eräs mahdollinen ryhmittely.

1. ALGORITMIIKASTA 5 Ratkaisun keskeinen idea on siis muodostaa syötettä vastaava verkko, etsiä verkon optimaalinen väritys ja palauttaa värityksen tulos varsinaisen ongelman ratkaisuksi. Idean toteuttaminen ei valitettavasti ole aivan yksinkertaista. Miten esimerkiksi etsitään yhteentörmäyksen aiheuttavat reittiparit, kun syötteenä annetaan vain sallitut reitit? Tämä osaongelma voidaan onneksi ratkaista kohtuullisella työmäärällä (miten?), mutta väritysongelma osoittautuu erittäin vaikeaksi: kyseessä on niin sanottu NP-täydellinen ongelma, joka ei ratkea polynomisessa ajassa! Tämä algoritmitutkimuksen teoreettinen tulos on nyt hyödyllinen, koska sen ansiosta vältytään tuhlaamasta aikaa tehokkaan algoritmin turhaan etsimiseen tehokasta algoritmiahan ei ole olemassakaan. Minimaalisen värityksen tuottava tehoton algoritmi toki löytyy (millainen?), mutta sen asemesta lienee hyödyllisempää yrittää löytää heuristinen algoritmi, joka tuottaa nopeasti lähes optimaalisen värityksen, muttei välttämättä parasta väritystä. Hyvällä onnella heuristisen algoritmin antama tulos on jopa yhtä hyvä kuin optimaalinen tuloskin, eikä tulos huonommassakaan tapauksessa toivottavasti ole aivan surkea. Varsin kelvollinen heuristiikka verkon väritysongelmaan on aloittaa värittämällä yhdellä värillä niin monta solmua kuin väritysehtoa rikkomatta on mahdollista, jatkaa värittämällä toisella värillä jäljelle jääneistä solmuista niin monta kuin väritysehtoa rikkomatta on mahdollista ja niin edelleen, kunnes kaikki solmut on väritetty. Tässä on kyseessä niin sanottu ahne menetelmä, joka ei ota huomioon väritettävän verkon erityispiirteitä, vaan käsittelee verkosta kerrallaan niin suuren osan kuin suinkin pystyy. Verkon rakenteen lisäksi ahneen menetelmän antamaan tulokseen voi vaikuttaa se, mistä solmusta värittäminen aloitetaan, sekä se, missä järjestyksessä vielä värittämättömät solmut käydään läpi. Onkin helppo nähdä, ettei ahneen algoritmin tuottama tulos aina ole optimaalinen edes yksinkertaisen verkon tapauksessa. Eräs ahneen menetelmän tuottama kuvan 1-2 verkon väritys nähdään taulukossa 1-5 (joka vastaa kuvan 1-4 väritystä). Kyseinen verkko värittyy kolmella värillä, kun tarkastelu aloitetaan solmusta AB ja solmut käydään läpi kuvan 1-2 mukaisessa järjestyksessä vasemmalta oikealle ja ylhäältä alas. Voidaan jopa osoittaa, ettei tämän verkon värittäminen onnistu ainakaan vähemmällä kuin kolmella värillä: solmut AB, BC, DB, AC ja BD muodostavat niin sanotun kehän eli nämä solmut yhdistyvät kaarten välityksellä yksinkertaiseksi renkaaksi, ja koska tässä renkaassa on solmuja pariton määrä, tarvitaan sen värittämiseen kolme väriä. Koska kolmivärinen ratkaisu löytyi, on ongelma ratkennut optimaalisesti: kuvan 1-1 liikennevaloihin tarvitaan kolme vaihetta, yksi kutakin taulukossa 1-5 samalla värillä väritettyä reittijoukkoa kohden. Muitakin vaiheiden määrään nähden yhtä hyviä ratkaisuja on olemassa. Taulukko 1-5: Eräs mahdollinen ryhmittely. Väri sininen punainen vihreä Reitit AB, AC, AD, DC BC, BD DB

1. ALGORITMIIKASTA 6 Edellisessä esimerkissä nähtiin algoritmien suunnittelussa usein käytetty lähestymistapa, jossa ongelma muunnetaan toiseksi ongelmaksi, jonka ratkaisumenetelmä tunnetaan. Näin saatu ratkaisu on lopuksi osattava palauttaa alkuperäisen ongelman ratkaisuksi. Kyse on siis reaalimaailman ongelman abstrahoimisesta algoritmisesti käsiteltäväksi ongelmaksi, algoritmisen ongelman ratkaisusta ja ratkaisun muuntamisesta takaisin reaalimaaliman käsitteisiin. Jatketaan äskeisen ahneen menetelmän tarkastelua pseudokielen tasolla. Pseudokielen käskyiltä ja rakenteilta ei vaadita täsmällistä muotoa, vaan asiat ilmaistaan kulloinkin tarkoituksenmukaisella tarkkuudella. Liiallista ohjelmointikieleen tai tiettyyn toteutukseen johdattelevaa tarkkuutta on vältettävä, koska liiallisessa tarkkuudessa vaanii vaara abstraktion katoamisesta, mikä puolestaan voi estää hyvän lopputuloksen muotoutumisen. Esimerkki 1-2: Olkoon G verkko, jonka solmuista osa on ehkä jo väritetty. Seuraava algoritmi greedycolor värittää uudella värillä sellaiset solmut joiden värittäminen ei riko väritysehtoa. public static void greedycolor(graph G, Color newcolor) { 1 for each uncolored vertex v of G { 2 if (v not adjacent to any vertex with color newcolor) 3 v.setcolor(newcolor); 4 } 5 public static int greedycolorstart(graph G) { 6 mark all vertices non-colored; 7 int numofcolors = 0; 8 while (not all vertices colored) 9 greedycolor(g, ++numofcolors); 10 return numofcolors; 11 } 12 Väritysongelma ratkaistaan suorittamalla greedycolor-algoritmia toistuvasti (yllä greedy- ColorStart, rivit 9-10), kunnes verkon kaikki solmut on väritetty, ja laskemalla samalla algoritmin suorituskertojen lukumäärä. Algoritmin ensimmäinen versio sisältää monia vielä tarkennettavia kohtia, kuten tyyppien Graph ja Vertex määrittelyn, joukkomuuttujan tyhjäksi alustamisen, solmujoukon yli toistamisen ja solmujen välisen naapuruuden tutkimisen. Tarkennetaan näistä rivin 2 toisto käyttämällä Java:n kokoelman yli toistoa sekä ohittamalla jo väritetyt solmut. Tarkennetaan samoin rivin 3 ehtolause käymällä läpi solmun v naapurisolmut ja tutkimalla, onko yksikään niistä jo väritetty nyt käytössä olevalla värillä. Ellei näin ole, voidaan solmu v nyt värittää. Tarkennettuna algoritmi näyttää seuraavanlaiselta: public static void greedycolor(graph G, Color newcolor) { 1 for (Vertex x : G.vertices()) { 2 if (x.getcolor()!= nocolor) 3 continue; 4

1. ALGORITMIIKASTA 7 boolean found = false; 5 for (Vertex w : v.neighbors()) { 6 if (w.getcolor() == newcolor) 7 found = true; 8 } 9 if (! found) 10 v.setcolor(newcolor); 11 } 12 } 13 Vastaavasti olisi tarkennettava algoritmin käynnistysaliohjelmaa greedycolorstart. Jotta ratkaisusta saataisiin todella tehokas, on syytä ennen toistojen tarkentamista tarkistaa, miten verkkotyyppi on toteutettu. Se puolestaan edellyttää TRA2 kurssilla nähtävien tietorakenteiden tuntemusta, joten päätetään esimerkin käsittely tähän. 1.2 Abstraktit tietotyypit Esimerkeissä 1-1 ja 1-2 käytetty asteittaisen tarkentamisen idea on tuttu jo aiemmilta ohjelmointikursseilta, mutta verkko- ja joukkotyyppien toteuttamiseen ei muilla kursseilla ole vielä paneuduttu. Kaikkia näitä monimutkaisia tietotyyppejä ei Java-kirjastoon sisälly valmiina. Erityisesti Java-kirjaston kokoelmat ovat jossain määrin rajoittuneempia kuin mitä tällä kurssilla ajatellaan. Algoritmin suunnittelun kannalta olisi hyödyllistä jos esimerkiksi voitaisiin käyttää käsitettä "joukko" sekä tavanomaisia joukko-operaatioita kuten "yhdiste" ja "leikkaus" ikään kuin ne olisivat todella olemassa. Näin johdutaan abstrakteihin tietotyyppeihin, jotka ovat tiedon esittämisen ja käsittelyn malleja. Abstraktin tietotyypin määrittelyssä kuvataan aina kokoelma operaatioita, joilla tietotyypin esittämää tietoa käsitellään. Tämän operaatiokokoelman kuvaus muodostaa abstraktin tietotyypin liittymän. Liittymä yksin määrää sen, miten abstraktia tietotyyppiä saadaan käyttää. ADT voidaan ajatella kokoelmien hallinnan apuvälineenä. Hyötytietoelementit ADT Hyötytieto Kuva 1-6: ADT kokoelman ylläpidon apuvälineenä. "ripustetaan" ADT:n ylläpidettäväksi, jolloin meidän ei tarvitse huolehtia kokoelman ylläpitämisestä, vaan voimme keskittyä itse elementteihin liittyvään tietojenkäsittelytehtävään. Kuva 1-6 esittää ADT:n ja hyötytiedon suhdetta. ADT:n toteutusrakenne (kuvassa neliöt katkoviivan sisällä) on käyttäjän kannalta (lähes) yhdentekevä. Parhaimmillaan Looginen järjestys

1. ALGORITMIIKASTA 8 unohdamme koko ADT:n ja käsittelemme hyötytietoa (elementtejä) kuten ne itse osaisivat säilyttää itsensä ja järjestyksensä. Useimmiten tehtävämme elementeillä on jokin looginen järjestys jonka mukaan haluamme niitä käsitellä. Erilaisia järjestystarpeita varten määrittelemme erilaisia ADT:tä. Kuhunkin tarpeeseen on osattava valita oikeanlainen abstrakti tietotyyppi. Esimerkki 1-3: Joukko (abstraktina tietotyyppinä) on kokoelma keskenään samantyyppisiä alkioita, vaikkapa verkon solmuja. Joukkomallille tyypillinen piirre on se, että sama alkio voi sisältyä joukkoon vain yhtenä esiintymänä kerrallaan. Joukkoja käsitellään esimerkiksi muodostamalla kahden joukon yhdiste tai tutkimalla, kuuluuko jokin alkio joukkoon. Joukkotyypin liittymä voi sisältää vaikkapa seuraavankaltaisen osan: // returns union of this set and set B 1 public static Set<E> union(set<e> B); 2 // returns whether object x is a member of this set or not 3 public static boolean member(<e> x); 4 Tässä esiintyvä tyyppi <E> on joukon alkioiden tyyppi, joka luonnollisesti on eri ohjelmissa erilainen. Joukon alkiothan voivat itsekin olla joukkoja, kuten on laita esimerkiksi potenssijoukkojen tapauksessa. Liittymä antaa abstraktin tietotyypin käyttäjälle kaiken tarpeellisen tiedon tyypin käyttämiseksi, nimittäin sallitut operaatiot parametreineen ja tulostyyppeineen. Lisäksi liittymässä tulee mainita operaatioiden oikeellista käyttöä mahdollisesti rajoittavat ehdot. Jos käyttäjä noudattaa näitä ehtoja, on hänellä oikeus odottaa operaatioiden toimivan asianmukaisella tavalla. Ehtojen vastainen käyttö puolestaan voi johtaa virhetilanteeseen tai aiheuttaa muuten kummallisen toiminnan. Edelleen liittymän kuvauksen tulisi kertoa kunkin operaation aika- ja tilavaativuus. Java-kirjastojen dokumentaatiossa tämä on useimmiten kerrottu implisiittisesti tai ei lainkaan, mikä onkin niiden ehkä suurin puute. Abstrakti tietotyyppi voitaisiin määritellä aksiomaattisesti, jolloin liittymä ehtoineen saataisiin miltei sellaisenaan tietotyypin määrittelystä. Sivuutetaan tällä kurssilla aksiomaattinen lähestymistapa ja tarkastellaan abstrakteja tietotyyppejä pikemminkin intuitiivisesti. Olkoon tarkastelukulma mikä hyvänsä, on selvää, ettei pelkkä liittymä vielä mahdollista abstraktin tietotyypin konkreettista käyttämistä, vaan käyttämisen edellytyksenä on tietotyypin toteuttaminen. Toteutus voi pohjautua toisiin abstrakteihin tietotyyppeihin, jotka on edelleen toteutettava kukin erikseen. Lopulta toteutus palautuu ohjelmointikielen tarjoamiin valmiisiin välineisiin. Toteutus sisältää ainakin liittymässä kuvattujen operaatioiden ohjelmakoodin sekä abstraktia mallia vastaavan todellisen tietorakenteen määrittelyn. Toteutus on usein operaatioiltaankin liittymässä kuvattua laajempi, koska esimerkiksi todellisen tietorakenteen käsittelemiseksi saatetaan tarvita välineitä, joista käyttäjän ei tarvitse tietää mitään. Käyttäjä ei luonnollisesti näe todellista tietorakennettakaan, vaan käyttäjän mielikuvassa abstrakti tietotyyppi on sellaisenaan olemassa. Vastaavasti toteuttaja ei tiedä, millaisiin sovelluksiin toteutusta tullaan käyttämään, vaan ainoastaan sen, millaisia operaatioita käyttäjät tuntevat. Tällainen abstraktin tietotyypin koteloinnin idea helpottaa sekä käyttäjän että toteuttajan työtä. Käyttäjä näet välttyy toteutuksen yksityiskohtiin tutustumiselta ja voi sen sijaan paneutua tehokkaammin varsinai-

1. ALGORITMIIKASTA 9 sen ongelmansa ratkaisuun. Toteuttaja puolestaan voi keskittyä etsimään tehokasta toteutusta liittymän kuvaamalle abstraktille mallille. Kotelointi helpottaa myös tietotyypin toteutuksen muuttamista, jos se on tarpeen. Toteutusta voidaan näet muuttaa miten hyvänsä ilman, että käyttäjän tarvitsee edes tietää muutoksista, kunhan liittymä säilyy ennallaan. Toisaalta saman tietotyypin eri toteutuksia kyetään vaivattomasti vaihtelemaan esimerkiksi empiirisessä tutkimustyössä. Tähän saakka on käytetty tietotyypin ja tietorakenteen käsitteitä esittämättä niiden täsmällistä merkitystä. Määritellään nyt nämä kaksi käsitettä: Määritelmä 1-4: Muuttujan tietotyyppi on kyseisen muuttujan sallittujen arvojen joukko. Esimerkki 1-5: Kokonaislukujen tyyppi sisältää periaatteessa äärettömän monta arvoa: 0, 1, 1, 2, 2, 3, 3, Käytännössä tietokoneen sananpituus rajaa mahdollisten arvojen joukon aina äärelliseksi. Esimerkiksi 32 bitillä voidaan esittää 4294967296 eri lukua. Pascal-kielen tyypin set of 0..9 arvoja ovat joukot { }, {0}, {1}, {2}, {3},, {9}, {0, 1}, {0, 2},, {8, 9}, {0, 1, 2},, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Kaikkiaan näitä arvoja on 2 10 eli 1024 erilaista. Boolen tyypissä erilaisia arvoja on vain kaksi, false ja true. Määritelmä 1-6: Tietorakenne on kokoelma toisiinsa kytkettyjä muuttujia. Kyseessä on siis ohjelmointikielen rakenne josta bitit voidaan periaatteessa piirtää näkyviin. Joskin Javan tapauksessa bittien täsmällinen piirtäminen vaatisi hieman enemmän paneutumista Javan toteutukseen. Pelkkä kielen dokumentaatio ei riitä. Esimerkki 1-7: Java-kielen taulukot, tietueet ja tiedostot ovat tietorakenteita samoin kuin viitteiden avulla toisiinsa kytkettyjen dynaamisten muuttujien muodostamat monimutkaisemmatkin rakenteet. Ääritapauksessa yhtäkin muuttujaa voidaan sanoa tietorakenteeksi tällainen on esimerkiksi yhden kokonaisluvun sisältävä tietue. Abstraktin tietotyypin kuvaaman mallin toteutuksessa määritellään aina jokin todellinen tietorakenne. Käyttäjä ei näe tämän todellisen rakenteen sisältöä, vaan abstraktin mallin mukaisia arvoja, esimerkiksi tyhjän joukon. Toteuttaja puolestaan näkee todellisen rakenteen kaikki osaset, kuten esimerkiksi muuttujien välisten kytkentöjen toteuttamisessa käytettävät osoittimet. Toteuttajan on silti kyettävä hahmottamaan myös se abstraktin mallin mukainen arvo, jota todellinen rakenne kuvaa. Tietorakenteiden ja tietotyyppien käsittelyyn sekä niiden määrittelytapoihin palataan esimerkkien kera luvussa 2. Samalla pohditaan miten abstrakteja rakenteita pitää konkretisoida jotta ne olisi toteutettavissa oikeilla ohjelmointikielillä. 1.3 Suorituksen vaativuus Kun saman ongelman ratkaisemiseksi tunnetaan useita erilaisia algoritmeja, on näistä osattava valita kulloiseenkin sovellukseen tarkoituksenmukaisin. Valinta perustuu usein seuraaviin kahteen kriteeriin:

1. ALGORITMIIKASTA 10 1) Algoritmin tulee olla helppo ymmärtää ja toteuttaa. 2) Algoritmin tulee olla tietokoneen muistitilan ja ajankäytön suhteen tehokas. Ikävä kyllä nämä kriteerit ovat usein keskenään ristiriidassa, sillä ihmisen ymmärrettäväksi helppo algoritmi saattaa olla kovin hidas, mutta tehokkaan algoritmin toiminnan ymmärtäminen voi tuottaa suuria vaikeuksia, toteuttamisen vaikeuksista puhumattakaan. Jollei sama algoritmi täytä molempia kriteerejä, kannattaa valinta tavallisesti perustaa käyttötarpeeseen. Muutaman kerran käytettävältä ohjelmalta ei ole järkevää vaatia äärimmäistä tehokkuutta, kun taas toistuvasti tarvittavan ohjelman on paras olla tehokas. Samoin odotettavissa oleva syötteen koko vaikuttaa algoritmin valintaan. Jos voidaan esimerkiksi varmasti ja perustellusti olla varmoja, ettei syötteen koko koskaan tule olemaan enempää kuin muutamia satoja, eikä sekunti suoritusaikana ole liikaa, meille riittää hieman hitaampi algoritmi, eikä ole perusteltua tuhlata aikaa tehokkaamman mahdollisen algoritmin toteuttamiseen. Yleisesti kuitenkin meidän voi olla vaikea nähdä kaikkia mahdollisia tulevaisuudessa käytettäviä syötekokoja. Algoritmien tehokkuutta arvioitaessa käytetään erilaisia vaativuuden käsitteitä. Käytännössä useimmin tarkastellaan algoritmin aikavaativuutta eli arvioidaan algoritmin suoritusaikaa. Joskus ollaan kiinnostuneita myös tilavaativuudesta, jolloin arvioidaan algoritmin suorituksen vaatiman aputilan tarvetta. Aputila tarkoittaa tässä algoritmin syötteiden ja tulosteiden tallettamisen lisäksi tarvittavaa muistitilaa. Joskus joudutaan paneutumaan myös laitteistovaativuuteen, kuten esimerkiksi selvitettäessä, kuinka monta ulkoista muistivälinettä algoritmin tehokas toteuttaminen edellyttää tai kuinka nopeasti tietoja on pystyttävä vaihtamaan eri laitteiden kesken. Muisti- ja muut resurssit ovat tosin nykyisin yhä harvemmin tehokkuuden pullonkauloja. Tällä kurssilla arvioidaan lähinnä aikavaativuutta, mutta paikoin tarkastellaan muitakin vaativuuskysymyksiä. Aikavaativuutta arvioitaessa on tärkeä ymmärtää, että vaativuudeltaan erilaistenkin algoritmien todellisten suoritusaikojen erot ovat merkityksettömiä pienten ongelmien käsittelyssä. Vasta kooltaan suuret ongelmat paljastavat algoritmin hitauden. Erot voivat tällöin olla dramaattiset. Ongelman pienuus ja suuruus puolestaan ovat suhteellisia käsitteitä. Esimerkiksi lajitteluongelma on pieni, kun lajiteltavana on kymmeniä alkioita, mutta joillekin verkkoalgoritmeille kymmensolmuinen verkkokin voi olla suuri käsiteltävä. Ohjelman suoritusaikaa voitaisiin mitata kellolla. Tällainen mittaaminen ei kuitenkaan tuottaisi vertailukelpoisia tuloksia, koska kulloiseenkin suoritukseen kuluvan ajan pituuteen vaikuttavat monet sellaiset tekijät, joita on vaikea tai jopa mahdoton kontrolloida. Näitä ovat esimerkiksi käytetty kääntäjä ja laitteisto sekä laitteiston hetkelliset kuormituserot. Absoluuttisen suoritusajan mittaamisen asemesta onkin mielekkäämpää arvioida suhteellista suoritusaikaa, jonka arviointi onnistuu paitsi ohjelmille, myös algoritmeille. Tämä on algoritmin suunnittelijan kannalta merkittävä etu, koska sen ansiosta voidaan keskenään vertailla vielä toteuttamattomiakin algoritmeja ja siten välttää tehottomaksi todetun algoritmin toteuttamisesta aiheutuva turha työ. Suoritusajan yksikkönä käytetään joustavaa termiä "askel". Kuva 1-7 havainnollistaa erään tulkinnan, myöhemmin tarkennamme käsitettä edelleen. Suoritusaika suhteutetaan tavallisesti algoritmin syötteen kokoon. Syöte on tässä ymmärrettävä laajasti: algoritmi voi saada syötteensä suoraan käyttäjältä, tiedostoista tai vaikkapa parametrien välityksellä. Joissakin tapauksissa varsinaista syötettä ei ole lain-

1. ALGORITMIIKASTA 11 a = 1; 1 b = 3; 2 for (i = 0; i < 1000; i++) 1 a = a + 1; 2 Kuva 1-7: Suoritusaskel. (n.) kaksi askelta (n.) 1000 askelta kaan olemassa. Silloin suorituksen kesto määräytyy esimerkiksi käytettyjen vakioiden arvoista. Satunnaisuuteen perustuvien algoritmien aikavaativuuden arvioinnissa puolestaan sovelletaan todennäköisyyslaskennan menetelmiä (satunnaisalgoritmeja käsitellä lyhyesti TRA2 kurssilla). Syötteestäkään eivät kaikki osat aina ole aikavaativuutta arvioitaessa mielenkiintoisia, kuten seuraavassa esimerkissä todetaan: Esimerkki 1-8: Lukekoon algoritmi ensiksi sata kokonaislukua ja sen jälkeen vielä mielivaltaisen määrän muita syötteitä, joihin kaikkiin sovelletaan jotakin sadan ensimmäisen syötteen määräämää laskentaa. Koska algoritmin jokaisella suorituskerralla luetaan mainitut sata lukua, ei niiden lukeminen vaikuta suhteelliseen suoritusaikaan. Sen sijaan syötteen loppuosan vaikutus on olennainen: viisi syötettä luetaan ja käsitellään varmasti nopeammin kuin miljoona syötettä. Siirräntään kuluvaa aikaa ei vaativuusanalyysissä useinkaan oteta lukuun, koska siirrännän nopeuteen vaikuttavat algoritmista riippumattomat tekijät. Syötteen oletetaankin tavallisesti olevan jollakin tavoin valmiina saatavilla, esimerkiksi lajiteltavien alkioiden taulukossa, josta ne saadaan vaivattomasti esiin. Tällä oletuksella ei yleensä ole merkitystä suoritusaikaa määrättäessä, mutta joskus syötteiden käsittelyyn kuluva aika on erityisesti huomattava ottaa aika-arvioon mukaan. Näin on laita seuraavan esimerkin tapauksessa: Esimerkki 1-9: Algoritmi lukee kokonaislukuja, kunnes syötteenä annetaan sama luku viisi kertaa peräkkäin. Lopuksi algoritmi tulostaa neljännen lukemistaan luvuista. Mikä on algoritmin suoritusaika? Koska algoritmi tulostaa neljännen syötteensä, joka saadaan selville, kun on ensin luettu kolme muuta syötettä, syntyy helposti käsitys, että algoritmin suoritukseen kuluva aika on "4" eli vakio. Tämä käsitys paljastuu asiaa tarkemmin pohdittaessa virheelliseksi: Algoritmin suoritus päättyy vasta sitten, kun kaikki syötteet on saatu luetuksi. Mielivaltaista määrää kokonaislukuja ei mitenkään pystytä lukemaan vakioajassa, vaan aikaa kuluu sitä enemmän, mitä useampia lukuja algoritmille syötetään. Sen vuoksi suoritusajan määrääkin nyt syötteen lukemiseen kuluva aika, toisin sanoen syötteen todellinen koko. Syötteen tai sen vastineen suoritusajan arvioimisen kannalta merkityksellisen osan tunnistamista varten ei voida antaa täsmällisiä ohjeita. Taito tähän tunnistamiseen kehittyy harjoituksen myötä. Varsin usein tunnistaminen itse asiassa onkin triviaali tehtävä. Syötteen koko saattaa vaihdella suorituskerrasta toiseen, mutta aikavaativuus tulisi pystyä arvioimaan yleisesti algoritmin mielivaltaiselle suoritukselle. Sen vuoksi suoritusaika esitetään syötteen koon funktiona. Jos syötteen kokoa merkitään n:llä, on luontevaa

1. ALGORITMIIKASTA 12 käyttää suoritusaikafunktiolle merkintää T(n). Näinollen askelten määrä ilmaistaan syötteen funktiona, esimerkiksi: Kuten jo merkinnöistäkin nähdään, tarkastellaan yleensä aina pahinta tapausta, ellei erityisesti mainita jostakin muusta tapauksesta. Paras tapaus ei useinkaan ole edes mielenkiintoinen. Esimerkiksi lajittelussa paras tapaus voisi olla valmiiksi lajiteltu kokoelma. Tämä tuskin kertoo lajittelualgoritmin hyvyydestä mitään. Keskimääräisen suoritusajan arviointi saattaa puolestaan osoittautua erittäin hankalaksi tehtäväksi, koska kaikki samankokoiset syötteet voidaan vain harvoin olettaa keskenään yhtä todennäköisiksi. Pahimman tapauksen tarkka analysointikin voi tosin joskus olla vaivalloista. Jos esimerfor (i = 0; i < n; i++) 1 a = a + 1; 2 (n.) n askelta Mikäli syöte koostuu useista toisistaan riippumattomista osasista, jotka kaikki ovat aikaarvioiden kannalta merkittäviä, käytetään syötteen koon kuvaamiseen useita muuttujia ja vastaavasti suoritusaikakin ilmaistaan usean muuttujan funktiona. Aina on muistettava varmistaa, että suoritusaikafunktiossa käytettävien tunnusten (esim. n) merkitys on selkeä, eli kertoa mitä syötteen ominaisuutta ne kuvaavat. Vastaavasti, jos annetun ohjelmanosan syötteen kokoa merkitään jollain muulla kirjaimella, myös aikavaativuusfunktio annetaan sitä käyttäen. Jos sitten muuttuja korvataan toisella, muutos on tehtävä myös aikavaativuusfunktioon. Esimerkki 1-10: Suoritusaikafunktio T(n) = cn 2 + b, missä b ja c ovat vakioita, ilmaisee suoritusajan olevan neliöllinen suhteessa syötteen kokoon n. Tämä merkitsee, että syötteen koon kymmenkertaistuessa suoritusaika suurin piirtein satakertaistuu. Esimerkki 1-11: Esimerkiksi merkkijonon etsinnän toisesta merkkijonosta vaativuus riippuu sekä etsittävästä avaimesta, että läpikäytävästä kohdetekstistä. Kuvataan avaimen pituutta m:llä ja kohdetekstin pituutta n:llä. Erään yksinkertaisen etsintäalgoritmin suoritusaikafunktio on T(n, m) = cnm, missä c on vakio. Funktion T mittayksikköä ei kiinnitetä. Voidaan ajatella, että lasketaan algoritmin suorittamien käskyjen tai muiden keskeisten perustoimintojen lukumäärä. Taito nähdä, minkä toimintojen lukumäärä kulloinkin on mielekästä laskea, kehittyy harjoituksen myötä samoin kuin syötteen koon tunnistamisen taitokin. Funktion lausekkeessa esiintyvien vakioiden todelliset arvot taas riippuvat käytettävästä kääntäjästä ja laitteistosta, joiden vaikutusta ei voida ennakoida. Sen vuoksi näiden vakioiden merkitykselle ei pidä antaa liian suurta painoa. Tärkeimpiä ovat syötteen kokoa sisältävät termit. Suoritusaika ei aina riipu pelkästään syötteen koosta, vaan myös syötteen laadusta. Kun tämä otetaan huomioon, voidaan tarkastelu eriyttää seuraaviin kolmeen tapaukseen: 1) T(n) tarkoittaa pahimman tapauksen suoritusaikaa eli pisintä mahdollista n:n kokoisen syötteen aiheuttamaa suoritusaikaa. 2) T avg (n) tarkoittaa keskimääräistä suoritusaikaa eli kaikkien n:n kokoisten syötteiden aiheuttamien suoritusaikojen keskiarvoa. 3) T best (n) tarkoittaa parhaan tapauksen suoritusaikaa eli lyhintä mahdollista n:n kokoisen syötteen aiheuttamaa suoritusaikaa.

1. ALGORITMIIKASTA 13 kiksi sama syöte ei ole pahin algoritmin kaikille osille, joudutaan ensin etsimään kokonaisvaikutukseltaan pahin syöte. Seuraavassa esitettävä kertaluokkatarkastelu kuitenkin helpottaa analysointia melkoisesti. Kertaluokat Kun algoritmien suoritusajat ilmaistaan syötteen koon funktioina, voidaan aikoja vertailla toisiinsa funktioiden kasvunopeuksia vertailemalla. Vakiokerrointen todellisten arvojen häilyvyyden vuoksi tarkastelua ei viedä äärimmilleen, yksittäisten funktioiden tasolle, vaan tarkastellaan funktioiden kertaluokkia. Kertaluokkatarkastelussa käytetään apumerkintöjä O, Ω, Θ ja o, joiden merkitys määritellään seuraavasti: Määritelmä 1-12: Kertaluokkamerkinnät O, Ω, Θ ja o. 1) T(n) = O(f(n)), jos on olemassa positiiviset vakiot c ja n 0 siten, että T(n) cf(n), kun n n 0. [Lue: T(n) on kertaluokkaa f(n), iso-o, ordo; "rajoittaa ylhäältä"] 2) T(n) = Ω(g(n)), jos on olemassa positiiviset vakiot c ja n 0 siten, että T(n) cg(n), kun n n 0. [omega; "rajoittaa alhaalta"] 3) T(n) = Θ(h(n)), jos T(n) = O(h(n)) ja T(n) = Ω(h(n)). [theta; "rajoittaa ylhäältä ja alhaalta"] 4) T(n) = o(p(n)), jos T(n) = O(p(n)) ja T(n) Θ(p(n)). [pikku-o; "rajoittaa aidosti ylhäältä"] Ensimmäinen määritelmistä antaa suoritusaikafunktion T kasvunopeudelle ylärajan: kyllin suurilla n:n arvoilla funktio T kasvaa enintään yhtä nopeasti kuin vakiolla c kerrottu funktio f. Toinen määritelmä antaa vastaavasti kasvunopeuden alarajan: kyllin suurilla n:n arvoilla funktio T kasvaa vähintään yhtä nopeasti kuin vakiolla c kerrottu funktio g. Kolmas määritelmä sitoo funktion T kasvunopeuden samaksi kuin on funktion h kasvunopeus. Viimeinen määritelmä rajaa funktion T kasvunopeuden aidosti hitaammaksi kuin funktion p kasvunopeus, eli kaikilla vakiolla c on T(n) < cp(n), kun n on kyllin suuri. Määritelmät esitetään joissakin lähteissä hieman eri muotoisina, mutta olennaisesti samaa tarkoittavina. Tällä kurssilla käytetään lähinnä vain ylärajan ilmaisevaa O-merkintää, vaikka usein voitaisiin sen asemesta käyttää täsmällisempää Θ-merkintää. Ylärajaominaisuus on näet transitiivinen, toisin sanoen jos f(n) = O(g(n)) ja g(n) = O(h(n)), niin silloin myös f(n) = O(h(n)). Tämä merkitsee, että ylärajoja on aina useita. Θ-merkinnän määräämä rajafunktio sen sijaan on yksiymmärteinen: mahdollisimman tiukka. Ylärajoistakin pyritään aina löytämään tiukin, jotta kertaluokkavertailut vastaisivat tarkoitustaan. Esimerkki 1-13: Olkoon T(n) = 5n+2. Silloin T(n) = O(n), mikä nähdään vaikkapa valitsemalla c = 7 ja n 0 = 1: 5n+2 5n+2n = 7n, kun n 1. Ylärajan n rinnalle kelpaisivat myös ylärajat n 2, n 3 ja niin edelleen. Koska T(n) = Ω(n), mikä nähdään esimerkiksi valitsemalla c = 5 ja n 0 = 1, on n myös alaraja, joten n on tiukin yläraja ja itse asiassa T(n) = Θ(n). Määritelmässä 1-12 esiintyvien epäyhtälöiden ei tarvitse päteä arvoilla n < n 0. Usein funktioiden keskinäinen järjestys pienillä n:n arvoilla poikkeaakin siitä järjestyksestä, joka vallitsee tarkasteltaessa suuria n:n arvoja. Esimerkiksi 5n+2 > 6n, kun n = 1, ja vasta

1. ALGORITMIIKASTA 14 kun n 2, on 5n+2 6n. Aikavaativuustarkastelussa pienet n:n arvot ovat merkityksettömiä, koska vasta suuret syötteet paljastavat algoritmien suoritusaikojen kertaluokkien erot. Kertaluokkaa arvioitaessa voidaan määritelmässä 1-12 esiintyvät vakiot c ja n 0 valita vapaasti, kunhan valinta vain toteuttaa määritelmän epäyhtälön. Esimerkin 1-13 yläraja n olisi löytynyt myös valitsemalla c = 6 ja n 0 = 12, c = 70 ja n 0 = 1 tai jollakin muulla tavoin. Ylärajatarkastelussa negatiiviset termit on helppo pudottaa pois ja positiiviset on siis saatava sulautumaan merkitsevimpään termiin. Esimerkki 1-14: Ylärajatarkastelu: T(n) = 2n 2 4n+5. 2n 2 4n+5 2n 2 +5 2n 2 +5 n 2 = 7n 2, eli 2n 2 4n+5 = O(n 2 ). Alarajatarkastelussa on päinvastoin positiiviset termit helppo pudottaa pois ja negatiiviset termit on saatava sulautumaan merkitsemimpään termiin. Sulauttamisessa on varottava ettei merkitsevin termi mene negatiiviseksi (negatiivisia suoritusaikoja ei vielä ole keksitty!). Esimerkki 1-15: Alarajatarkastelu: T(n) = 2n 2 4n+5. 2n 2 4n+5 2n 2 4n 2n 2 4n 2n 2 1 4n -- n [pitää paikkansa kun n 4] = 1n 2, eli 2n 2 4n+5 = Ω(n 2 ). 4 1 Alarajatarkastelussa voidaan lisätä kerroin -- n negatiiviseen termiin, sillä kun n 4 4 (siis n 0 = 4), niin negatiivinen termi on entistä suurempi ja siten pienentää kokonaisuutta. Esimerkiksi kerroin n olisi sensijaan tehnyt koko funktion negatiiviseksi ja siten mahdottomaksi. Esimerkki 1-16: Esimerkkien 1-14 ja 1-15 nojalla 2n 2 4n+5 = Θ(n 2 ). Ellei epäyhtälön toteuttavia vakioita laisinkaan löydy, on kertaluokkayrite tarkasteltavaan funktioon nähden väärä. Näytettäessä ettei funktio f(n) ole kertaluokkaa g(n) on toisin sanoen osoitettava, ettei ole olemassa sellaisia positiivisia vakioita c ja n 0, että epäyhtälö f(n) cg(n) pätisi kaikilla arvoilla n n 0. Esimerkki 1-17: Näytetään, ettei funktio T(n) = 5n+2 ole kertaluokkaa 1 eli vakio: Jos olisi T(n) = O(1), niin määritelmän 1-12 nojalla olisi olemassa positiiviset vakiot c ja n 0 siten, että 5n+2 c, kun n n 0. Epäyhtälöt 5n+2 c ja n (c 2)/5 ovat yhtäpitävät. Koska c on vakio, on myös (c 2)/5 vakio. Tämä merkitsee, ettei epäyhtälö 5n+2 c toteudu ainakaan silloin, kun n > max{n 0, (c 2) /5}, mikä on vastoin oletusta. Ristiriita, eli väite on väärä. Kertaluokkatarkastelussa ei suoritusaikafunktioiden lausekkeissa esiintyvillä vakiokertoimilla ja vakiotermeillä ole merkitystä. Esimerkiksi funktiot 2n ja 1000000n ovat molemmat O(n), mikä nähdään esimerkiksi valitsemalla c = 1000001 ja n 0 = 1. Näiden funktioiden arvot toki poikkeavat toisistaan huomattavasti, mutta ne kasvavat samaa vauhtia. Sen sijaan esimerkiksi kertaluokat n ja n 2 ovat olennaisesti erilaiset: kun n kasvaa, kasvaa n 2 yhä vauhdikkaammin. Kahdesta eri kertaluokkaa olevasta algoritmista kertaluokaltaan pienempi on yleensä myös tehokkaampi. Pienillä syötteillä tilanne tosin voi kääntyä päinvastaiseksi, mutta pienillä syötteillä suoritusajalla ei yleensä ole merkitystä.

1. ALGORITMIIKASTA 15 400 Suoritusaika (T(n)) 350 300 250 200 n 3 2 n n 2 nlog 2 n 150 100 n n nlogn 3n 50 n+10 logn n 0 0 5 10 15 20 25 30 Syötteen koko (n) Kuva 1-8: Eri funktioiden kasvunopeuksia. Huomaa, että suurinkin kuvassa näkyvä n on varsin pieni, 30. Suuremmilla n:n arvoilla jotkin funktiot vielä vaihtavat järjestystä. Esimerkki 1-18: Kuva 1-8 esittää kymmenen aikavaativuudeltaan eri kertaluokkaa olevan ohjelman suoritusajat syötteen koon funktioina. Ajat on mitattu millisekunteina ja kaikki mittaukset on tehty samalla laitteistokokoonpanolla. Graafisesti eri funktioiden kasvunopeutta on vaikea tarkastella, varsinkaan suuremmilla n:n arvoilla. Esimerkiksi kuvasta on vaikea uskoa, että nlog 2 n = o( n n ). Sensijaan taulukosta 1-9 näemme saman asian paremmin numeerisessa muodossa. Taulukossa on esitetty kymmenen eri ohjelman aikavaativuuksien vaikutus käsiteltävissä olevaan syötteeseen. Oletamme, että yhden operaation viemä aika on yksi millisekunti. Kullekkin ohjelmalle on laskettu se syötteen koko, jonka ne ehtivät käsitellä yhdessä sekunnissa, minuutissa, jne. Esimerkiksi aikavaativuudeltaan n oleva ohjelma ehtii minuutissa käsittelemään syötteen, jossa on 60000 alkiota. Näemme, että logaritmista aikavaativuutta oleva ohjelma ehtii varmasti käsitellä sekunnissa kaiken tarvittavan tiedon. (Tosin logaritmista aikavaativuuttahan ei peräkkäisalgoritmeissa voi koko algoritmilla ollakaan. Algoritmin osa, kuten binäärihaku, voi olla logaritminen aikavaativuudeltaan). Aikavaativuudeltaan O(n) ja O(nlogn) olevat ohjelmat hyötyvät lisääntyneestä ajasta edelleen hyvin. Sensijaan O(n 2 ) ja O(n 3 ) hyötyvät jo huomattavasti vähemmän. Eksponentiaalinen O(2 n ) ohjelma ei kykene käsittelemään suuria tietomääriä, vaikka sitä suoritettaisiin vuosia. Samanlaisia tuloksia voitaisiin havaita vaikka yksi operaatio kestäisi millisekunnin sijasta nanosekunnin.

1. ALGORITMIIKASTA 16 Taulukko 1-9: Aikavaativuuserot numeroina. Aikavaativuus T(n), ms Ohjelman käsittelemän syötteen koko n annetussa ajassa: Sekun nissa Minuutissa Tunnissa Päivässä Vuodessa logn 10 301 10 18 061 10 1 083 707 10 26 008 991 109 493 281 943 n 1 000 60 000 3 600 000 86 400 000 31 536 000 000 n+10 990 59 990 3 599 990 86 399 990 31 535 999 990 3n 333 20 000 1 200 000 28 800 000 10 512 000 000 nlogn 140 4 895 204 094 3 943 234 1 052 224 334 nlog 2 n 36 678 18 013 266 037 48 391 041 n n 100 1 532 23 489 195 438 9 981 697 n 2 31 244 1 897 9 295 177 583 n 3 10 39 153 442 3 159 2 n 9 15 21 26 34 Aikavaativuuksien luokittelu Käytännössä aikavaativuusfunktioiden vertailu on melko helppoa. Tärkeintä on havaita suoritusaikafunktiosta merkittävin tekijä, käytännössä siis nopeimmin kasvava osa. Perusmuotoisen funktion lisäksi aikavaativuudessa voi olla jokin osa peruslaskutoimituksella (, /, +, ) mukaan liitettynä. Tällöin sen aiheuttama muutos on tietysti otettava huomioon. Nopeimmin kasvavia funktioita ovat eksponenttifunktiot, kuten, esimerkiksi 2 n, 3 n, 2 n /n. Näissä syötteen koko on siis ykköstä suuremman luvun eksponenttina. Käytännössä useimmat aikavaativuudet ovat polynomisia, esimerkiksi n, n 2, n 5, n 12345, n3 n. Mitä suurempi eksponentti, sitä suurempi aikavaativuus. Polynomisiin kuuluvat myös aikavaativuudet muotoa n c, missä 0<c<1 on. Nämä ovat alilineaariasia, ts. o(n). Näitäkin hitaammin kasvavia ovat logaritmiset aikavaativuudet, kuten logn, loglogn, (logn) 2. Vakiofunktiot (O(1)) eivät kasva lainkaan. Eri kertaluokkafunktioiden keskinäisen järjestyksen selvittäminen voi joskus olla pulmallista. Tämäntyyppinen ongelma ratkeaa lähes aina niin sanottua L'Hôspitalin sääntöä soveltaen (derivoimalla osamäärän molemmat puolet): jos lim n f(n) = ja lim n g(n) =, niin (1-1) lim n (f(n)/g(n)) = lim n (f'(n)/g'(n)). Sääntöä sovelletaan toistuvasti, kunnes raja-arvo saadaan selville. Jos raja-arvo on 0, on f(n) = o(g(n)) (1-2) c 0,on f(n) = Θ(g(n)), on g(n) = o(f(n)). Ellei raja-arvo ole yksiymmärteinen, ei kertaluokkia voida asettaa järjestykseen.

1. ALGORITMIIKASTA 17 Esimerkki 1-19: nlogn vs. n 1,5 f(n) = nlogn g(n) = n 1,5 (1-3) 1 3 2 f (n) = logn+ ------- g (n) = --n ln2 2 (1-4) 1 3 f (n) = ---------- g (n) = --------- nln2 4 n (1-5) 1 ---------- nln2 4 n lim ---------- = lim ------------- = 3 n 3nln2 --------- 4 n n 1 -- 4 lim ------------------ 0 3ln2 n n (1-6) f ( n) = o( g( n) ), ts. nlogn = o(n 1,5 ) (1-7) Sama voidaan todeta epämuodollisemmin päättelemällä: nlogn?? n 3/2 :n (1-8) logn?? n 1/2 ( ) 2 (1-9) log 2 n?? n (1-10) Kun muistetaan, että log k n = o(n), niin voimme todeta, että n 3/2 kasvaa nopeammin. Usein kertaluokkien järjestys voidaan päätellä yksinkertaisemminkin kuin laskemalla derivaattoja ja raja-arvoja. Esimerkiksi n k = o(n k+1 ) kaikilla vakioilla k. Edellisen tarkastelun perusteella voidaan todeta, että laitteiston tehokkuuden lisäyksen asemesta olisikin tuottoisampaa pyrkiä nopeuttamaan ohjelmia, toisin sanoen tulisi pyrkiä suunnittelemaan entistä tehokkaampia algoritmeja. Tämä on haasteellinen tehtävä, sillä moniin usein esiintyviin ongelmiin tunnetaan toistaiseksi vain tehottomia ratkaisuja, vaikka tehokkaitakin ratkaisuja saattaa olla olemassa. Joillekin ongelmille taas on osattu todistaa vaativuuden alaraja eli on näytetty, ettei ongelmaa voida ratkaista alarajaa nopeammin. Yksi näistä ongelmista on yleinen lajitteluongelma, jonka aikavaativuus on O(nlogn). Tämä alaraja on jo saavutettu, mutta jotkin muut todistetut alarajat ovat vielä teoreettisia. On myös olemassa joukko ongelmia, joita ei lainkaan voida ratkaista algoritmisesti, kuten esimerkiksi pysähtymisongelma. Algoritmiikan piirissä onkin vielä runsaasti tutkittavaa ja tässä tutkimustyössä vaativuusanalyysin rooli on keskeinen. Vaativuusanalyysiä tarvitaan jopa yksittäistä algoritmia tehostettaessakin, sillä algoritmin tehottomat osat täytyy löytää ennen kuin tehostamisyrityksiä kannattaa edes aloittaa. Johtopäätöksenä voitaisiin sanoa, että prosessorin kellotaajuuden kymmenkertaistamisen hyöty on aika pieni verrattuna algorimin kehittämiseen vaikkapa O(n 2 ):sta O(nlogn):een. 1.4 Suoritusajan laskeminen käytännössä Mielivaltaisesti valitun ohjelman suoritusajan laskeminen voi joskus osoittautua erittäin vaativaksi matemaattiseksi tehtäväksi. Onneksi useiden käytännössä esiintyvien ohjelmien suoritusaika on varsin helppo arvioida. Yleensä riittää tuntea aritmeettisen ja geo-