/* * Ohjelmoinnin perusteet, syksy 2008, harjoitus 6 (7.-10.10.) * Esimerkkiratkaisuja * Koostanut Ari Meriläinen */ /* ************************************************************************************ ********************************** Tehtävä 24 ************************************** ************************************************************************************ * * A. Laadi yksityisiä luokkametodeja ("pääohjelman pikku apulaisia"), jotka saavat * parametrina taulukko-olion, jonka tyyppi on int[]. Varaudu myös 0:n alkion * mittaisiin taulukoihin. Metodi... * a. asettaa taulukon jokaisen alkion arvoksi indeksin kerrottuna kahdella; * alkioiden arvoiksi tulee tulee siis 0, 2, 4, 6, ... * b. tulostaa taulukon jokaisen alkion, jonka arvo on pariton * c. etsii järjestämättömän taulukon alkioista suurimman ja palauttaa sen * indeksin arvonaan (suurin ei ole välttämättä yksikäsitteinen, miten * etsittäisiin ensimmäinen suurin, entä viimeinen?) * d. siirtää taulukon viimeisen alkion ensimmäiseksi, ensimmäisen toiseksi, toisen * kolmanneksi, jne.; taulukkoa siis "pyöräytetään ympäri" yhden alkion verran. * * B. Laadi yksityisiä luokkametodeja ("pääohjelman pikku apulaisia"), jotka saavat * parametrina kokonaislukumatriisin, jonka tyyppi on int[][]. Varaudu myös 0:n alkion * kokoisiin taulukoihin. Metodi... * a. asettaa matriisin jokaisen alkion arvoksi indeksien tulon (esimerkiksi alkio, * jonka indeksit ovat [2][4] saa arvokseen 8) * b. etsii matriisin alkoista pienimmän ja palauttaa sen arvonaan; jos matriisi * sisältää nolla alkiota, metodi palauttaa arvon nolla * c. selvittää, löytyykö parametrina annettu luku matriisista; jos löytyy, metodi * palauttaa arvon true, muuten false * */ public class Taulukkometodit { /* *** A) Taulukkoa käsittelevät metodit *** */ /* * a. Metodi asettaa taulukon jokaisen alkion arvoksi indeksin kerrottuna * kahdella; alkioiden arvoiksi tulee tulee siis 0, 2, 4, 6, ... */ public static void asetaIndeksiKertaa2(int[] taulu) { for (int i=0; i < taulu.length; i++) { taulu[i] = i*2; } } /* * b. Metodi tulostaa taulukon jokaisen alkion, jonka arvo on pariton. */ public static void tulostaParittomat(int[] taulu) { // Ei tarvita tietoa alkion indeksistä, joten voidaan käyttää for-each -silmukkaa for(int alkio : taulu) { if (alkio%2 != 0) { System.out.println(alkio); } } } /* * c. Metodi etsii järjestämättömän taulukon alkioista suurimman ja palauttaa sen * indeksin arvonaan (suurin ei ole välttämättä yksikäsitteinen, miten * etsittäisiin ensimmäinen suurin, entä viimeinen?) */ public static int etsiSuurin(int[] taulu) { // Jos taulukossa ei ole arvoja, palautetaan -1 if (taulu.length < 1) { return -1; } int suurimmanIndeksi = 0; // Suurimman toistaiseksi löytyneen alkion indeksi. // Alkuarvo (0) palautetaan, jos taulukon koko on 1. // Silmukkaan mennään vain, jos taulukon koko on suurempi kuin 1. for (int i=1; i=) löytäisi viimeisen. if (taulu[i] > taulu[suurimmanIndeksi]) { suurimmanIndeksi = i; } } return suurimmanIndeksi; } /* * d. Metodi siirtää taulukon viimeisen alkion ensimmäiseksi, ensimmäisen toiseksi, * toisen kolmanneksi, jne.; taulukkoa siis "pyöräytetään ympäri" yhden alkion verran. */ public static void siirräOikealle(int[] taulu) { if (taulu.length > 0) { int temp = taulu[taulu.length-1]; // Otetaan viimeinen alkio talteen // Käydään taulu lopusta alkuun (Mikä ongelma syntyisi, jos käytäisiin alusta loppuun?) for (int i=taulu.length-1; i > 0; i--) { taulu[i] = taulu[i-1]; } taulu[0] = temp; // Sijoitetaan vanha viimeinen alkio ensimmäiseksi } } /* *** B) Matriisia käsittelevät metodit *** */ /* * a. Metodi asettaa matriisin jokaisen alkion arvoksi indeksien tulon (esimerkiksi * alkio, jonka indeksit ovat [2][4] saa arvokseen 8) */ public static void asetaIndeksienTulo(int[][] matriisi) { for (int i=0; i "); int lukuja = reader.nextInt(); while (lukuja < 1) { System.out.print("Loisin mieluummin taulukon, jossa on lukuja. Eli kuinka monta? > "); lukuja = reader.nextInt(); } int[] taulukko = new int[lukuja]; // Luodaan taulukko lukuja varten // Kysytään käyttäjältä luvut taulukkoon kysyLuvut(taulukko); // Tulostetaan luvut lähinnä testauksen vuoksi tulosta(taulukko); // Järjestetään luvut nousevaan järjestykseen järjestä(taulukko); // Tulostetaan taulukko, jotta uskotaan järjestämisalgoritmin toimineen tulosta(taulukko); /* Binäärihaku siis vaatii järjestetyn aineiston, jotta algoritmi toimisi. * Tässä ratkaisussa luvut kysyttiin ensin ja järjestettiin sitten, mutta * tietysti taulukon järjestyksestä voisi pitää huolta jo lukuja kysyttäessä. */ // Kysellään lukuja ja etsitään niitä annetusta lukujoukosta: boolean jatketaan = true; // silmukan lopetusehtomuuttuja do { System.out.print("Anna syötteistä etsittävä luku (syötä ei-kokonaisluku lopettaaksesi) > "); if (reader.hasNextInt()) { int luku = reader.nextInt(); if (binääriHae(luku, taulukko)) { System.out.println("Luku löytyi taulukosta!"); } else { System.out.println("Lukua ei löydy taulukosta"); } } else { // Ei-kokonaisluku lopettaa reader.next(); jatketaan = false; } } while (jatketaan); System.out.println("Nähdään!"); } /* * Kysyy käyttäjältä kokonaislukuja, kunnes annettu taulukko on täynnä. */ private static void kysyLuvut(int[] taulu) { for (int i=0; i "); taulu[i] = reader.nextInt(); } } /* * Järjestää kokonaislukutaulukon alkiot nousevaan järjestykseen. * Järjestämiseen käytetään yksinkertaista vaihtojärjestämistä. */ private static void järjestä(int[] taulu) { for (int i=0; i taulu[j]) { int apu = taulu[i]; taulu[i] = taulu[j]; taulu[j] = apu; } } } } /* * Etsii annettua lukua annetusta taulukosta. Palauttaa true, jos luku löytyy, * tai muutoin false. Etsintä tehdään binäärihaulla, joten taulukon täytyy olla * järjestetty nousevaan järjestykseen. */ private static boolean binääriHae(int haettava, int[] taulu) { int vasen = 0; // Vasen sormi taulukon alkuun int oikea = taulu.length-1; // Oikea sormi taulukon loppuun while (vasen <= oikea) { int keski = (oikea+vasen)/2; // Sormien keskikohta if (taulu[keski] == haettava) { return true; // Luku löytyi! } // Jos etsittävä luku keskikohdan oikealla puolella, // vasen sormi siirretään pykälää keskikohdasta oikealle if (haettava > taulu[keski]) { vasen = keski+1; } // Jos etsittävä luku vasemmalla puolella, oikea sormi // siirretään pykälää keskikohdasta vasemmalle else { oikea = keski-1; } } // Vasen ja oikea sormi menivät ristiin, joten lukua ei löydy: return false; } /* * Tulostaa taulukon alkiot. */ private static void tulosta(int[] taulu) { for(int alkio : taulu) { System.out.print(alkio + " "); } System.out.println(); } } ---------------------------------------------- /* ************************************************************************************ ********************************** Tehtävä 26 ************************************** ************************************************************************************ * * Lotto on numeroveikkaus, jossa arvotaan 7 numeroa ja 3 lisänumeroa 39 numerosta. * Loton voittoluokat ovat 7 oikein, 6 ja lisänumero oikein, 6 oikein, 5 oikein ja * 4 oikein. Tee ohjelma joka ensin arpoo oikean lottorivin. Sitten ohjelmalta voi * kysellä, onko jokin lottorivi oikein. Kyselyitä voi olla useampia. Suunnittele * itse, miten ohjelman toiminta päättyy. Satunnaisluvun väliltä 1-39 saat * arvottua vaikkapa seuraavasti: * * int arvottu = (int)(39*Math.random()) + 1; * */ import java.util.Scanner; public class Lotto { // Vakiot määrittelevät, kuinka monta numeroa arvotaan private static final int NUMEROITA = 7; private static final int LISÄNUMEROITA = 3; private static Scanner reader = new Scanner(System.in); public static void main(String[] args) { System.out.println("Lottosovellus!"); System.out.println("Arvotaan "+NUMEROITA+" numeroa ja "+LISÄNUMEROITA+" lisänumeroa"); int[] numerot = new int[NUMEROITA]; int[] lisänumerot = new int[LISÄNUMEROITA]; arvoLuvut(numerot, lisänumerot); // Arpoo numerot // Tulostetaan numerot testauksen vuoksi: System.out.print("Numerot: "); tulosta(numerot); System.out.print("Lisänumerot: "); tulosta(lisänumerot); System.out.println("Lottorivi arvottu. Nyt voit tarkistaa omia rivejäsi."); boolean kysyUudestaan = true; do { int[] omaRivi = new int[NUMEROITA]; System.out.println("Syötä " + NUMEROITA + " kokonaislukua (väliltä 1-39)."); for (int i=0; i < omaRivi.length; i++) { int numero; boolean lukuOk = false; do { // Kysytään, kunnes saadaan kelvollinen luku System.out.print((i+1) + ". numero > "); numero = reader.nextInt(); if (numero < 1 || numero > 39) { System.out.println("Lottonumero voi olla vain väliltä 1-39, syötä uusi."); } else if (etsiNumero(numero, omaRivi, i)) { System.out.println("Rivilläsi oli jo luku " + numero +", syötä uusi."); } else { lukuOk = true; } } while(!lukuOk); // Sijoitetaan luku riville omaRivi[i] = numero; } System.out.print("Lottorivisi on: "); tulosta(omaRivi); int numeroitaOikein = laskeSamat(omaRivi, numerot); int lisäNumeroitaOikein = laskeSamat(omaRivi, lisänumerot); if (numeroitaOikein == 7) { System.out.println("Käsittämätöntä! Sait 7 oikein!"); } else if (numeroitaOikein == 6 && lisäNumeroitaOikein >= 1) { System.out.println("6 ja lisänumero oikein! Lähes yhtä käsittämätöntä!"); } else if (numeroitaOikein >= 4) { System.out.println("Sait "+numeroitaOikein+" oikein. Rahan tuloa ei voi estää!"); } else { System.out.println("Sait "+numeroitaOikein+" numeroa ja "+lisäNumeroitaOikein+ " lisänumeroa oikein."); System.out.println("Harmillisesti et tällä kertaa voittanut mitään."); } System.out.println("Haluatko syöttää uuden rivin? (k/e)"); String vastaus = reader.next(); if (vastaus.charAt(0) == 'e') { // Lopetetaan, jos vastaus alkaa e:llä, muuten jatketaan kysyUudestaan = false; } } while(kysyUudestaan); System.out.println("Nähdään taas!"); } /* * Laskee kuinka monta yhteistä numeroa annetuissa taulukoissa on. * (Huom! Ei toimisi oikein, jos tauluissa voisi esiintyä sama luku useaan kertaan, * jonka tämä sovellus muualla estää.) */ private static int laskeSamat(int[] taulu, int[] taulu2) { int samoja = 0; for (int i=0; i < taulu.length; i++) { for (int j=0; j < taulu2.length; j++) { if (taulu[i] == taulu2[j]) { samoja++; } } } return samoja; } /* * Arpoo kaksi taulukko täyteen lukuja väliltä 1-39 siten, että kukin luku voi * esiintyä korkeintaan kerran jommassa kummassa taulukossa. */ private static void arvoLuvut(int[] taulu, int[] toinenTaulu) { // Täytä ensimmäinen taulukko for (int i=0; i= 1) { // Järjestä komentoriviparametrit.. lisäysJärjestä(args); // ..ja tulosta ne for (String s : args) { System.out.println(s); } } else { System.out.println("Ole hyvä ja anna joitain komentoriviparametreja."); } } /* * Järjestää String-taulukon nousevaan järjestykseen lisäysjärjestämisalgoritmia * käyttäen. */ public static void lisäysJärjestä(String[] taulu) { for (int i=1; i 0 && alkio.compareTo(taulu[j-1]) < 0) { taulu[j] = taulu[j-1]; j--; } taulu[j] = alkio; } } } /* ************************************************************************************ ********************************** Tehtävä 28 ************************************** ************************************************************************************ "Vertaile peräkkäishakua ja binäärihakua järjestetystä taulukosta. Kuinka monta alkiota joudutaan tutkimaan helpoimmassa ja vaikeimmassa tapauksessa kun taulukon koko on 10, 100, 1000, 10000, 100000 alkiota? Löydätkö yleistä kaavaa vaikeimmalle tapaukselle? Osaatko arvioida, montako alkiota keskimäärin joudutaan tutkimaan?" Peräkkäishaussa helpoin tapaus on tietysti aina se, jossa etsittävä arvo on taulukossa ensimmäisenä. Vaikeimmassa tapauksessa taas kaikki taulukon alkiot on käytävä läpi, ennen kuin tulos selviää. Tämä voi tarkoittaa joko sitä, että haettu arvo on taulukon viimeinen alkio, tai että se on arvo, jota taulukosta ei löydy, ja myös suurempi kuin taulukon suurin arvo. Jos etsittäisiin arvoa, jota taulukossa ei ole, mutta se olisi pienempi kuin taulukon suurin arvo, peräkkäishaun voisi lopettaa heti, kun ohitetaan se paikka, johon etsittävä arvo taulukossa kuuluisi. Yleinen kaava peräkkäishaun vaikeimmalle tapaukselle on siis yksinkertaisesti "n, missä n=taulukon koko". Ja koska helpoin tapaus on aina 1 ja vaikein aina n, lienee keskimääräinen tapaus n/2. Binäärihaussakin etsittävä löytyy helpoimmassa tapauksessa ensimmäisestä tutkitusta paikasta, eli taulukon puolivälistä. Binäärihaku ei kuitenkaan pahimmassa tapauksessa joudu käymään läpi lähellekään samanlaista määrää alkioita kuin peräkkäishaku. Jokaisella ohi menneellä yrityksellä binäärihaku voi eliminoida puolet jäljellä olevista alkioista. Esimerkiksi 1000 alkion taulusta binäärihaku etenisi pahimmassa tapauksessa tähän tapaan: 1. yritys : 500 mahdollista jäljellä 2. yritys : 250 mahdollista jäljellä 3. yritys : 125 mahdollista jäljellä 4. yritys : 62 mahdollista jäljellä 5. yritys : 31 mahdollista jäljellä 6. yritys : 15 mahdollista jäljellä 7. yritys : 7 mahdollista jäljellä 8. yritys : 3 mahdollista jäljellä 9. yritys : 1 mahdollinen jäljellä 10. yritys : haettava löytyi TAI tiedetään, ettei haettavaa ole taulukossa Koska hakualue puoliintuu joka kierroksella, voidaan ajatella jokaisen yrityksen osumatodennäköisyyden siis olevan kaksinkertainen. Näin voimme junailla etsinnälle kaavan 2^t=n, missä t on tarvittavien yritysten määrä ja n taulukon koko. Tästä 't' selviää pyöräyttämällä kaava muotoon t=lb(n) (lb tarkoittakoon kaksikantaista logaritmia). Tulokseksi ei tietenkään aina tule kokonaislukua, ja intuitiivisesti lienee ymmärrettävää, että tulos (eli yritysten määrä) täytyykin pyöristää ylöspäin lähimpään kokonaislukuun. Täten pahimman tapauksen kaava on siis ceil(lb(n)). Keskimääräinen tapaus on binäärihaussa hyvin lähellä pahinta tapausta. Jos palataan tuohon kaavaan 2^t=n, niin huomataan, että 2^(t-1) = (n/2). Eli jos 't' yrityksellä saadaan tutkittua joka ainoa taulukon alkio, niin t-1 yrityksellä saadaan tutkittua vain puolet. Todennäköisyyslaskenta sanonee, että on noin 50% mahdollisuus, että mielivaltainen kaivattu alkio on juuri siinä puolikkaassa, jota ei vielä ole tutkittu. Siispä binäärihaun keskimääräisen tapauksen kaava on lb(n)-1 (tässä ei tarvita pyöristystä ylöspäin). Teorian mukaan binäärihaku vaatisi taulukoista seuraavanlaisia hakumääriä: Taulukon koko 10 100 1000 10000 100000 Pahin tapaus 4 7 10 14 17 Keskimääräinen 2,32 5,64 8,97 12,29 15,61 Mutta onneksi näissä asioissa ei tarvitse turvautua vain matematiikkaan. On tunnetusti aina hauskaa ja jännittävää tehdä hieman empiiristä tutkimusta ohjelmoiden! Vaikkapa seuraavanlaisen koodinpätkän avulla: (Huomautettakoon, että ratkaisussa tulostukseen käytettyä System.out.format()-metodia ei kuulukaan vielä osata!) */ public class BinäärihakuTutkimus { public static void main(String[] args) { final int TOISTOJA = 1000; // Montako kertaa haku toistetaan // Käydään läpi taulukon koot 10, 100, 1000, 10000 ja 100000 for (int i=1; i<=5; i++) { int koko = (int)Math.pow(10, i); // 10 potenssiin i System.out.println("Taulukon koko "+koko); int[] taulu = new int[koko]; luoTaulu(taulu); // Alustetaan taulu alkioilla 1-koko int haettava = koko+1; // Luku, jota taulusta ei löydy etsi(haettava, taulu); // Toistetaan hakuja satunnaisilla luvuilla: toistaHaku(taulu, TOISTOJA); // Ja vielä verrokkina teorian antamat tulokset: System.out.format("Kaksikantainen logaritmi %d = %.2f (pahin tapaus)\n", koko, Math.log10(koko)/Math.log10(2)); System.out.format("Kaksikantainen logaritmi %.1f = %.2f (keskimääräinen tapaus)\n", koko/2.0, Math.log10(koko/2.0)/Math.log10(2)); System.out.println(); } } /* * Suorittaa annetun määrän binäärihakuja annetusta taulusta arvotuilla luvuilla. */ private static void toistaHaku(int[] taulu, int kertoja) { int summa = 0; for (int i=0; i 0) { System.out.format("%d alkion taulusta luku %d löytyi %d haun jälkeen\n", taulu.length, haettava, tulos); } else { System.out.format("%d alkion taulusta tarvittiin %d hakua, jotta havaittiin, ettei lukua %d löydy\n", taulu.length, (-1*tulos), haettava); } } /* * Etsii haluttua kokonaislukua annetusta taulukosta käyttäen binäärihakua, ja palauttaa * tiedon tarvituista hakuyrityksistä. Jos alkio löytyi, metodi palauttaa yritysten määrän. * Jos alkiota ei löytynyt, metodi palauttaa yritysten määrän käänteisluvun. */ private static int binääriHae(int haettava, int taulu[]) { int vasen = 0; int oikea = taulu.length-1; int keski = (vasen+oikea)/2; int hakuja = 0; while (vasen <= oikea) { hakuja++; if (taulu[keski] == haettava) { return hakuja; // Löytyi! } if (haettava < taulu[keski]) { oikea = keski-1; } else if (haettava > taulu[keski]) { vasen = keski+1; } keski = (vasen+oikea)/2; } // Ei löytynyt return -1*hakuja; } /* * Alustaa annetun taulukon arvoilla siten, että kunkin alkion arvoksi tulee indeksi+1. */ private static void luoTaulu(int[] taulu) { for (int i=0; i