(Muutettu viimeksi 16.9.2010, sivu perustettu 20.8.2010. Arto Wikla)
Tässä luvussa tutustutaan algoritmin ideaan ja perustavanlaatuisten algoritmien laadintaan. Algoritmi on toimintaohje jonkin ongelman ratkaisemiseen tai vaikkapa jonkin palvelun toteuttamiseen. Nykyiset tietokoneet aina suorittavat jotakin algoritmia, kun ne yleensäkin tekevät jotakin. Myös useimmat ohjelmointikielet perustuvat algoritmin kirjoittamiseen – toisin sanoen ohjelmoija ajattelee laativansa ajassa etenevää toimintaohjetta. Tosin on toisenlaisiakin ohjelmointikieliä...
Tässä luvussa algoritmeja kirjoitetaan Scala-ohjelmointikielellä, joka nimensä mukaisesti skaalautuu hyvin yksinkertaisesta hyvin monimutkaiseen. Turha varmaan mainitakaan, kummassa päässä skaalaa tässä luvussa liikutaan. On syytä harjoituksissa noudattaa tämän luvun esimerkkien tyyliä, vaikka Scalalla kaiken voi tehdä ties millä tavalla.
Aloitetaan ohjelmointi kansainväliseen tapaan.
Kirjoita seuraava ohjelma tekstitiedostoon Hello.scala
.
println("Hello world!");
Toisinaan tuollaista tekstitiedostoon kirjoitettua ohjelmaa kutsutaan koodiksi. Nimitys on kotoisin 1950-luvulta, jolloin konekielikäskyjä "koodattiin" symbolisilla nimillä.
Tämän jälkeen anna komentotulkissa
komento scala hello.scala
.
Olet nyt itse asiassa aloittanut ohjelmoijan urasi!
Editoi tiedostoasi: muuta tulostettavaa tekstiä ja lisää
uusia tulostuslauseita. Talleta tiedosto ja suorita taas.
Osaat siis jo tehdä ohjelmia, jotka tulostavat, mitä ikinä
maailmassa voidaankaan pyytää tulostamaan.
Tietokone ei kerta kaikkiaan "osaa" suorittaa mitään muuta algoritmia kuin sellaista, joka on kirjoitettu koneen sisäisellä omalla konekielellä. Tuo kieli kuitenkin on useimpiin ohjelmointitehtäviin auttamattoman kömpelöä ja koneenläheistä.
Ohjelma kirjoitetaan useimmiten jollakin konekieltä korkeamman tason kielellä, lausekielellä. Toisinaan näin kirjoitettua ohjelmaa kutsutaan lähdekoodiksi. Lähdekoodi talletetaan tekstimuodossa ja suoritetaan jollakin tavalla.
Tietokone ei siis osaa suorittaa mitään muita kuin konekielellä ilmaistuja algoritmeja. Siksi tarvitaan kääntäjä tai tulkki, joka muokkaa lähdekielisen ohjelman tavalla tai toisella konekielioperaatoiden jonoksi.
---> Lisätietoa kääntäjistä ja tulkeista
Algoritmisessa ohjelmoinnissa muuttuja on keskeinen käsite. Muuttuja on nimetty "laatikko", jossa säilytetään jotakin arvoa. Ja kuten nimi "muuttuja" antaa ymmärtää, tuota arvoa voi muuttaa. Algoritmin tila tarkoittaa algoritmin muuttujien arvoja "tällä hetkellä".
Vaikka tietokoneen muistissa kaikki arvot esitetään lopulta vain bittien jonona, "bittimoskana", nykyaikaisissa ohjelmointikielissä ohjelmoija käsittelee arvoja ja muuttujia siten, että niillä on tyyppi.
Tyypillisiä tyyppejä ovat esimerkiksi merkkijono,
kokonaisluku, "liukuluku" eli desimaaliluku
ja totuusarvo.
Muuttujaan sijoitetaan arvo yhtäsuuruusmerkillä (=
).
Sijoituslause on syytä lukea "muuttuja saa arvon", ei
"muuttuja on". Miksi?
---> Lisätietoa tyypeistä
Scala-kielellä muuttuja määritellään ilmauksella var muuttujan nimi
.
Muuttuja saa tyypikseen määrittelyn yhteydessä annetun arvon tyypin.
(Muitakin tapoja Scalassa on...)
Esimerkkejä muuttujien määrittelystä ja arvojen muuttamisesta
var teksti = "juttuhomma"; var kokonaisluku = 1234; var liukuluku = 3.14; var onkoTotta = true; println(teksti); println(kokonaisluku); println(liukuluku); println(onkoTotta); teksti = "uusjuttu"; kokonaisluku = -666; liukuluku = 1.2; onkoTotta = false; println(teksti); println(kokonaisluku); println(liukuluku); println(onkoTotta);
Kuten esimerkissä nähdään, muuttujan arvoa siis voi muuttaa. Mikä on algoritmin tila ennen ensimmäisiä tulostuksia? Entä ennen toisia?
Ohjelmia laadittaessa monenlaisille itse määritellyille asioille annetaan itse päätettäviä
nimiä eli tunnuksia (identifier).
Esimerkiksi juuri muuttujien nimet ovat näitä tunnuksia.
Javassa ja Scalassa tunnukset alkavat aina kirjaimella ja voivat sisältää
kirjaimia ja numeromerkkejä. (Myös alaviiva "_
lasketaan "kirjaimeksi".)
Vaikka skandinaaviset "ääkkösemmekin" kelpaavat tunnuksissa käytettäviksi, niitä on paras välttää, koska maailmalla on käytössä pari hyvin yleistä toisistaan poikkeavaa tapaa koodata nuo rakkaat kirjaimemme, ks. Wikipedia-artikkeli merkkikoodauksesta.
Nimessä ei siis saa olla erikoismerkkejä, kuten vaikkapa huutomerkkejä (!).
Välilyönti ei ole sallittu, sillä se erottaa kielen rakenteita toisistaan.
Välilyönti kannattaa korvata alaviivalla tai
camelCase
tyylillä, jolloin nimi muistuttaneeKamelia
.
Muuttujan nimi ei myöskään saa olla jo entuudestaan käytössä. Tälläisiä nimiä ovat
mm. aikaisemmin määritellyt muuttujat ja esimerkiksi tulostusoperaation nimi
println
– ja monet muut varatut nimet.
Muuttuja kannattaa nimetä siten, että sen käyttötarkoitus on selvää ilman selityksiä tai miettimistä. Itse asiassa tällä kurssilla muuttujat pitää nimetä kuvaavasti!
Kunnollisia muuttujien nimiä:
Huonompia muuttujien nimiä:
Virheellisiä muuttujien nimiä:
Kurssilla suositaan (Javan tapaan) Camel Case -nimiä:
kuukaudenEnsimmainenPaiva
, jne...
Ja säästetään isolla kirjaimella alkavat tunnukset Javan luokkanimille.
Kurssin Scala-osiossa siis kaikki tunnukset alkakoot pienellä
kirjaimella.
Ohjelmatekstiin on mahdollista, hyödyllistä ja välttämätöntä liittää selittäviä tekstejä eli kommentteja. Miksi?
Scalalla ja Javalla on käytössä kaksi kommentointitapaa, monirivinen ja rivin loppu -kommentti:
/* Tämä ohjelma esittelee muuttujan määrittelyä, arvon tulostamista ja arvon muuttamista. */ var kokonaisluku = 1234; // määritellään ja annetaan alkuarvo println(kokonaisluku); // tulostellaan /* ja sitten muutetaan muuttujan arvoa: */ kokonaisluku = -666; println(kokonaisluku); // muuttuiko?
Tulostukseen on käytettävissä jo tutun
println
-operaation lisäksi operaatio
print
.
Ensin mainittu vaihtaa riviä tulostettuaan, jälkimmäinen ei
vaihda.
print("kissa") println("kävelee") // tulostaa: kissakävelee println("kissa") println("kävelee") // tulostaa: kissa // kävelee var i = 4321; print("Muuttujan i arvo on "); print(i); println("."); // tulostaa Muuttujan i arvo on 4321.
Tuttu operaatio +
sovellettuna kahden merkkijonon välille synnyttää
uuden merkkijonon, jossa kaksi merkkijonoa liitetään toisiinsa. Hienolla
nimellä tätä kutsutaan merkkijonojen katenoinniksi.
println("kissa" + "kävelee") // tulostaa: kissakävelee
Eipä tästä vielä paljon iloa ole, mutta kun kuulee ilosanoman, että merkkijonoja ja lukuarvoja voi katenoida, alkaa tulostusoperaatoiden kirjoittaminen helpottua:
var i = 4321; println("Muuttujan i arvo on " + i + "."); // tulostaa Muuttujan i arvo on 4321. println("Yhtä suurempi arvo on " + (i+1) + "."); // tulostaa Yhtä suurempi arvo on 4322. // HUOM: SULUT I:N KASVATUKSEN YMPÄRILLÄ VÄLTTÄMÄTTÖMÄT, SILLÄ println("Yhtä suurempi arvo on " + i+1 + "."); // tulostaa Yhtä suurempi arvo on 43211. println("ahaa " + i + 7); // ahaa 43217 println("ahaa " + (i + 7)); // ahaa 4328 println(i + 7 + " ahaa"); // 4328 ahaa
Merkkijonokatenointi on toki käytettävissä myös merkkijonomuuttujien käsittelyssä:
var kissa = "topi"; var hanta = "katti"; var koko = kissa + hanta; println(koko); // tulostaa topikatti
Merkkijonon sisään voi kirjoittaa eräitä ohjausilmauksia, mm.:
println("Merkkijonon sisäänkin saa lainausmerkin: \". Mukavaa."); println("Sarkain eli tabulointikin onnistuu: \t Kas näin."); println("Rivinvaihtojakin voi kirjoittaa: \n Tässä yksi. \n\nJa pari lisää.");
Ohjelmoinnissa – ja tietojenkäsittelytieteessä yleensäkin – laskennaksi kutsutaan melkeinpä mitä tahansa toimintaa, jossa arvoista tavalla tai toisella muokataan uusia arvoja. Tässä luvussa tarkastellaan numeerista laskentaa, aritmetiikkaa.
Operaatiot ovat tuttuja: +
, -
,
*
ja /
.
Erikoisempi operaatio on %
, joka on jäännösjako,
jakolaskun jakojäännös eli modulo.
Myös laskentajärjestys on tuttu: kertolaskut ennen yhteenlaskuja,
suluissa olevat ennen muita, ...
var eka = 2; var toka = 4; var kolmas = 6; var arvo = eka + toka * kolmas; println(arvo); // tulostaa 26
var sulut = (1+1) + 3 * (2+5) // 23 var suluitta = 1+1 + 3 * 2+5 // 13
Laskutoimituksia voi toki kirjoittaa myös tulostuslauseeseen
println((1+1) + 3 * (2+5)); // tulostaa 23 println(5 / 2) // tulostaa 2 println(19 / 5) // tulostaa 3 println(5 % 2) // tulostaa 1 println(19 % 5) // tulostaa 4
Desimaalilukujen (tai reaalilukujen) esittämiseen tietokoneissa käytetään ns. liukulukuja (floating point number). Yksinkertaistaen sanottuna ne esittävät reaaliluvun likiarvon kahtena kokonaislukuna: "merkitsevät numerot" ja "pisteen paikka". Tärkeää on muistaa, että tietokoneen liukuluku on aina likiarvo, ei "tarkka arvo". Asian voisi selittää vaikkapa sillä, että "merkiteseviä numeroita" voi tietokoneessa olla vain äärellinen määrä.
Scalassa muuttujan voi määritellä liukulukutyyppiseksi (mm.) sijoittamalla muuttujan alkuarvoksi desimaaliluvun:
var x = 0; // x on kokonaislukutyyppinen var y = 0.0; // y on liukuluku println(x + " " + y) // tulostaa 0 0.0 x = 123; y = 1.23; println(x + " " + y) // tulostaa 123 1.23 // x = y; // error: type mismatch; // mutta y = x; println(x + " " + y) // tulostaa 123 123.0
Jakolaskuoperaation "/
" merkitys – onko kyse kokonaisjakolaskusta
vai liukulukujakolaskusta – määräytyy siten, että jos jaettava, jakaja
tai molemmat ovat liukulukuja, kyseessä on liukulukujakolasku.
Kokonaisjako on kyseessä vain, jos sekä jakaja että jaettava ovat kokonaislukuja:
println(5 / 2) // 2 println(5.0 / 2) // 2.5 println(5 / 2.0) // 2.5 println(5.0 / 2.0) // 2.5
Usein ohjelmoija saattaa haluta kahden kokonaisluvun jakolaskun tulokseksi liukuluvun:
var summa = 55; var lukumaara = 20; println(summa/lukumaara); // 2 println(1.0*summa/lukumaara); // 2.75 // mutta println(summa/lukumaara*1.0); // 2.0
Usein muuttujan uusi arvo lasketaan sen vanhasta vanhasta arvosta:
var pituus = 98; pituus = pituus + 2; pituus = pituus - 50; pituus = pituus * 2; pituus = pituus / 2; println(pituus); // 50
Tällaisessa tuiki tavanomaisessa tilanteessa voi käyttää erityisiä muuttavia sijoitusoperaatioita:
var pituus = 98; pituus += 2; pituus -= 50; pituus *= 2; pituus /= 2; println(pituus); // 50
Useimmiten on kuitenkin selkeämpää kirjoittaa sijoitusoperaatiot tutummassa muodossa!
Scala kielellä syötteiden lukeminen on vaivatonta:
var nimi = ""; // tyhjä merkkijono var ika = 0; // kokonaisluku var pituus = 0.0 // liukuluku println("Mikä on nimesi?"); nimi = readLine; // merkkijono print("Ja ikäsi: "); ika = readInt; // kokonaisluku print("Entä pituutesi? "); pituus = readDouble; // liukuluku println("Moi " + nimi +"!"); print("Tiedän että olet " + ika + "-vuotias "); println("ja että olet " + pituus + " senttiä pitkä."); println("Enko olekin viisas?");
Käyttöesimerkki:
Mikä on nimesi? Violetta Ja ikäsi: 20 Entä pituutesi? 167.8 Moi Violetta! Tiedän että olet 20-vuotias ja että olet 167.8 senttiä pitkä. Enko olekin viisas?
Jos ohjelmalle syöttää väärän tyyppistä tietoa, ohjelman suoritus päättyy virheeseen. Mutta ei välitetä siitä vielä...
Usein algoritmin halutaan toimivan vaihtoehtoisilla tavoilla riippuen esimerkiksi syötteiden arvosta. Valintalauseella ohjataan algoritmin toimintaa tällaisessa tilanteessa. Scalan, Javan ja monen muun kielen valintalause on ns. if-lause:
println("Anna kokonaisluku!"); var luku = readInt; if (luku > 0) println("Luku on positiivinen.");
If-lauseen ehto (luku > 0)
on väittämä algoritmin tilasta. Kun algoritmi etenee tuon ehdon laskentaan, tutkitaan juuri sillä hetkellä,
sattuuko muuttujan arvo olemaan positiivinen. Jos sattuu, suoritetaan
if-lauseen alilause println("Luku on positiivinen.")
. Tällöin
ehdon totuusarvo on true
. Jos ehto ei ole tosi eli jos väittämä
ei pidä paikkaansa (false
), alilausetta ei suoriteta.
If-lauseeseen voi liittää myös else-osan, jos haluaa jotakin tehtäväksi myös tilanteessa, jossa ehto on epätosi:
println("Anna kokonaisluku!"); var luku = readInt; if (luku > 0) println("Luku on positiivinen."); else println("Luku ei ole positiivinen.");
If- lauseita myös ketjutetaan melko usein:
println("Anna kokonaisluku!"); var luku = readInt; if (luku > 0) println("Luku on positiivinen."); else if (luku < 0) println("Luku on negatiivinen."); else println("Ahaa, luku on nolla."); // ei ollut positiivinen eikä negatiivinen...
Loogisilla lausekkeilla esitetään väitteitä ohjelman tilasta eli ohjelman muuttujien arvoista. Väitteiden totuusarvoa - totta tai ei totta - käytetään ohjaamaan algoritmin suoritusta. Loogisia lausekkeita (eli totuusarvoisia lausekkeita) muodostetaan tavallisimmin erilaisista vertailuista, joita mahdollisesti liitetään toisiinsa loogisilla operaatioilla.
Tavallisimpia vertailuoperaatioita:
>
suurempi kuin>=
suurempi tai yhtäsuuri kuin<
pienempi kuin<=
pienempi tai yhtäsuuri kuin==
yhtäsuuri kuin!=
erisuuri kuinKaikki vertailut tuottavat totuusarvon true tai false.
On syytä pitää mielessa sijoitusoperaation "=" ja vertailuoperaation "==" ero:
i = 1; // "i saa arvokseen yksi" i == 1 // "onko i:n arvo yksi"
Suuremmuutta tai pienemmyyttä tutkivilla operaatioilla voi vertailla vain numeerisia lausekkeita, yhtäsuuruus ja erisuuruus ovat käytettävissä monien muidenkin arvojen vertailussa.
Esimerkki: Olkoon ohjelmassa vaikkapa seuraavat määrittelyt:
var i = 7; var d = 3.14; var s = "hupsista"; var b = false;
Tässä tilanteessa looginen lauseke
(i < 10) on arvoltaan true (d > 10.0) on arvoltaan false (8 == (i+1)) on arvoltaan true (b != true) on arvoltaan true (s != "hupsista") on arvoltaan false (s == "hupsista") on arvoltaan true
Huom: Liukulukujen (so. desimaalilukujen) vertailussa ei ole syytä käyttää yhtäsuuruusvertailuja (== ja !=), koska ns. pyöristysvirheiden takia yhtäsuuruusvertailut voivat antaa odottamattomia tuloksia. Tällaisessa tilanteessa on parempi tutkia lukujen erotuksen itseisarvon pienuutta.
Huom: String-arvojen vertailu ei sitten aikanaan Javalla onnistu yhtä vaivattomasti...
Usein on tarpeen liittää algoritmin tilaa koskevia väittämiä yhteen monimutkaisemmiksi väittämiksi. Käytössä on mm. seuraavat loogiset operaatiot:
&&
ehdollinen "ja" (and)
(ehto_1) && (ehto_22)
on tosi, jos molemmat
ehdot ovat totta, muuten se on epätosi
||
ehdollinen "tai" (or)
(ehto_1) || (ehto_2)
on tosi, jos jompikumpi
ehto on tosi tai jos molemmat
ehdot ovat tosia, muuten se on epätosi
!
negaatio (not)
!(ehto)
on epätosi, jos (ehto)
on tosi,
muuten se on tosi
Näiden operaatioiden operandit eli laskettavat voivat olla vain totuusarvoisia lausekkeita, tavallisimmin ne ovat vertailuja. Operaatioiden ehdollisuus tarkoittaa sitä, että operaation jälkimmäinen laskettava lasketaan vain, jos totuusarvo ei selviä jo ensimmäisestä!
println("Anna luku väliltä 5-10: "); var luku = readInt; if ( 5 <= luku && luku <= 10 ) // HUOM: ILMAUS (5 <= luku <= 10) EI OLE LUVALLINEN! println("Oikein!"); else println("Luku ei kelpaa.")
println("Valitse joko 1, 50 tai 100: "); var vastaus = readInt; if ( vastaus == 1 || vastaus == 50 || vastaus == 100 ) println("Valitsit ohjeen mukaisesti."); else println("Valitsit väärin.");
Loogisien operaatioiden totuustaulukko (a ja b voivat itse olla mitä tahansa loogisia lausekkeita, esimerkiksi vertailuja) (tuo ^ on ns. poissulkeva tai, xor):
a | b || a && b | a || b | a ^ b | !a -------------------------------------------------------- true | true || true | true | false | false true | false || false | true | true | false false | true || false | true | true | true false | false || false | false | false | true
Hyvin usein if-lauseen alilauseeksi if-osaan tai else-osaan halutaan useampi kuin yksi lause.
Tällöin nuo lauseet kootaan yhdeksi lauseeksi, lohkoksi, aaltosulkeiden
"{
" ja "}
" väliin:
println("Anna kokonaisluku!"); var luku = readInt; if (luku > 0) { println("Luku on positiivinen."); println("Kaksinkertaisena se on " + 2*luku); } else { println("Luku ei ole positiivinen."); println("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku); }
Huomaa että nuo aaltosulkeet ovat välttämättömät! Pelkkä sisentäminen ei riitä. Seuraava on siis väärin:
println("Anna kokonaisluku!"); var luku = readInt; if (luku > 0) // TÄMÄ ON VIRHEELLINEN! println("Luku on positiivinen."); // AALTOSULUT PUUTTUVAT! println("Kaksinkertaisena se on " + 2*luku); else println("Luku ei ole positiivinen."); println("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku);
Jotkut suosivat seuraava ulkoasua:
println("Anna kokonaisluku!"); var luku = readInt; if (luku > 0) { println("Luku on positiivinen."); println("Kaksinkertaisena se on " + 2*luku); } else { println("Luku ei ole positiivinen."); println("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku); }
Myös seuraavaa else-layout on mahdollinen:
if (luku > 0) { ... } else { ... }
Useimmissa ohjelmointikielissä (mm. Scala ja Java) ohjelman ulkoasulla ei ole kääntäjälle viestiä. Niinpä kääntäjälle kelpaa jopa seuraava sotku — sinänsä täysin toimiva ohjelma:
println("Anna kokonaisluku!"); var luku = readInt;if (luku > 0) {println("Luku on positiivinen.");println("Kaksinkertaisena se on " + 2*luku);}else{println("Luku ei ole positiivinen.");println( "Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: "+ luku*luku);}
Mutta ohjelmoijalle ohjelman selkeä ja johdonmukainen ulkoasu on välttämätön! Miksi?
Samaan tapaan kuin väittämät algorimin tilasta ohjaavat valintaa if-lauseissa, väittämiä käytetään myös ohjaamaan algoritmin jonkin osan toistamista.
Ehdollinen toistolause, while
-lause, toistaa alilausettaan
yhä uudelleen ja uudelleen kunnes toistoehto ei enää ole totta.
Toki on mahdollista, että ehto alkujaankin on epätosi, jolloin toistoja
ei ole ainuttakaan:
println("Montako onnentoivotusta haluat?"); var lukumaara = readInt; while (lukumaara > 0) { println("Onnea!"); lukumaara = lukumaara - 1; }
Toistolauseen alussa tutkitaan, onko väittämä (lukumaara > 0)
totta.
Jos se on, koko toistettava alialgoritmi suoritetaan. Sen jälkeen taas tutkitaan,
onko ehto voimassa, jne...
Huomaa että toistettavan alialgoritmin on syytä vaikuttaa jotenkin
toistoehdon arvoon! Miksi?
Toinen esimerkki: Vaaditaan että käyttäjä syöttää parittoman luvun. Luku on pariton, jos se ei ole parillinen. Luku on parillinen, jos jakojäännös kahdella jaettaessa on nolla:
println("Anna pariton luku."); var luku = readInt; while (luku % 2 == 0) { println("Eihän luku " + luku + " ole pariton!"); println("Yritä uudelleen"); luku = readInt; } println("Kyllä! " + luku + " todella on pariton.");
Käyttöesimerkki:
Anna pariton luku. 44 Eihän luku 44 ole pariton! Yritä uudelleen -128 Eihän luku -128 ole pariton! Yritä uudelleen 235 Kyllä! 235 todella on pariton.
Tähän mennessä tavatut muuttujat ovat olleet yksittäisiä "laatikoita, joihin mahtuu vain yksi arvo kerrallaan". Tällaisilla saa toki ohjelmoitua kaikki mahdolliset algoritmit. Mutta työlääksi ja tylsäksi se voi käydä...
Esimerkki: Laadi ohjelma, joka lukee viisi lukua ja tulostaa ne käänteisessä järjestyksessä:
var eka=0; var toka=0; var kolmas=0; var neljas=0; var viides=0; print("Anna 1. luku: "); eka = readInt; print("Anna 2. luku: "); toka = readInt; print("Anna 3. luku: "); kolmas = readInt; print("Anna 4. luku: "); neljas = readInt; print("Anna 5. luku: "); viides = readInt; println("Luvut käänteisessä järjestyksessä:"); println(viides); println(neljas); println(kolmas); println(toka); println(eka);
Kyllähän homma siis onnistuu, mutta vaikkapa jo sadan luvun tapauksessa ohjelmoija joutuisi näkemään melkoisesti vaivaa. Puhumattakaan kymmennetuhannen tai miljoonan luvun tapauksesta. Ja aivan turhaan!
Taulukko (array) on muuttuja, joka sisältää monta "lokeroa", joihin jokaiseen voidaan tallettaa yksi arvo. Taulukko on siis ikään kuin monen muuttujan "lokerikko".
Ja nyt tulee koko homman idis: Taulukkomuuttujan yksittäiseen lokeroon voidaan viitata lokeron järjestysnumerolla! Ja tuo järjestysnumero voi olla muuttujan arvona. Muista, että muuttujan arvoa voi muuttaa!
Scalassa, Javassa ja monessa muussa ohjelmointikielessä taulukkomuuttujan lokerot on numeroitu nollasta alkaen. Lokeroiden numeroita on tapana kutsua indekseiksi ja lokeroita taulukon alkioiksi.
Äskeisen esimerkin voi taulukkoa käyttäen ohjelmoida vaikkapa seuraavasti:
var luvut = new Array[Int](5); // tämä selitetään kohta... var lokeronNumero = 0; while (lokeronNumero < 5) { print("Anna " + (lokeronNumero+1) + ". luku: "); // pyydetään 1., 2., ..., siksi tuo +1 luvut(lokeronNumero) = readInt; lokeronNumero = lokeronNumero +1; } println("Luvut käänteisessä järjestyksessä:"); lokeronNumero = 4; while (lokeronNumero >= 0) { println(luvut(lokeronNumero)); lokeronNumero = lokeronNumero - 1; }
Yllä ilmaus new Array[Int](5)
on Scala-kielen tapa määritellä
taulukkomuuttuja, jonka alkiot ovat tyypiltään kokonaislukuja ja jonka
koko eli alkioiden lukumäärä on viisi. Java-kielessä taulukko määritellään
hieman eri tavalla, mutta merkitys on sama.
Ilmaus luvut(lokeronNumero)
on taulukon indeksointi;
se tarkoittaa taulukkomuuttujan alkiota, jonka järjestysnumero on
lokeronNumero
.
Jos muuttujan arvoa vastaavaa alkiota ei taulukossa ole, ohjelman
suoritus keskeytyy virheeseen. Kelvollinen indeksi on siis ohjelmoijan
vastuulla.
Taulukon pituuden voi tarkistaa ilmauksella luvut.length
:
var t1 = new Array[Int](5); var t2 = new Array[Double](500); var t3 = new Array[int](1000000); println(t1.length); // 5 println(t2.length); // 500 println(t3.length); // 1000000
Melkein kaikissa ohjelmointikielissä taulukkoa indeksoidaan hakasulkeilla
"[" ja "]" tyyliin luvut[lokeronNumero]
, mutta Scalassa
käytetään tavallisia kaarisulkeita.
Useimmissa ohjelmointikielissä on erityinen lause, joka soveltuu erityisen hyvin taulukon alkioiden läpikäymiseen. Monissa kielissä se on nimeltään for-lause. Niin myös Scalassa:
for (i <- 5 to 10) print(i + " "); // 5 6 7 8 9 10 println(); // rivinvaihto for (i <- (5 to 10).reverse) print(i + " "); // 10 9 8 7 6 5 println(); // rivinvaihto
Viimeistellään ja yleistetään luvun alun esimerkki, jossa syöttöluvut tulostetaan käänteisessä järjestyksessä:
println("Montako lukua haluat tulostaa käännetyssä järjestyksessä?"); var koko = readInt; var luvut = new Array[Int](koko); // HUOM: taulukon koko saa olla siis myös muuttuja! for (lokeronNumero <- 0 to luvut.length-1) { print("Anna " + (lokeronNumero+1) + ". luku: "); luvut(lokeronNumero) = readInt; } println("Luvut käänteisessä järjestyksessä:"); for (lokeronNumero <- (0 to luvut.length-1).reverse) // arvot luvut.length-1, luvut.length-2, ..., 0 println(luvut(lokeronNumero));
Vaihtelevaan määrään syöttötietoja voidaan varautua eri tavoin. Tarkastellaan esimerkkinä ohjelmaa, joka laskee syötteinä saamiensa kokonaislukujen summan. Tehdään ohjelmasta kolme erilaista versiota:
println("Monenko luvun summa lasketaan?"); var lukuja = readInt; var summa = 0; for (i <- 1 to lukuja) { print("Anna " + i + ". luku? "); var luku = readInt; summa = summa + luku; } println("Lukujen summa on " + summa);
println("Syötä lukuja. Negatiivinen lopettaa."); var summa = 0; var lukuja = 1; print("Anna 1. luku? "); var lukutarjokas = readInt; // jo ensimmäinenkin voi olla loppumerkki while (lukutarjokas >= 0) { summa = summa + lukutarjokas; // viimeksi luettu siis oli varsinainen syöte lukuja = lukuja + 1; print("Anna " + lukuja + ". luku? "); lukutarjokas = readInt; } println("Lukuja oli " + (lukuja-1) + ". Summa on " + summa); // Huom: laskurin korjaus!
var summa = 0; var lukuja = 0; var jatkuu = true; while (jatkuu) { println("Onko vielä lukuja? (k = kyllä, kaikki muu ei)"); var vastaus = readLine; if (vastaus == "k") { lukuja = lukuja + 1; print("Anna " + lukuja + ". luku? "); var luku = readInt; summa = summa + luku; } else jatkuu = false; } println("Lukuja oli " + lukuja + ". Summa on " + summa);
Jo aiemmin nähtiin esimerkki merkkijonomuuttujasta:
var teksti = "juttuhomma"; println(teksti); teksti = "uusjuttu"; println(teksti);
Myös merkkijonot kelpaavat taulukon alkioiksi. Tällöin
alkiotyypin nimi on String
.:
var nimet = new Array[String](3); nimet(0) = "Aku"; nimet(1) = "Iines"; nimet(2) = "Hessu"; for (indeksi <- 0 to nimet.length-1) println(nimet(indeksi));
Taulukon voi määritellä myös luettelemalla alkioiden alkuarvot seuraavaan tapaan:
var henkilot = Array("Matti", "Maija", "Pekka", "Liisa"); var ika = Array( 29, 22, 19, 27); println("Kenen henkilön iän haluat tietää?"); var kuka = readLine; // etsitään indeksi: var indeksi = 0; while (indeksi < henkilot.length && henkilot(indeksi) != kuka) indeksi = indeksi + 1; // Nyt joko löytyi tai indeksi kasvoi yli: if ( indeksi == henkilot.length ) println("Henkilöä " + kuka + " ei löytynyt.") else { var haetunIka = ika(indeksi); println("Henkilön " + kuka + " ikä on " + haetunIka + "."); }
Tässä esimerkissä on paljon muutakin opittavaa kuin tuo taulukon määrittely alkuarvot luettelemalla. Mitä? Mieti, tutki ja opi!
Ohjelmia laadittaessa esiintyy usein tilanne, että jokin toiminto - alialgoritmi - täytyy suorittaa ohjelman useassa eri kohdassa. Alialgoritmi voidaan tietenkin kirjoittaa uudelleen ja uudelleen, mutta tällä on joitakin huonoja seurauksia:
Jo varhaisissa ohjelmointikielissä käytettiin aliohjelmia, nimettyjä algoritmeja, jotka ensin määritellään ja nimetään. Tällaista nimettyä apuvälinettä voidaan sitten nimellä kutsua siellä missä tuota algoritmia tarvitaan, esimerkiksi pääohjelmassa tai jossakin aliohjelmassa. Kutsuminen tarkoittaa siis, että aliohjelma käydään suorittamassa ja suorituksen jälkeen automaattisesti palataan kutsua seuraavaan ohjelmakohtaan.
Mahdollisuus nimetä ja kutsua alialgoritmeja on jo sinänsä arvokasta. Näin voidaan välttyä "koodin kertaamiselta" ja samalla selkeyttää ohjelmaa. Tärkeä lisäominaisuus on mahdollisuus antaa aliohjelmille lähtötietoja, ns. parametreja, jotka ohjaavat alialgoritmin suoritusta.
Nimetyt alialgoritmit voivat olla "komentoja tehdä jotakin" samaan tapaan
kuin vaikkapa println
osaa kirjoittaa parametrinsa ohjelman
käyttäjän kuvaruudulle.
Alialgoritmeja voi kirjoittaa myös sellaisiksi, että ne laskevat jonkin arvon ja palauttavat tuon arvon. Esimerkkejä tällaisista ovat vaikkapa trigonometriset funktiot, jotka useimmista ohjelmointikielistäkin löytyvät.
Vaikka vain jälkimmäiset muistuttavat matematiikan funktioita, eräissä ohjelmointikielissä myös komennon kaltaisia aliohjelmia kutsutaan "funktioiksi". Java-maailmassa nimettyjä aliohjelmia tavataan kutsua "metodeiksi", koska niillä on tärkeä merkitys olio-ohjelmoinnissa. Tästä lisää myöhemmin.
Nimetyt aliohjelmat olkoot toistaiseksi "funktioita".
Funktio määritellään def
-määreellä.
Ensin esimerkki "komennon kaltaisesta" aliohjelmasta.
Funktion nimen tarkoittama algoritmi kirjoitetaan alltosulkeiden
väliin:
def tervehdi() { // määrittely println("Moi!"); } println("Kutsutaanpa tervehdystä:") tervehdi(); // kutsu println("Kutsutaanpa kolmasti:"); tervehdi(); tervehdi(); tervehdi();
Funktion määrittely ei vielä johda sen algoritmin suorittamiseen. Vasta kutsu – nimi mainiten – johtaa algoritmin suorittamiseen.
Arvon palauttava aliohjelma määritellään samaan tapaan, mutta
määrittelyssä käytetään (sinänsä luontevasti) yhtäsuuruusmerkkiä
funktion nimen ja merkityksen välissä. Palautettava arvo
ilmaistaan return
-lauseella.
Lisäksi myös funktion paluuarvon tyyppi pitää mainita esimerkin mukaisesti.
Tässä vaiheessa käytämme paluuarvojen tyyppeinä vain kolmea
Int
, Double
ja String
.
def onnenluku():Int = { // määrittely return 14; } println("Onnenluku = " + onnenluku()); // kutsu println("Ja kolmeen kertaan: " + (onnenluku() + onnenluku() + onnenluku()));
Huom: Näissä kahdessa esimerkissä funktioilla ei ole parametreja. Silti sekä määrittelyssä että kutsussa niiden nimen yhteyteen kirjoitettiin "tyhjät" kaarisulut. Scalassa tämä ei olisi välttämätöntä, mutta koska Java ne vaatii, niitä on hyvä käyttää jo nyt.
Funktiolle siis voi antaa lähtötietoja parametrien avulla. Idea on seuraava:
def tervehdi(kuka: String) { ... // muodollisen parametrin määrittely
println("Moi " + kuka + "!"); // muodollisen parametrin käyttö algoritmissa
tervehdi("Aku"); // kutsussa annetaan todellinen parametri
Annettava arvo voi olla myös muuttujan arvo tai laskutoimituksen tulos:
var i = 7; var nimi = "Jaska Jokunen"; tervehdi(nimi + ", lukusi on " + (i+2));
Yleistetty tervehtimisalgoritmi ja sitä esittelevä ohjelma kokonaisuudessaan:
def tervehdi(kuka: String) { // määrittely println("Moi " + kuka + "!"); } println("Kutsutaanpa tervehdystä:") tervehdi("Aku"); // kutsu println("Kutsutaanpa kolmasti:"); tervehdi("Iines"); tervehdi("Minni"); tervehdi("Mikki"); var i = 7; var nimi = "Jaska Jokunen"; tervehdi(nimi + ", lukusi on " + (i+2)); // tulostus: Moi Jaska Jokunen, lukusi on 9!
Tietenkin myös arvon palauttavalle aliohjelmalle voi antaa parametreja:
def tuplaa(luku: Int):Int = { // määrittely return 2 * luku; } println("Kolme tuplana = " + tuplaa(3)); // kutsu var i = 8; var j = tuplaa(i); println( tuplaa(j) + tuplaa(2) ); println(tuplaa(tuplaa(tuplaa(1))));
Parametreja voi toki olla useampiakin:
def onnittele(kuka: String, kertaa: Int) { for (i <- 1 to kertaa) println( "Onnea " + kuka + "!"); } onnittele("Liisa", 3); onnittele("Pekka", 2);
def neliosumma(a: Int, b: Int): Int = { var summa = a + b; var nelio = summa * summa; return nelio; } println(neliosumma(1, 1)); println(neliosumma(3, 2));
Funktion eli aliohjelman sisällä määritellyt muuttujat eivät koskaan näy funktion oman lohkon ulkopuolelle. Tällaisia muuttujia kutsutaan funktion paikallisiksi muuttujiksi. Ne ovat käytettävissä vain metodin omassa algoritmissa.
Funktion paikalliset muuttujat ja muodolliset parametrit itse asiassa syntyvät, kun funktiota kutsutaan. Ja näille varattu tila vapautetaan, kun funktion suoritus päättyy. Koska paikalliset muuttujat eivät säily suorituskerrasta toiseen, niiden avulla myöskään "ei voi muistaa mitään" seuraavalle suorituskerralle.
Java-kielessä pätee sääntö: Mistään aliohjelmasta ei koskaan voi nähdä minkään toisen aliohjelman paikallista muuttujaa. Scalassa asia ei ole ihan näin yksinkertainen, mutta siitä ei tällä kurssilla tarvitse tietää mitään.
Myös taulukon voi välittää parametrina, mikä usein on näppärää:
def asetaJokaAlkiolle(taulu: Array[Int], arvo: Int) { for (i <- 0 to taulu.length-1) taulu(i) = arvo } var t = new Array[Int](10); println(t(7)); // 0 asetaJokaAlkiolle(t, 100); println(t(7)); // 100
Esimerkki peräkkäishausta: funktio etsii taulukosta yksi kerrallaan kysyttyä arvoa ja palauttaa vastaavan indeksin ensimmäisestä löytyneestä. Jos päästään talukon loppuun löytämättä haettua arvoa, palautetaan arvo -1, joka ei kelpaa taulukon indeksiksi. Se viestii, ettei haettua arvoa löytynyt:
def mikaOnArvonIndeksi(etsittava: Int, taulu: Array[Int]): Int = { for (i <- 0 to taulu.length-1) if (taulu(i) == etsittava) return i; // jos tänne päästään, ei löytynyt return -1; // -1 ei voi olla indeksi } var t = Array(2, 4, 6, 8, 10, 12, 14); println("Luvun 8 indeksi on " + mikaOnArvonIndeksi(8, t)); println("Luvun 7 indeksi on " + mikaOnArvonIndeksi(7, t)); if (mikaOnArvonIndeksi(8, t) != -1) println("8 löytyy"); else println("8 ei löydy"); if (mikaOnArvonIndeksi(7, t) != -1) println("7 löytyy"); else println("7 ei löydy");