Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

Ohjelmoinnin jatkokurssi: harjoitukset s2011: 2/6 (7.-11.11.)

(Muutettu viimeksi 7.11.2011, sivu perustettu 26.10.2011.)

Nämä harjoitukset liittyvät lähinnä oppimateriaalin lukuihin 2-6.

Kaikki harjoitustehtävät on syytä tehdä. Jotkin tehtävät on merkitty keltaisella värillä. Ne ovat ehkä hieman muita haastavampia. Ilman niitäkin harjoituksista voi saada maksimipisteet, mutta ne lasketaan silti mukaan harjoituspisteitä määrättäessä – ne voivat siis korvata joitakin haasteettomampia tehtäviä tms. Mutta ennen kaikkea noista keltaisista tehtävistä sitä vasta oppiikin!

Huom:

MyString-luokan käyttöä tuotannossa

Yksi opiskelija valitteli, ettei ensimmäisen viikon hienoa MyStringiä käytetty mihinkään varsinaiseen sovellukseen. Korjataan asia!

Rivieditori.java

Laadi sovellus, joka komentotulkkilogiikalla tarjoaa yksirivisen tekstin editoinnin. Kyseessä on siis alkeellinen editori. Ohjelman keskeisenä tietorakenteena käytetään itse ohjelmoitua MyStringluokan ilmentymää.

Seuraava esimerkki havainnollistaa, mitä ja miten sovellus osaa. (Koneen tekstit ovat kursiivilla.)

Komennot ovat # = lopetus, + = lisää, - = poista
+
Mihin kohtaan lisätään?
3
Suurin sallittu indeksi on 0.
+
Mihin kohtaan lisätään?
0
Mitä lisätään?
Xyz
Rivi on: Xyz
+
Mihin kohtaan lisätään?
4
Suurin sallittu indeksi on 3.
+
Mihin kohtaan lisätään?
3
Mitä lisätään?
kissa
Rivi on: Xyzkissa
+
Mihin kohtaan lisätään?
3
Mitä lisätään?
**
Rivi on: Xyz**kissa
-
Mistä kohdasta poistetaan?
7
Rivi on: Xyz**kisa
Ähäkutti! Mites tästä selviät?
Komennot ovat # = lopetus, + = lisää, - = poista
-
Mistä kohdasta poistetaan?
4
Rivi on: Xyz*kisa
#

Neuvoja:

Paranneltu rivieditori

[Tässä harjoitellaan vasta myöhemmin opittavia asioita joita siis saa oppia jo nytkin jos tahtoo...;-]

Ei ole kovin kivaa, että rivieditori kaatuu muodoltaan virheelliseen numeeriseen syötteeseen. On monia tapoja varustautua myös tuohon virheeseen. Yksi on ns. poikkeuksen ns. sieppaaminen vaikkapa seuraavaan tapaan:

...
int luku;
...
try {
  luku = lukija.nextInt();
} 
catch (NumberFormatException e) {
  ... hoitele virhetilanne ...
}

Ohjelmoi ensin työkalu, joka lukee kokonaislukusyötteen eikä anna periksi ennen kuin saa muodoltaan kelvollisen kokonaisluvun. Parantele sitten tällä uudella välineellä rivieditori entistä ehommaksi.

Kokonaislukujoukon toteutus: IntJoukko.java

Javan APIssa on valmiita välineitä joukon toteutukseksi. Mutta kuten jo viime viikolla nähtiin, tällaisten dynaamisilta vaikuttavien välineiden toteuttaminen itse - omin käsin - on hyvin opettavaista.

Ensimmäisen viikon tehtävissä jo perehdyttiin vaihtelevanmittaisen merkkijonon toteuttamiseen. Puutteena tuon tyypin dynaamisuudessa oli kiinteä yläraja pisimmälle mahdolliselle merkkijonolle. Tämäkin puute korjataan nyt oman kokonaislukujoukon toteutuksessa.

Tälläkin kertaa rakenteen elementit – nyt kokonaisluvut – talletetaan taulukon alkuun ja käytössä olevien taulukon alkioiden lukumäärä muistetaan muuttujan avulla. Mutta nyt menetellään lisäksi siten, että jos uusi alkio ei enää mahtuisi taulukkoon, luodaan uusi isompi taulukko, jonka alkuun ensin kopioidaan vanhan taulukon sisältö ja vasta sitten toteutetaan lisäysoperaatio.

IntJoukko, vaihe 1

Joukolle varatun taulukon oletuskoko löytyy julkisesta luokkavakiosta IntJoukko.KAPASITEETTI. Jos lisäysoperaatio joutuu luomaan suuremman taulukon, uusi taulukko on IntJoukko.OLETUSKASVATUS-vakion verran suurempi kuin vanha taulukko.

Luokan IntJoukko määrittely alkaa seuraavasti:

public class IntJoukko {

  public final static int KAPASITEETTI   = 5,  // aloitustalukon koko
                          OLETUSKASVATUS = 5;  // luotava uusi taulukko on 
                                               // näin paljon isompi kuin vanha

  private int[] taulukko;    // Joukon luvut säilytetään taulukon alkupäässä. 
  private int alkioidenLkm;  // Tyhjässä joukossa alkioiden_määrä on nolla. 

  // ...

Ohjelmoi luokkaan parametriton konstruktori ja aksessorit

Neuvo: Ohjelmoi lisaa-metodi aluksi ilman uuden talukon luontia. Saat mallia MyString-luokan metodien toteutuksesta. Nyt vain on pidettävä huoli siitä, ettei taulukkoon viedä lukua, jos se jo siellä on. Lisää taulukon korvaaminen isommalla vasta kun olet saanut luvun lisäämisen logiikan toimimaan.

Esittele luokan käyttöä seuraavaan tyyliin:

    ...
IntJoukko x = new IntJoukko();

System.out.println("Joukko on: " +x); // {}

x.lisaa(57);    // Huomaa miten luontevaa "lausekelauseen" käyttäminen lauseena on:
x.lisaa(-32);   // Jos ei kiinnosta, oliko lisäys aito vai turha, jätetään vain
x.lisaa(12);    // tuo boolean-palautusarvo huomiotta.

System.out.println("Joukko on: " +x); // {57, -32, 12}

x.lisaa(57); // on jo, ei pitäisi lisätä!
System.out.println("Joukko on: " +x); //  {57, -32, 12}

x.lisaa(60);
x.lisaa(-31);
System.out.println("Joukko on: " +x); // {57, -32, 12, 60, -31}
System.out.println("Joukon koko on " + x.mahtavuus()); // 5

// nyt se kriittiinen kohta; osaako tehdä isomman taulukon:

x.lisaa(16);
System.out.println("Joukko on: " +x); // {57, -32, 12, 60, -31, 16}
System.out.println("Joukon koko on " + x.mahtavuus()); // 6

IntJoukko, vaihe 2

Ensimmäisessä vaiheessa vakiot IntJoukko.KAPASITEETTI ja IntJoukko.OLETUSKASVATUS- määräsivät IntJoukko-olion sisäisen toiminnan aina sellaiseksi, että alussa luodaan viisialkoinen taulukko ja kasvatuskoko on aina viisi. Nämä oletusarvot olivat epärealistisen pienet testaamisen helpottamikseksi.

IntJoukkoa käyttävän sovelluksen laatija voi tietää, että hänen tilanteessaan joukot ovat "yleensä sen ja sen kokoisia". Hän saattaa myös osata arvella jotakin siitä, mitä tapahtuu siinä tilanteessa, kun joukko on tavanomaista isompi: miten paljon "suuret joukot tapaavat kasvaa".

Laajenna IntJoukkoa tällaisen valistuneen ohjelmoijan tarpeisiin lisäämällä luokkaan konstruktorit, joilla luotavan IntJoukko-olion oletusarvoja voidaan muuttaa.

Luokan IntJoukko määrittely alkaa nyt seuraavaan tapaan:

public class IntJoukko {

  public final static int KAPASITEETTI   = 5,  // aloitustalukon koko
                          OLETUSKASVATUS = 5;  // luotava uusi taulukko on 
                                               // näin paljon isompi kuin vanha

  private int kasvatuskoko;     // Uusi taulukko on tämän verran vanhaa suurempi.

  private int[] taulukko;      // Joukon luvut säilytetään taulukon alkupäässä. 
  private int alkioidenLkm;    // Tyhjässä joukossa alkioiden_määrä on nolla. 

  public IntJoukko() {
    // muilta osin tämä konstruktori ohjelmoitiin jo edellisessä kohdassa:
    kasvatuskoko = OLETUSKASVATUS;
  }

  public IntJoukko(int kapasiteetti) {
    // Annetaan oma kapasiteetti.
    // Muista tarkistaa parametrin kelvollisuus!
    // ... 
  }

  public IntJoukko(int kapasiteetti, int kasvatuskoko) {
     // Annetaan oma kapasiteetti ja kasvatuskoko.
     // Muista tarkistaa parametrien kelvollisuus!
     // ...
  }
  // ...

Kokeile ja havainnollista uusien konstruktoreiden toimintaa sopivalla pääohjelmalla.

IntJoukko, vaihe 3

Joukon toteutus on vielä aika köyhä. Täydennetään sitä parilla keskeisellä operaatiolla:

Käyttöesimerkki:

IntJoukko x = new IntJoukko();
x.lisaa(57);
x.lisaa(-32);
x.lisaa(12);
x.lisaa(60);
System.out.println("Joukko on: " +x); // {57, -32, 12, 60}

System.out.println(x.kuuluu(22)); // false
System.out.println(x.kuuluu(12)); // true

if (x.kuuluu(12))
  System.out.println("12 kuuluu joukkoon");  // tämä!
else
  System.out.println("12 ei kuulu joukkoon");

x.poista(12);
System.out.println("Joukko on: " +x); // {57, -32, 60}

if (x.kuuluu(12))
  System.out.println("12 kuuluu joukkoon");
else
  System.out.println("12 ei kuulu joukkoon"); // tämä!

x.poista(48);
  System.out.println("Joukko on: " +x); // {57, -32, 60}

System.out.println(x.poista(48)); // false
System.out.println(x.poista(60)); // true ja joukko on {57, -32}

x.poista(57);  // {-32}
x.poista(-32); // {}
x.poista(23);  // {}

IntJoukko, vaihe 4

Syystä tai toisesta (yksi sellainen nähdään pian) lukujoukosta halutaan saada tuotettua taulukko, joka sisältää kaikki joukon alkiot. Lisää luokkaan seuraava aksessori:

Käyttöesimerkki:

IntJoukko x = new IntJoukko();
x.lisaa(57);
x.lisaa(-32);
x.lisaa(12);
x.lisaa(60);
System.out.println("Joukko on: " +x); // {57, -32, 12, 60}

int[] taulukkona = x.toIntArray();
for (int alkio: taulukkona)
  System.out.print(alkio + ", ");
System.out.println();                // 57, -32, 12, 60,

IntJoukon rakenteen parantelua, "refaktorointia"

Edellä IntJoukko toteutettiin vaiheittain, minkä seurauksena ohjelmaan syntyi paikoin "turhaa" algoritminpätkien kertaamista ja ehkä muutakin rakenteellista epäselvyyttä. Esimerkiksi vaiheessa 1 ohjelmoitu lisaa-metodi joutuu tutkimaan, ettei lukua lisätä, jos se jo kuuluu joukkoon. Tuo sama joukkkoon kuuluminen ohjelmoitiin omaksi metodikseen kuuluu vaiheessa 3.

Muokkaa IntJoukko-luokan toteutusta siten, että lisaa-aksessori käyttää hyväkseen kuuluu-aksessoria sen sijaan, että omin voimin tuon tutkimuksen tekisi. Huomaa siis, että aksessorit voivat vallan mainiosti kutsua toisia aksessoreita! Ellet jo ole huomannut...

Keksitkö muita tapoja virtaviivaistaa luokan toteutusta?

Ohjelmarakenteen muuttamista selkeämmäksi ohjelman toimintaa muuttamatta kutsutaan hienolla sivistyssanalla refaktoroinniksi. Sitä tehdään, jotta ohjelman ylläpitäminen helpottuisi.

IntJoukko, vaihe 5

Muokkaa edellä toteutettua IntJoukko-luokkaa siten, että joukon alkiot pidetään taulukossa aina nousevassa suuruusjärjestyksessä ja hakuoperaatiot toteutetaan binäärihaulla.

Luonnollisestikaan IntJoukko-olioiden toiminta ei saa muuttua tämän tehostamisen seurauksena — lukuunottamatta sitä, että toStringin tuottamassa merkkiesityksessä alkiot ovat järjestyksessä.

Voisit toki ohjelmoida järjestämisalgoritminkin, mutta toteutuksesta tulee tehokkaampi, jos muutat lisäämisalgoritmia siten, että lisättävä luku viedään heti oikeaan kohtaansa taulukossa. Tällöin erillistä järjestämistä ei tarvita. Muista että aksessoreille voi mainiosti ohjelmoida "pikku apulaisia" yksityisinä ilmentymämetodeina.

Joukko-operaatioita kirjastossa

Toteutetaan sitten tuttuja joukko-operaatiota IntJoukko-olioille. Ensin public static -kirjastometodeina luokkaan JoukkoOp

IntJoukko-luokan aksessoreita käyttäen ohjelmonnin ei pitäisi olla kovin työlästä. Erityisesti tarvitset toIntArray()-aksessoria joukon alkioiden läpikäymiseen. Muutkin aksessorit helpottanevat ohjelmointia.

Yhdiste

public static IntJoukko yhdiste(IntJoukko a, IntJoukko b) palauttaa arvonaan joukon, joka sisältää kaikki a:n ja b:n alkiot

Käyttöesimerkki:

IntJoukko a, b, c;
a = new IntJoukko();
b = new IntJoukko();
// ....  a:han ja b:hen viedään kaikenlaisia lukuja
c = JoukkoOp.yhdiste(a, b);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);

Leikkaus

public static IntJoukko leikkaus(IntJoukko a, IntJoukko b) palauttaa arvonaan joukon, joka sisältää täsmälleen kaikki alkiot, jotka kuuluvat sekä joukkoon a että joukkoon b

Käyttöesimerkki:

IntJoukko a, b, c;
a = new IntJoukko();
b = new IntJoukko();
// ....  a:han ja b:hen viedään kaikenlaisia lukuja
c = JoukkoOp.leikkaus(a, b);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);

Erotus

public static IntJoukko erotus(IntJoukko a, IntJoukko b) palauttaa arvonaan joukon, joka sisältää kaikki a:n alkiot, jotka eivät kuulu joukkoon b

Käyttöesimerkki:

IntJoukko a, b, c;
a = new IntJoukko();
b = new IntJoukko();
// ....  a:han ja b:hen viedään kaikenlaisia lukuja
c = JoukkoOp.erotus(a, b);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);

Komentotulkki

Havainnollista näiden kirjastometodien käyttöä esimerkkiohjelmalla, joka perustuu komentotulkin käyttöön.

Käyttöesimerkki: (koneen tekstit ovat kursiivilla)

Tervetuloa joukkolaboratorioon!
Käytössäsi ovat joukot A, B ja C.
Komennot ovat lisää, poista, kuuluu, yhdiste ja leikkaus.
Joukon nimi komentona tarkoittaa pyyntöä tulostaa joukko.

?> lisää
Mihin joukkoon?  A
Mikä luku lisätään? 57

?> A
A = {57}

?> lisää
Mihin joukkoon?  A
Mikä luku lisätään? -32

?> lisää
Mihin joukkoon?  A
Mikä luku lisätään? 12

?> A
A = {57, -32, 12}

?> lisää
Mihin joukkoon?  B
Mikä luku lisätään? 47

?> B
A = {47}

?> yhdiste
1. joukko? A
2. joukko? B
A yhdiste B = {57, -32, 12, 47}

?> kuuluu
Mihin joukkoon?  A
Mikä luku? -32
- 32 kuuluu joukkoon A

?> kuuluu
Mihin joukkoon?  A
Mikä luku? 29
29 ei kuulu joukkoon A

... jne...

Jos haluat lisähaasteita, voit kehitellä yksirivisiä komentoja, jotka eivät erikseen kysele mitään. Ja jos tämäkään ei riitä, voit ottaa mukaan sijoitukset joukkomuuttujille. Ja lopulta tässä päädyttäisiin ohjelmointikieleen, jonka lauseet ja lausekkeet käsittelevät joukkoja...

Joukko-operaatiot aksessoreina

Vaihtoehto edellisen tehtäväryhmän tyylille on täydentää itse luokkaa IntJoukko joukko-operaatioaksessoreilla.

IntJoukko-luokan aksessoreita käyttäen ohjelmonnin ei pitäisi olla kovin työlästä. Erityisesti tarvitset toIntArray()-aksessoria joukon alkioiden läpikäymiseen. Muutkin aksessorit helpottanevat ohjelmointia.

Yhdiste

public IntJoukko yhdiste(IntJoukko b) palauttaa arvonaan joukon, joka sisältää kaikki this-joukon ja joukon b alkiot.

Käyttöesimerkki:

IntJoukko a, b, c;
a = new IntJoukko();
b = new IntJoukko();
// ....  a:han ja b:hen viedään kaikenlaisia lukuja
c = a.yhdiste(b);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);

Leikkaus

public IntJoukko leikkaus(IntJoukko b) palauttaa arvonaan joukon, joka sisältää täsmälleen kaikki alkiot, jotka kuuluvat sekä this-joukkoon että joukkoon b.

Käyttöesimerkki:

IntJoukko a, b, c;
a = new IntJoukko();
b = new IntJoukko();
// ....  a:han ja b:hen viedään kaikenlaisia lukuja
c = a.leikkaus(b);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);

Erotus

public IntJoukko erotus(IntJoukko b) palauttaa arvonaan joukon, joka sisältää kaikki this-joukon alkiot, jotka eivät kuulu joukkoon b

Käyttöesimerkki:

IntJoukko a, b, c;
a = new IntJoukko();
b = new IntJoukko();
// ....  a:han ja b:hen viedään kaikenlaisia lukuja
c = a.erotus(b);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);

Komentotulkki

Muokkaa tehtävän 2.4 komentotulkista sellainen, jossa joukko-operaatiot on toteutettu aksessorein (siis käyttäen juuri tämän 3. tehtäväsarjan tapaa joukko-operaatioiden toteutuksessa). Tämä tehtävä on itse asiassa melko vaivattomasti muokattavissa tehtävän 2.4 ratkaisusta.

Kokonaislukujoukon toteutus kehittynein välinein

Ohjelmoidaan sitten IntJoukko käyttäen Javan valmista kalustoa.

IntJoukko ja ArrayList<Integer>

Toteuta IntJoukko-luokka käyttäen kapseloituna tietorakenteena ArrayList<Integer>-oliota. ArrayListin aksessorit tekevät operaatioiden toteutuksen aika sujuvaksi. Ja nyt ei tarvita kuin parametriton konstruktori, koska ArrayList osaa itse kasvaa ja pienetä tarpeen mukaan.

API on melkein sama kuin aiemmin:

IntJoukko ja HashSet<Integer>

Nyt se sitten alkaa! Nyt on aika ruveta ihan omin päin tutustumaan Javan valmiiseen kalustoon: Perehdy luokkaan HashSet Javan APIn pakkauksessa java.util ja toteuta IntJoukko-luokkasi siten, että se kapseloi piilossa pidetyksi tietorakenteeksi HashSet<Integer>-olion.

Joukon toteutusten vertailua

Tutki kolmen erilaisen IntJoukko-luokan toteutuksen nopeutta ohjaajien laatimalla mittausohjelmalla. Anna kokonaislukujoukon ArrayList-toteutukselle nimi IntJoukkoAL ja HashSet-toteutukselle nimi IntJoukkoHS.