Skip to content

Latest commit

 

History

History
608 lines (531 loc) · 19.6 KB

rekursio.org

File metadata and controls

608 lines (531 loc) · 19.6 KB

Rekursio

{{{example}}}

Alla oleva kuva voitaisiin piirtää for- tai while-silmukan avulla. Se voidaan kuitenkin piirtää myös ilman silmukoita määrittelemällä funktio, joka kutsuu itse itseään.

// piirretään parametrin ympyroita mukainen lukumäärä sisäkkäisiä
// ympyröitä siten, että uloimman halkaisija on d ja uloimman väri on
// joko valkoinen tai musta; sisäkkäisten ympyröiden halkaisijoiden
// erotus on 30
void piirraYmpyrat (int ympyroita, float d, boolean valkoinen)
{
  // palataan jos on piirretty jo riittävästi
  if (ympyroita == 0)
    return;

  // valkoinen tai musta ympyrä
  if (valkoinen)
    fill (100);
  else
    fill (0);

  // piirretään ympyrä
  ellipse (width / 2.0, height / 2.0, d, d);

  // jatketaan piirtämistä, seuraavaksi pienempi jolla eri väri
  piirraYmpyrat (ympyroita - 1, d - 30, !valkoinen);
}

void setup ()
{
  size (400, 400);
  colorMode (HSB, 100);
  noLoop ();
}

void draw ()
{
  // piirretään 5 ympyrää, joista uloin on piirtoikkunan levyinen ja
  // väriltään musta
  piirraYmpyrat (5, width, false);
}
<<rekursio-sisakkaiset-ympyrat>>

Alla oleva kuva esittää, miten ohjelman aikana funktiota piirraYmpyrat() kutsutaan eri arvoilla. Viimeisessä kutsussa parametrin ympyroita arvo on 0, jolloin funktio ei enää kutsu itseään uudestaan. Kuvaa kutsutaan rekursiopuuksi - nimityksen syy selviää pian.

size (18cm, 0);
usepackage ("qtree");
usepackage ("amsmath");
defaultpen (fontsize (14));
texpreamble ("\newcommand{\Circles}[1]{\texttt{piirraYmpyrat (#1)}}");

label ("\Tree [ .{\Circles{5, 400, false}} [ .{\Circles{4, 370, true}} [
    .{\Circles{3, 340, false}} [ .{\Circles{2, 310, true}} [
    .{\Circles{1, 280, false}} [ .{\Circles{0, 250, true}} ] ]
    ] ] ] ]", (0, 0));
  

rekursio-sisakkaiset-ympyrat-kutsupuu.svg

Rekursio ja rekursiivinen funktio

  • Rekursiossa tehtävä tai ongelma ratkaistaan pilkkomalla se pienempiin, samanlaisiin tehtäviin. Yllä olevassa esimerkissä alkuperäinen tehtävä

    piirrä 5 sisäkkäistä ympyrää

    ratkaistaan piirtämällä ensin uloin ympyrä ja ratkaisemalla sen jälkeen alkuperäisen ongelman kanssa samanlainen mutta pienempi ongelma

    piirrä 4 sisäkkäistä ympyrää.

    Pienempään ongelmaan siirtymistä kutsutaan rekursioaskeleeksi. Jotta rekursio päättyisi joskus, tarvitaan perustapaus, jota ei enää pilkota pienempiin osiin. Yllä olevassa esimerkissä perustapaus on tilanne, jossa kaikki ympyrät on piirretty.

  • Rekursiivinen funktio on ohjelmoinnin tapa toteuttaa rekursio. Rekursiivinen funktio kutsuu itseään (rekursioaskel). Jotta rekursio päättyisi joskus, funktiossa täytyy olla myös lopetusehto (perustapaus). Yllä olevassa esimerkissä lopetusehto täyttyy silloin, kun kaikki ympyrät on piirretty, eli kun parametrin ympyroita arvo on 0.

{{{example}}}

Fibonaccin lukujono on kuuluisa esimerkki rekursiosta. Lukujono alkaa

0, 1, 1, 2, 3, 5, 8, 13, 21

Lukujonon kaksi ensimmäistä lukua ovat siis 0 ja 1, ja muut luvut ovat kahden edellisen luvun summa.

Esimerkkien helpottamiseksi sovitaan, että tässä kappaleessa Fibonaccin lukujen indeksi alkaa nollasta, eli lukujonon ensimmäiset jäsenet ovat: \begin{align*} f_0 &= 0
f_1 &= 1\ f_2 &= 1\ f_3 &= 2\ f_4 &= 3\ f_5 &= 5 \end{align*} Alla oleva rekursiivinen ohjelma piirtää piirtoikkunan keskelle lukujonon luvun \(f_6.\)

// palauttaa luvun f_n
int fib (int n)
{
  // perustapaukset
  if (n == 0 || n == 1)
    return (n);

  // rekursioaskel
  return (fib (n - 1) + fib (n - 2));
}

<<bonacci-setup>>

void draw ()
{
  text (fib (6), width / 2.0, height / 2.0);
}

Funktiokutsun fib (6) rekursiopuu näyttää seuraavalta.

usepackage ("qtree");
defaultpen (fontsize (14));
string fibtree (int n)
{
  string treeBeg, treeEnd, leaves;
  treeBeg = format (" [ .{\texttt{fib (%d)}} ", n);
  treeEnd = " ] ";
  if (n == 0 || n == 1)
    leaves = format (" {\texttt{%d}} ", n);
  else
    leaves = " [ .+ " + fibtree (n - 1) + fibtree (n - 2) + " ] ";
       
  return (treeBeg + leaves + treeEnd);
}
label ("\Tree " + fibtree (6));

Taulukointi

Funktiokutsun fib (6) rekursiopuusta huomataan, että esimerkiksi $f_4$ lasketaan kahteen kertaan, $f_3$ kolmeen kertaan jne. Ohjelmassa on siis paljon ylimääräistä laskentaa, josta seuraa ongelmia laskettaessa jonoa pidemmälle.

Yksi tapa ongelman välttämiseen on taulukointi, jossa luku $f_n$ tallennetaan taulukkoon kun se lasketaan ensimmäisen kerran. Alla olevassa ohjelmassa tämä taulukko on f. Taulukko alustetaan ohjelman alussa luvuilla \(-1\), jotta ohjelmassa voidaan tunnistaa onko arvo jo laskettu. Ohjelmassa lasketaan ja piirretään Fibonaccin lukujonon 16 ensimmäistä lukua.

int[] f; // taulukko arvojen tallentamiseksi

// palauttaa luvun f_n
int fib (int n)
{
  // jos arvoa f_n ei ole vielä laskettu...
  if (f [n] < 0)
  {
    // lasketaan f_n ja tallennetaan taulukkoon
    if (n == 0 || n == 1)
      f [n] = n; // perustapaukset
    else
      f [n] = fib (n - 1) + fib (n - 2); // rekursioaskel
  }

  return (f [n]); // palautetaan arvo taulukosta
}

<<bonacci-setup>>

void draw ()
{
  final int N = 15; // lasketaan f_0, f_1, ..., f_15

  // varataan tila taulukolle ja alustetaan taulukko luvuilla -1
  f = new int [N + 1];
  for (int i = 0; i <= N; i++)
    f [i] = -1;

  for (int n = 0; n <= N; n++)
    text (fib (n), (n + 1) * 35, height / 2.0); // 35 pikselin välein
}

{{{example}}}

Alla olevassa kaaviossa toistuu sama rakenne: kaavion yläosasta lähtee kaksi haaraa, joista kukin jakautuu kahteen haaraan, joista kukin jakautuu edelleen kahteen jne.

// piirretään kaavio, jossa on annettu määrä tasoja; kaavion yläreunan
// keskikohta on annetuissa koordinaateissa (x, y), ja ensimmäisenä
// piirrettävän tason vaakaviivan pituus on parametri leveys
void piirraKaavio (int tasoja, float x, float y, float leveys)
{
  if (tasoja == 0)
    return;

  // piirretään tämän tason vaakaviiva ja pystyviivat
  float p = leveys / 2; // puolet leveydestä
  float xVasen = x - p; // seuraavan tason vasemman haaran x-koordinaatti
  float xOikea = x + p; // seuraavan tason oikean haaran x-koordinaatti
  line (xVasen, y, xOikea, y); // vaakaviiva
  float yUusi =  y + p; // seuraavan tason y-koordinaatti
  line (xVasen, y, xVasen, yUusi); // vasen pystyviiva
  line (xOikea, y, xOikea, yUusi); // oikea pystyviiva

  // rekursioaskel: piirretään vasen ja oikea haara, joissa molemmissa
  // yksi taso vähemmän
  int tasojaJaljella = tasoja - 1;
  piirraKaavio (tasojaJaljella, xVasen, yUusi, p); // vasen haara
  piirraKaavio (tasojaJaljella, xOikea, yUusi, p); // oikea haara
}

void setup ()
{
  size (600, 400);
  colorMode (HSB, 100);
  background (0);
  stroke (100);
  noLoop ();
}

void draw ()
{
  // kaavio, jossa on 7 tasoa; ylin taso sivusuunnassa kuvan keskellä
  // ja kuvan yläreunasta 1/7 alaspäin; ylimmän tason leveys puolet
  // piirtoikkunan leveydestä
  piirraKaavio (7, width / 2.0, height / 7.0, width / 2.0);
}

Kuvan piirtävä ohjelma voidaan kirjoittaa rekursiivisesti. Esimerkiksi 7-tasoinen kaavio voidaan piirtää

  1. piirtämällä ensin ylimmän tason vaakaviiva ja kaksi pystyviivaa
  2. piirtämällä sen jälkeen vasen ja oikea kaavio, joissa molemmissa on 6 tasoa.

Näin on tehty alla olevassa ohjelmassa.

<<rekursio-kaavio>>

Tehtäviä

  1. Alla olevassa kuvassa vasemmanpuoleisen pystyviivan korkeus on 350 ja kunkin seuraavan pystyviivan korkeus on 10 % edellistä pienempi. Pystyviivojen etäisyys \(x\)-akselilla on 10 pikseliä.

    Tee vastaava ohjelma kopioimalla alla oleva koodi Processing-editoriin ja kirjoittamalla rekursiivisen funktion piirraPystyviivat() runko: poista rivi

    <<rekursio-pystyviivat-runko>>
        

    ja kirjoita tarvittavat rivit tilalle.

    // piirretään 'viivoja' kappaletta pystyviivoja siten, että
    // vasemmanpuoleisen pystyviivan korkeus on 'korkeus' ja
    // x-koordinaatti 'x'
    void piirraPystyviivat (int viivoja, float x, float korkeus)
    {
      <<rekursio-pystyviivat-runko>>
    }
    
    void setup ()
    {
      size (600, 400);
      noLoop ();
    }
    
    void draw ()
    {
      piirraPystyviivat (50, 10, 350);
    }
        
  2. Alla oleva kuva esittää eräänlaista satunnaiskävelyä, joka etenee seuraavasti.
    • Kävely lähtee liikkeelle origosta.
    • Kullakin askeleella sennhetkiseen \(x\)-koordinaattiin lisätään satunnaisluku, jonka arvo on vähintään 0 mutta pienempi kuin 15. Sama tehdään \(y\)-koordinaatille. Näin saadusta askeleesta piirretään jana.

    Kirjoita alla olevan ohjelmakoodin rekursiivisen funktion satunnaiskavely() runko, eli poista rivi

    <<rekursio-satunnaiskavely-runko>>
        

    ja kirjoita tarvittavat rivit tilalle.

    // piirretään satunnaiskävely, joka alkaa pisteestä (x, y) ja jossa on
    // 'askeleita' askelta
    void satunnaiskavely (int askeleita, float x, float y)
    {
      <<rekursio-satunnaiskavely-runko>>
    }
    
    void setup ()
    {
      size (400, 400);
      noLoop ();
    }
    
    void draw ()
    {
      satunnaiskavely (30, 0, 0);
    }
        
  3. Kirjoita rekursiota käyttävä ohjelma, joka piirtää alla olevan kaltaisen kuvan.
    final int VALI = 30; // lukujen välimatka kuvassa
    
    void piirraLuvut (int luku, float x)
    {
      if (luku < 0)
        return;
    
      text (luku, x, height / 2.0);
      piirraLuvut (luku - 1, x + VALI);
    }
    
    void setup ()
    {
      size (600, 200);
      colorMode (HSB, 100);
      textAlign (CENTER, CENTER);
      textSize (16);
    
      background (0);
      fill (100);
      noLoop ();
    }
    
    void draw ()
    {
      piirraLuvut (15, VALI);
    }
    
        
  4. Kolme ensimmäistä tribonaccin lukujonon lukua ovat 0, 0 ja 1, ja muut luvut ovat kolmen edellisen luvun summa. a) Kirjoita rekursioon perustuva ohjelma, joka piirtää piirtoikkunan keskelle tribonaccin lukujonon 7. luvun. Voit aloittaa kopioimalla ohjelmakoodin Fibonaccin lukujonoesimerkistä. b) Lisää ohjelmaasi taulukointi ja kirjoita ohjelma joka piirtää 16 ensimmäistä tribonaccin lukujonon lukua.
  5. Tee rekursioon perustuva ohjelma, joka piirtää alla olevan “puun”.
    • Puun alin haarautumiskohta on kuvan keskellä alareunassa.
    • Puun seuraavat haarautumiskohdat saadaan siirtymällä nykyisestä haarautumiskohdasta yhtä pitkä matka sivusuunnassa ja ylöspäin. Alussa tämä “askel” on 100 pikseliä, ja se puolittuu joka tasolla.
    • Puussa on 6 tasoa, missä kukin taso sisältää haarautumiskohdan sekä siitä lähtevät kaksi haaraa.
    void piirraPuu (int tasoja, float x, float y, float askel)
    {
      if (tasoja == 0)
        return;
    
      float xVasen = x - askel; // vasemman haaran pää
      float xOikea = x + askel; // oikean haaran pää
      float yUusi = y - askel; // molempien päiden y-koordinaatti
    
      line (x, y, xVasen, yUusi); // vasen haara
      line (x, y, xOikea, yUusi); // oikea haara
    
      // rekursioaskel
      int tasojaJaljella = tasoja - 1;
      float uusiAskel = askel / 2;
      piirraPuu (tasojaJaljella, xVasen, yUusi, uusiAskel);
      piirraPuu (tasojaJaljella, xOikea, yUusi, uusiAskel);
    }
    
    void setup ()
    {
      size (600, 400);
      noLoop ();
    }
    
    void draw ()
    {
      piirraPuu (6, width / 2.0, height, 100);
    }
        
  6. Eri kokoisia “shakkilautoja”, joissa on vaaka- ja pystysuunnassa \(1, 2, 4, 8, …\) ruutua - eli ruutujen lukumäärä vaaka- ja pystysuunnassa on kahden potenssi - voidaan piirtää rekursiivisesti.
    Perustapaus.
    Jos piirrettävää aluetta ei tarvitse enää jakaa useampaan ruutuun, piirretään ruutu (tilanteen mukaan keltainen tai musta).
    Rekursio perustapausta kohden.
    Jos piirrettävä alue täytyy vielä jakaa, jaetaan se neljään neliöön. Vasen yläkulma ja oikea alakulma merkitään piirrettäväksi mustalla, toiset kaksi neliötä keltaisella.

    Alla on ohjelman piirtämiä shakkilautoja, kun jakojen määrä on 0, 1, 2 tai 4. Kirjoita ohjelma, jolla voit piirtää vastaavia kuvia.

Ratkaisuja

  1. <<rekursio-pystyviivat-runko>>
        
  2. <<rekursio-satunnaiskavely-runko>>
        
  3. <<rekursio-laskeva-lukujono>>
        
  4. a)
    // palauttaa luvun t_n
    int trib (int n)
    {
      // perustapaukset
      if (n == 0)
        return (0);
    
      if (n == 1 || n == 2)
        return (n - 1);
    
      // rekursioaskel
      return (trib (n - 1) + trib (n - 2) + trib (n - 3));
    }
    
    <<bonacci-setup>>
    
    void draw ()
    {
      text (trib (6), width / 2.0, height / 2.0);
    }
        

    b)

    int[] t; // taulukko arvojen tallentamiseksi
    
    // palauttaa luvun t_n
    int trib (int n)
    {
      // jos arvoa t_n ei ole vielä laskettu...
      if (t [n] < 0)
      {
        // perustapaukset
        if (n == 0)
          t [n] = 0;
        else if (n == 1 || n == 2)
          t [n] =  n - 1;
        else
          t [n] = trib (n - 1) + trib (n - 2) + trib (n - 3); // rekursioaskel
      }
    
      return (t [n]); // palautetaan arvo taulukosta
    }
    
    <<bonacci-setup>>
    
    void draw ()
    {
      final int N = 15; // lasketaan t_0, t_1, ..., t_15
    
      // varataan tila taulukolle ja alustetaan taulukko luvuilla -1
      t = new int [N + 1];
      for (int i = 0; i <= N; i++)
        t [i] = -1;
    
      for (int n = 0; n <= N; n++)
        text (trib (n), (n + 1) * 35, height / 2.0); // 35 pikselin välein
    }
        
  5. <<rekursio-puu>>
        
  6. Kuvia voidaan piirtää sijoittamalla jokin luku vakion JAOT paikalle.
    void piirraShakkilauta (int jakoja,
                            float x,
                            float y,
                            float sivu,
                            boolean musta)
    {
      // perustapaus
      if (jakoja == 0)
      {
        if (musta)
          fill (0);
        else
          fill (17, 100, 100);
        rect (x, y, sivu, sivu);
    
        return;
      }
    
      // rekursioaskel
      float p = sivu / 2; // sivun puolikas
      float xOikea = x + p; // uuden jaon oikeanpuoleisten neliöiden x
      float yAla = y + p; // uuden jaon alaneliöiden y
      int jakojaJaljella = jakoja - 1;
    
      piirraShakkilauta (jakojaJaljella, x, y, p, true); // vasen ylä
      piirraShakkilauta (jakojaJaljella, xOikea, yAla, p, true); // oikea ala
      piirraShakkilauta (jakojaJaljella, x, yAla, p, false); // vasen ala
      piirraShakkilauta (jakojaJaljella, xOikea, y, p, false); // oikea ylä
    }
    
    void setup ()
    {
      size (200, 200);
      colorMode (HSB, 100);
      noStroke ();
      noLoop ();
    }
    
    void draw ()
    {
      piirraShakkilauta (JAOT, 0, 0, width, true);
    }