Helsingin yliopisto / Tietojenkäsittelytieteen laitos
Copyright © Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

1.1 Tietokoneita ja algoritmeja

(Muutettu viimeksi 8.9.2009)

Tietokone ja ohjelma

Tietokone on rakennettu "osaamaan" yksinkertaisia operaatioita, jotka käsittelevät tietokoneen komponenteissa olevia bittien jonoja. Tuollaista operaatiojoukkoa kutsutaan konekieleksi. Operaatiot itsekin esitetään tietokoneen muistissa bittijonoina.

Konekielinen ohjelmointi (yleensä ns. "assemblerilla") on hyvin kömpelöä ja virhealtista. Käytännössä nykyään lähes aina ohjelmoidaan ns. lausekielillä. Niitä käyttäen ohjelmoija välttyy koneen sisäisen rakenteen ajattelemiselta. Ohjelmista saadaan lausekielellä ohjelmoiden myös siirrettäviä: ne ovat käytettävissä kaikissa tietokoneissa, joille ko. lausekieli on toteutettu.

Lausekielten ajattelutapa on konekieltä paljon ihmisläheisempi: jo ensimmäiset lausekielet mahdollistivat matematiikasta tuttujen laskutoimitusten kirjoittamisen; voitiin kirjoittaa vaikkapa A+B*C sen sijaan, että olisi itse kerrottu, mistä koneen osasta minne bittijonoja siirrellään ja minkä bittijonojen välillä käynnistetään peruslaskutoimituksia. Sittemmin lausekieliin on kehitelty monenlaisia ajatusten järjestämisen välineitä: nimettyjä aliohjelmia ja aliohjelmakirjastoja, lohkorakennetta, olioita ja luokkia, ...

Jotta tietokone saataisiin suorittamaan lausekielellä laadittuja ohjelmia, lausekieliset ohjelmat on tavalla tai toisella kuitenkin muokattava konekielisiksi - tietokonehan ei muuta "osaa" kuin vain konemaisesti suorittaa muistissaan olevaa konekielisten operaatioiden jonoa. Tuollaista muokkaamista kieleltä toiselle kutsutaan kääntämiseksi.

Lausekielisen ohjelman kääntäminen konekielelle on aika työlästä ja mekaanistakin puuhaa, joten se sopii mainiosti tietokoneelle: ohjelmaa, jonka syöttötietoina on algoritmi lausekielisenä ja tulostietoina vastaava algoritmi vaikkapa konekielisenä, kutsutaan kääntäjäksi (compiler).

                                         syöttötiedot
                                      omalle algoritmille 
                        
                                             |
                                             V

   oma algoritmi       KÄÄNTÄJÄ          OMA ALGORITMI
   lausekielellä  --> KONEKIELELLÄ  -->  KONEKIELELLÄ

                                             |
                                             V

                                       oman algoritmin
                                        tulostiedot


Kääntäjä usein ohjelmoidaan itsekin lausekielellä. Miten se itse saadaan käännettyä konekieliseksi? Tietokonehan ei muunlaisia algoritmeja osaa suorittaa!

Ohjelmointikieli voidaan toteuttaa myös tulkin (interpreter) avulla. Tällöin konekielinen tulkkiohjelma suorittaa lausekielistä algoritmia "enemmän tai vähemmän sellaisenaan":

                                       syöttötiedot
                                    omalle algoritmille 
                        
                                           |
                                           V

              oma algoritmi              TULKKI
              lausekielellä  -->      KONEKIELELLÄ

                                           |
                                           V

                                      oman algoritmin
                                       tulostiedot


Lausekielisen ohjelman tulkkaaminen "enemmän tai vähemmän sellaisenaan" on joustavaa, mutta se saattaa olla hidasta monestakin syystä. Yksi mahdollisuus tehostaa tulkintaa on ns. välikielen käyttäminen. Välikieli voi olla käsitetasoltaan konekielen kaltaista, mutta todellisia konekieliä yksinkertaisempaa. Jos välikielen operaatiot esitetetään tietokoneen muistin tavuina (yleensä 8 bittiä), kieltä kutsutaan tavukoodiksi. Java on toteutettu juuri näin. Javan tavukoodin nimi on Bytecode.

                                                              syöttötiedot
                                                            omalle algoritmille 
                        
                                                                   |
                                                                   V

 oma algoritmi      VÄLIKIELIKÄÄNTÄJÄ       OMA ALGORITMI      VÄLIKIELITULKKI
 lausekielellä  --> KONEKIELELLÄ TAI   -->  VÄLIKIELELLÄ  -->  KONEKIELELLÄ
                    VÄLIKIELELLÄ
                                                                   |
                                                                   V

                                                             oman algoritmin
                                                              tulostiedot


Lausekieltä välikielelle kääntävä ohjelma voi tietenkin olla konekielinen, mutta se voi olla myös välikielellä ilmaistu! Miten kääntäjä tässä tapauksessa saadaan suoritettua tietokoneessa, joka tietenkin osaa vain omaa konekieltään?

Algoritmi ja sen tila

Algoritmisen ohjelmoinnin peruskäsitteitä ovat muuttuja (variable) ja sijoitusoperaatio (assignment operation).

Muuttuja tarkoittaa matematiikassa jotakin melko syvällistä: esimerkiksi ilmauksessa a+b=b+a väitetään vaikkapa minkä tahansa lukuparin summan olevan riippumaton laskentajärjestyksestä. Tilastotieteessä muuttuja voi tarkoittaa "selittävää tekijää"; esimerkiksi myytyjen jäätelöiden määrä voisi olla muuttujana hukkumiskuolemien määrää selitettäessä.

Algoritmisessa ohjelmoinnissa muuttuja on paljon konkreettisempi asia: muuttuja on arvon säilytyspaikka, "laatikko, jonka kyljessä on nimi". Laatikon sisältöa voi tutkia ja sisältöä voi muuttaa. Uuden sisällön asettamista laatikkoon kutsutaan sijoittamiseksi, sijoitusoperaatioksi tai sijoituslauseeksi.

Sijoitus korvaa muuttujan vanhan sisällön uudella. Uusi arvo voi olla jonkin laskutoimituksen, ns. lausekkeen (expression) arvo. Tuo lauseke voi sisältää vakioita, muuttujien arvoja ja laskutoimituksia.

Sijoitusoperaation ilmauksena algoritmeja kirjoitettaessa käytetään usein merkkijonoa ":=". Myös ilmausta "<-" näkee joskus. Valitettavasti eräissä ohjelmointikielissä (mm. Javassa!) sijoitusoperaation merkkinä käytetään yhtäsuuruusmerkkiä "=". Näissä kielissä matematiikasta tuttuun yhtäsuuruuden vertailuun joudutaan siten käyttämään matematiikan kielestä poikkeavaa ilmausta, esimerkiksi kahta yhtäsuuruusmerkkiä peräkkäin "==". Tässä oppimateriaalissa käytetään Javan tapaan sijoitusoperaation ilmaisemiseen yhtäsuuruusmerkkiä. :-(

Huom: Algoritmiesimerkeissä käytetään Scala-ohjelmointikieltä toukokuusta 2008 lähtien! Tämän Java-pitoisen kurssin materiaalia Scala-kielellä (ja muuta Scala-tietoa) löytyy alkaen sivultani: Scala.

Esimerkki:
(Ilmaus var eka tarkoittaa, että määritellään muuttuja ("variable") nimeltä eka.)

  var eka = 7
  var toka = 9 * eka
  var kolm = (eka + toka) * 100
  eka = eka + 1
Huom: Sijoitusoperaatio kannattaa heti alussa opetella lukemaan "saa arvokseen", ei "on", vaikka ohjelmointikielessä Javan tapaan käytettäisiin yhtäsuuruusmerkkiä sijoitusoperaation ilmauksena! Erityisen hyvin tuo viimeinen esimerkki osoittaa, miksi.
     eka = eka + 1
Ilmaus "eka on eka plus yksi" on looginen epätotuus. Sitävastoin "eka saa arvokseen eka plus yksi" ilmaisee, mistä on kysymys: ekan vanhaan arvoon lisätään yksi ja saatu summa sijoitetaan muuttujan eka uudeksi arvoksi.

Algoritmia siis suoritetaan ajassa: "ensin tehdään sitä, sitten tätä". Algoritmin muuttujien arvot kullakin ajan hetkellä muodostavat algoritmin ns. tilan (state). Ja oikeastaan kaikki tietokoneohjelmat ovat lopulta vain algoritmeja, jotka etenevät tilasta toiseen. Jos ohjelmassa näyttäisi olevan jotakin "viisautta", kyse on siitä, että ohjelman laatija on osannut rakentaa algoritmilleen sellaisen tilojen ketjun, joka ohjelman käyttäjästä vaikuttaa viisaalta.

Edellisen esimerkin tilat:


tila:     |__?____|  |__?____|  |__?____| 
            eka        toka       kolm

     eka = 7

tila:     |__7____|  |__?____|  |__?____| 
            eka        toka       kolm

     toka = 9 * eka

tila:     |__7____|  |__63___|  |__?____| 
            eka        toka       kolm

     kolm = (eka + toka) * 100

tila:     |__7____|  |__63___|  |__7000_| 
            eka        toka       kolm

     eka = eka + 1

tila:     |__8____|  |__63___|  |__7000_| 
            eka        toka       kolm


Algoritmi liitetään ympäristöönsä ns. syöttö- ja tulostusoperaatioin. Syöttöoperaation eli lukemisen seurauksena jokin muuttuja saa arvon ohjelman käyttäjältä tai vaikkapa jostakin tiedostosta. Lukeminen siis muuttaa ohjelman tilaa. Tulostusoperaatio, kirjoittaminen puolestaan laskee jonkin lausekkeen arvon ja siirtää tuloksen jollekin tulostuslaitteelle. Tällöin ohjelman tila ei muutu.

Syöttötietojen ajatellaan olevan syöttöjonossa, josta ne yksi kerrallaan luetaan. Tulostietojen ajatellaan puolestaan olevan tulosjonossa, johon ne yksi kerrallaan kirjoitetaan. Nämä jonot voivat ihan konkreettisestikin olla "jonoja" jossakin tiedostossa, mutta esimerkiksi vuorovaikutteisen ohjelman syötteiden ja tulosteiden "jonot" ovat pikemminkin jonoja ajassa.

Esimerkki:

  println("Anna kaksi lukua")
  var eka  = readInt
  var toka = readInt
  var kolmas = eka + toka
  println("Niiden summa on")
  println(kolmas)

Jos tämän ohjelman syöttöjonossa on luvut 3 ja 4, muuttujaan eka luetaan arvo 3, muuttujaan toka arvo 4. Sitten muuttujaan kolmas sijoitetaan summa eka+toka eli arvo 7. Lopuksi ohjelma kirjoittaa tulosjonoon arvon 7. Näin "jonot" ovat siis ohjelman käyttäjän näkökulmasta toimintoja näytön ja näppäimistön äärellä.

Peräkkäin kirjoitetut operaatiot siis suoritetaan peräkkäin - "vasemmalta oikealle, ylhäältä alaspäin". Alkeisoperaatioiden (sijoitus, lukeminen, kirjoittaminen) suorittamista ohjataan ns. rakenteisin operaatioin. Monet rakenteiset operaatiot perustuvat siihen, että väitetään jotakin algoritmin tilasta, ja jos väite on tosi, "tehdään jotakin", jos epätosi, "tehdään jotakin muuta". Esimerkkejä tällaisista ovat valinta ja toisto:

Esimerkki valinnasta:

  println("Anna luku. Tutkin, onko se positiivinen")
  var luku = readInt
  if (luku>0)             // valinta
    println("Luku on positiivinen.")
  else
    println("Luku ei ole positiivinen.")

Esimerkki toistosta:
  println("Moneenko kertaan haluat saada onnittelut?")
  var määrä = readInt
  var monesko = 0
  while (monesko < määrä) {      // toisto
    println("Onnea!")
    monesko = monesko + 1
  }

Mitä seuraava ohjelmointivirheen sisältävä ohjelma tekee kun sille syötetään positiivinen luku? Entä kun sille syötetään negatiivinen luku? Simuloi ja opi! Mitä opit? Esimerkki toistosta:
  println("Moneenko kertaan haluat saada onnittelut?")
  var määrä = readInt
  var monesko = 0
  while (monesko > määrä) {      // toisto
    println("Onnea!")
    monesko = monesko + 1
  }

Huom: Rakenteiset operaatiot ovat "rakenteisia" siksi, että niiden rakenneosina on muita lauseita. Noita rakenneosia voidaan kutsua alialgoritmeiksi: "Toistolause toistaa alialgoritmia..."

Lady Ada Lovelace, lordi Byronin tytär, keksi valinnan ja toiston 1800-luvun lopulla. Hän laati ohjelmia Charles Babbagen suunnittelemaan mekaaniseen tietokoneeseen, jossa ohjelmat suoritettiin suoraan reikäkorttipakalta. Lady Ada keksi täydentää konekieltä operaatioilla, jotka jostakin ehdosta riippuen hyppäävät joidenkin korttien ohi tai jotka siirtävät jo ohitetun korttipakan osan uudelleen luettavaksi!

Ehdollisuus - algoritmin tilasta riippuva toiminnan ohjaus - on keskeistä algoritmeja laadittaessa. Ilman ehdollisuutta algoritmi toimisi samoin kaikilla syöttötiedoilla! Alialgoritmien nimeäminen ja nimettyjen alialgoritmien kutsuminen on myös tärkeä väline: Kerran ohjelmoitua alialgoritmia ei tarvitse kirjoittaa uudelleen ja uudelleen.

Ajatusten järjestämistä ja turvallisuuden tavoittelua

Teoriassa algoritmien laatimiseen ei tarvittaisi muita kuin yllä luetellut välineet. Ja itse asiassa selvittäisiin vieläkin vähemmällä: jopa konekielellä tai ns. Turingin koneella (matemaattinen algoritmimalli) voitaisiin ohjelmoida maailman kaikki mahdolliset algoritmit.

Käytännössä ohjelmat kuitenkin ovat niin suuria ja monimutkaisia, että algoritmin laatijan ajatusten hallintaan tarvitaan välineita. Jollakin tavoin on voitava rajoittaa kerralla ajateltavien asioiden määrä niin pieneksi, että ajatusten voima vielä riittää oikean toimintalogiikan laatimiseen ja ohjelmointivirheiden välttämiseen.

Jo edellä mainitut valinta ja toisto sekä alialgoritmien nimeäminen ovat välineitä ohjelmointiongelman jäsentelyyn. Muita ovat mm. ns. lohkorakenne, oliot, luokat, kirjastot, ...

"Oikean" ohjelman tila on tyypillisesti hyvin suuri, ts. muuttujia on hyvin paljon. Kuitenkin useimmilla muuttujilla on merkitystä vain jossakin ohjelman osassa. Jäsentelyn välineet yleensä antavat keinoja myös muuttujien ja ohjelman muun kaluston käytettävyyden säätelyyn, ns. nimien näkyvyyden säätelyyn.

Rakenteiset lauseet ja lohkorakenne liittyvät ns. osittavaan ongelmanratkaisuun (stepwise refinement, "top down" -suunnittelu) ja ns. rakenteiseen ohjelmointiin (Algol, Pascal, ...).

Oliot, niiden luokat ja luokkien hierarkia puolestaan ovat ns. olio-ohjelmoinnin perusvälineitä. Ne täydentävät rakenteisen ohjelmoinnin ajatteluvälineitä toisaalta kokoavalla ongelmanratkaisulla (abstraktit tietotyypit, abstraktit koneet, "bottom up" -suunnittelu), toisaalta ongelmamaailman mallinnusvälineillä ("Missu on kissa, kissa on imettäväinen, imettäväinen on selkärankainen, selkärankainen kuuluu 'eläinkuntaan'") (Smalltalk, Modula, C++, Java, ...)

Tietokoneessa kaikki tiedot, muuttujien arvot, esitetään bittijonoina ja koneen kannalta bittijono on vain bittijono. Mutta ohjelmoijan kannalta jotkin bittijonot esittävät kokonaislukuja, jotkin desimaalilukuja, jotkin merkkejä, jotkin totuusarvoja, jotkin merkkijonoja, jotkin kokonaislukumatriiseja, ... Ohjelmoijan kannalta siis muuttujien arvoilla ja samalla itse muuttujilla on jokin tyyppi (type). Vanhanaikaisilla ohjelmointikielillä ohjelmoidessa tyypillinen virhe oli vahingossa käsitellä vaikkapa desimaaliluvun bittiesitystä kokonaisluvun tavoin, kokonaislukua totuusarvon tavoin jne.

Tuollaisia virheitä oli vaikea välttää, mutta sitten keksittiin hyvä ajatus: Miksei panna tietokonetta tarkistamaan, että arvoja käytetään ohjelmoijan tarkoittamalla tavalla. Juuri tuollainen mekaaninen tarkistustyöhän sopii vallan erinomaisesti juuri koneelle.

Edellytys oli, että ohjelmoija ilmoittaa ennalta, mihin aikoo mitäkin muuttujaa käyttää. Kieliin otettiin mukaan muuttujien määrittelyt: esitellään ennalta, millaiseen tarkoitukseen kutakin muuttujaa käytetään. Sitten ohjelmointijärjestelmä (kääntäjä ja käännetty ohjelma) pyrkii tarkastamaan ja varmistamaan, ettei ohjelmoija yritä sijoittaa muuttujalle vääräntyyppistä arvoa. Tätä nimitetään vahvaksi tyypitykseksi.


Takaisin luvun 1 sisällysluetteloon.