Procedurální programování, A0B36PRI - PROGRAMOVÁNÍ Procedurální programování, rekurze Jazyk JAVA České vysoké učení technické Fakulta elektrotechnická V 2.02
Procedurální programování - zásady Postupný návrh programu rozkladem řešeního problému na podproblémy zadaný problém rozložíme na podproblémy pro řešení podproblémů zavedeme abstraktní příkazy pomocí abstraktních příkazů sestavíme hrubé řešení abstraktní příkazy realizujeme pomocí metod (funkcí, procedur) Navržené metody propojíme pomocí předávání parametrů Rozklad problému na podproblémy ukážeme na jednoduchých příkladech A0B36PRI „PROGRAMOVÁNÍ“ 06
Rozklad problému na podproblémy Vstup dat od uživatele metodaA( ) Výstupní Parametr(y) Dílčí výpočet 1 metodaB( ) Vstupní parametry Řešený problém main() Začátek programu Konec programu metodaA ( ) metodaB ( ) metodaC ( ) metodaD ( ) Dílčí výpočet 2 metodaC( ) Výstup výsledku pro uživatele metodaD( ) Vstupní parametry A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Výpočet s volbou metody Navrhněte řešení programu, který vypočítá buď obsah obdélníku nebo kruhu. Druh výpočtu lze volit z klávesnice z nabídkového menu (volba čisly) Výpočet lze spouštět z klávesnice opakovaně pro různé parametry Výsledky výpočtu se zobrazují na obrazovce Dílčí podproblémy (pro identifikované části zvolíme název budouci metody): Nabídkové menu s volbou obsluhy (1) ctiPrikaz Vykonání příkazu obsluhy (2) cmdObdelnik (3) cmdKruh Vlastní matematický výpočet (4) obsahObdelniku (5) obsahKruhu Pomocné metody Zjistění parametrů pro výpočet (6) prectiKladnyDouble Tisk nápovědy, pokynů a výsledků (7) tiskNaObrazovku Řídicí program celého řešení,zapíšeme v: (8) main Identifikovali jsem 8 dílčích bloků, ty zapíšeme pomocí metod A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Výpočet s volbou metody Hrubé řešení (zapsané pseudojazykem – ten si volíme) Opakuj { switch (ctiPrikaz) pocitat-obdelnik: cmdObdelnik tiskNaObrazovku // napoveda prectiKladnyDouble // cti parametry vypoctu obsahObdelniku // vlastni vypocet tiskNaObrazovku // zobraz vysledek break pocitat-kruh: cmdKruh obsahKruhu // vlastni vypocet tiskNaObrazovku // zobraz vysledek konec-programu: return } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Vstup a vyhodnocení příkazu static int ctiPrikaz ( ) { Scanner sc = new Scanner(System.in); final int CMD_KRUH=1,CMD_OBDELNIK=2,CMD_KONEC=3;int cmd; while (true) { tiskNaObrazovku ("\nObsah-obdelniku=1,Obsah-kruhu=2,Konec=3","\n"); tiskNaObrazovku("Prikaz = ",""); switch(cmd = sc.nextInt()) { case CMD_KRUH:…; case CMD_OBDELNIK:..; case CMD_KONEC: return (cmd); default: tiskNaObrazovku("==Neznamy prikaz==","\n");break; } } } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Příkaz pro výpočet obsahu obdélníku static void cmdObdelnik ( ) { // Akce "Obdelnik" tiskNaObrazovku("Vypocet obsahu OBDELNIKU","\n"); // Vstup dat double a = prectiKladnyDouble("a ="); double b = prectiKladnyDouble("b ="); // Vypocet double p = obsahObdelniku(a, b); // Vystup dat tiskNaObrazovku("Obsah Obdelniku = ","",p); } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Příkaz pro výpočet obsahu kruhu static void cmdKruh ( ) { // Akce "Kruh" tiskNaObrazovku("Vypocet obsahu KRUHU","\n"); // Vstup dat double r = prectiKladnyDouble("r ="); // Vypocet double p = obsahKruhu(r); // Vystup dat tiskNaObrazovku("Obsah KRUHU = ","",p); } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Vlastní výpočet obsahů obrazců static double obsahObdelniku (double a, double b) { // Vypocet double obsahObdelniku = a * b; return (obsahObdelniku); } static double obsahKruhu (double r) { double obsahKruhu = 2 * Math.PI * r; return (obsahKruhu); A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Zjištění parametrů výpočtu static double prectiKladnyDouble (String napis) { // Vstup dat (kladne racionalni cislo) Scanner sc = new Scanner(System.in); while (true) { tiskNaObrazovku("Zadej kladne cislo","\n"); tiskNaObrazovku(napis,""); double d = sc.nextDouble(); if (d >= 0) return(d); tiskNaObrazovku("Chyba","\n"); } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Tisk nápovědy a výsledků static void tiskNaObrazovku (String s,String lf,double d){ System.out.printf("%s%3.2f %s", s, d, lf); } static void tiskNaObrazovku (String s,String lf){ System.out.printf("%s %s", s, lf); Přetížené metody A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Řídicí program celého řešení public class Main { static {Locale.setDefault(Locale.US);} public static void main(String[] args) { final int CMD_KRUH=1,CMD_OBDELNIK=2,CMD_KONEC=3; while (true) { switch ( ctiPrikaz() ) { case CMD_KRUH: cmdKruh();break; case CMD_OBDELNIK: cmdObdelnik();break; case CMD_KONEC: tiskNaObrazovku("KONEC","\n");return; } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 1 – Připomenutí „Scanner“,“printf“ Před použitím metod „Scanneru“ (čtení klávesnice) je nutné: importovat knihovní soubor java.util.* pokud chceme zadávat z klávesnice desetinou tečku (ne čárku) zapíšeme deklaraci: static {Locale.setDefault(Locale.US);}. Založit proměnnou sc (viz níže), proč bude vysvětleno v lekci o objektech. Před použitím „printf“ není nutné importovat žádnou knihovnu. import java.util.*; public class Main { static {Locale.setDefault(Locale.US);} metody ( ) { Scanner sc = new Scanner(System.in); int x = sc.nextIn t(); System.out.printf("%s%3.2f %s", s, d, lf); } } // class END A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM Navrhněte řešení programu pro hru NIM „počítač-člověk“ Pravidla hry NIM: hráč zadá počet zápalek (např. od 15 do 35) pak se střídá se strojem v odebírání; odebrat lze 1, 2 nebo 3 zápalky, prohraje ten, kdo odebere poslední zápalku. Dílčí podproblémy: zadání počtu zápalek odebrání zápalek hráčem odebrání zápalek strojem A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM Pravidla pro odebírání zápalek strojem, která vedou k vítězství (je-li to možné): počet zápalek nevýhodných pro protihráče je 1, 5, 9, atd., obecně 4n+1, kde n >0, stroj musí z počtu p zápalek odebrat x zápalek tak, aby platilo p – x = 4n + 1 z tohoto vztahu po úpravě a s ohledem na omezení pro x dostaneme x = (p – 1) mod 4 vyjde-li x=0, znamená to, že okamžitý počet zápalek je pro stroj nevýhodný a bude-li protihráč postupovat správně, stroj prohraje. A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM Hrubé řešení: int pocet; boolean stroj = false; // zadání počtu zápalek do{ if (stroj) “odebrání zápalek strojem“ else “odebrání zápalek hráčem“ stroj = !stroj; }while (pocet > 0); if (stroj) “vyhrál stroj“ else “vyhrál hráč“ Podproblémy „zadání počtu zápalek“, „odebrání zápalek strojem“ a „odebrání zápalek hráčem“ budeme realizovat metodami, proměnné pocet a stroj pro ně budou statickými (čili nelokálními) proměnnými A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM public class Nim { static int pocet; // aktuální počet zápalek static boolean stroj; // =true znamená, že bere počítač public static void main(String[] args) { zadaniPoctu(); stroj = false; // zacina hrac do { if (stroj) bereStroj(); else bereHrac(); stroj = !stroj; } while (pocet>0); if (stroj) System.out.println("vyhrál jsem"); else System.out.println ("vyhrál jste, gratuluji"); } static void zadaniPoctu() { … } static void bereHrac() { … } static void bereStroj() { … } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM static void zadaniPoctu() { Scanner sc = new Scanner(System.in); do{ System.out.println("zadejte počet zápalek od15do 35)"); pocet = sc.nextInt(); }while (pocet<15 || pocet>30); } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM static void bereHrac() { Scanner sc = new Scanner(System.in); int x; boolean chyba; do{ chyba = false; System.out.println( "počet zápalek " + pocet ); System.out.println( "kolik odeberete" ); x = sc.nextInt(); if(x < 1) { System.out.println( "prilis malo" ); chyba = true;} else if (x>3 || x>pocet) { System.out.println( "prilis mnoho");chyba = true;} }while(chyba); pocet -= x; } A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 2 – Hra NIM static void bereStroj() { System.out.println( "počet zápalek " +pocet); int x = (pocet-1) % 4; if (x==0) x = 1; System.out.println( "odebírám " +x); pocet -= x; } A0B36PRI „PROGRAMOVÁNÍ“ 06
Rekurze Definice ze slovníku (pozor vtip) Rekurze viz „Rekurze“ ….nekonečná rekurze, lépe: pokud neznáte význam tohoto pojmu, pokračujte pojmem „Rekurze“ Rekurze - algoritmus, který volá v průběhu svého běhu sama sebe Příklad: výpočet faktoriálu: n! 0! = 1, 1! = 1, pro záporné číslo x budiž x! = 1 pro n>1, n! = n(n-1)! A0B36PR1 - 07
Rekurze Rekurzivní funkce (procedury) jsou přímou realizací rekurzivních algoritmů Rekurzivní algoritmus předepisuje výpočet „shora dolů“ v závislosti na velikosti (složitosti) vstupních dat: pro nejmenší (nejjednodušší) data je výpočet předepsán přímo pro obecná data je výpočet předepsán s využitím téhož algoritmu pro menší (jednodušší) data Výhodou rekurzivních funkcí (procedur) je jednoduchost a přehlednost Nevýhodou může být časová náročnost způsobená např. zbytečným opakováním výpočtu Řadu rekurzívních algoritmů lze nahradit iteračními, které počítají výsledek „zdola nahoru“, tj, od menších (jednodušších) dat k větším (složitějším) Pokud algoritmus výpočtu „zdola nahoru“ nenajdeme (např. při řešení problému Hanojských věží), lze rekurzivitu odstranit pomocí tzv. zásobníku A0B36PRI „PROGRAMOVÁNÍ“ 06
Rekurzivní algoritmus v některém kroku volá sám sebe Rekurzivní algorimus Rekurzivní algoritmus v některém kroku volá sám sebe Rekurzivní metoda v některém příkazu volá sama sebe ( i nepřímo ) Příklad – faktoriál : Rekurse n! = 1 pro n1 n! = n*(n-1)! pro n>1 Iterace n! = n*(n-1)*(n-2)*…*2*1 A0B36PRI „PROGRAMOVÁNÍ“ 06
Faktoriál pomocí rekurze a iterace n! = 1 pro n1 n! = n*(n-1)! pro n>1 Iterace n! = n*(n-1)*(n-2)*…*2*1 static int fakt(int n) { if (n<=1) return 1; return n*fakt(n-1); } static int fakt(int n) { if (n<=1) return 1; else return n*fakt(n-1); } static int fakt(int n) { int f = 1; while (n>1){ f *= n; n--; } return f; } static int fakt(int n) { return n<=1?1:n*fakt(n-1); // ternární operátor } A0B36PR1 - 07
Rekurze a rozklad problému na podproblémy Program, který přečte posloupnost čísel zakončenou nulou a vypíše ji obráceně Rozklad problému: zavedeme abstraktní příkaz přečti,vypiš a obrať posloupnost příkaz rozložíme do tří kroků: „obrať posloupnost“: přečti číslo (a ulož ho) if (přečtené číslo není nula) „obrať posloupnost“ (zbytek) vypiš číslo (uložené) A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 3 - Rekurze „Obrať posloupnost“ přečti číslo if (přečtené číslo není nula) „obrať posloupnost, tj. zbytek“ vypiš číslo 2 cti x 4 if()… cti x 6 if()… cti x 8 if()… 2 4 6 8 0 0 8 6 4 2 cti x if()… cti x if()… 8 6 piš x 4 piš x piš x 2 piš x piš x A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 3 - Rekurze – obrat() posloupnost Řešení: public class Obrat { static Scanner sc = new Scanner(System.in); public static void main(String[] args) { System.out.println("zadejte … zakončenou nulou"); obrat(); } static void obrat() { int x = sc.nextInt(); // načtení if(x!=0) obrat(); // otočení zbytku System.out.print(x + " "); // výpis uloženého A0B36PRI „PROGRAMOVÁNÍ“ 06
Příklad 4 - Rekurze - Hanojské věže Zavedeme abstraktní příkaz přenes_věž(n,1,2,3) který interpretujeme jako "přenes n disků z jehly 1 na jehlu 2 s použitím jehly 3". Pro n>0 lze příkaz rozložit na tři jednodušší příkazy přenes_věž(n-1,1,3,2) "přenes disk z jehly 1 na jehlu 2", přenes_věž(n-1,3,2,1) 1 2 3 A0B36PRI „PROGRAMOVÁNÍ“ 06 složitější
Příklad 4 - Rekurze - Hanojské věže public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("zadejte výšku věže"); int pocetDisku = sc.nextInt(); prenesVez(pocetDisku, 1, 2, 3); } static void prenesVez (int vyska,int odkud,int kam,int pomoci){ if(vyska>0) { prenesVez(vyska-1, odkud, pomoci, kam); System.out.println("přenes disk z "+odkud+" na "+kam); prenesVez(vyska-1, pomoci, kam, odkud); 3 přenes disk z 1 na 2 přenes disk z 1 na 3 přenes disk z 2 na 3 přenes disk z 3 na 1 přenes disk z 3 na 2 A0B36PRI „PROGRAMOVÁNÍ“ 06 složitější
Příklad rekurze - Hanojské věže prenesVez(3, 1, 2, 3); prenesVez(2, 1, 3, 2); (1, 2); prenesVez(2, 3, 2, 1); prenesVez(1, 1, 2, 3); (1, 3); prenesVez(1, 2, 3, 1); | prenesVez(1, 3, 1, 2); (3, 2); prenesVez(1, 1, 2, 3); (1, 2); (2, 3); (3, 1); (1, 2); prenesVez(int vyska, int odkud, int kam, int pomoci) { if (vyska>0) { prenesVez(vyska-1, odkud, pomoci, kam); System.out.println("přenes disk z "+odkud+" na "+kam); prenesVez(vyska-1, pomoci, kam, odkud); } 3 přenes disk z 1 na 2 přenes disk z 1 na 3 přenes disk z 2 na 3 přenes disk z 3 na 1 přenes disk z 3 na 2 A0B36PR1 - 07 složitější
Obecně k rekurzivitě Rekurzivní metody jsou přímou realizací rekurzivních algoritmů Rekurzivní algoritmus předepisuje výpočet „shora dolů“ v závislosti na velikosti (složitosti) vstupních dat: pro nejmenší ( nejjednodušší ) data je výpočet předepsán přímo pro obecná data je výpočet předepsán s využitím téhož algoritmu pro menší ( jednodušší ) data Výhodou rekurzivních metod je jednoduchost a přehlednost Nevýhodou může být časová náročnost způsobená např. zbytečným opakováním výpočtu A0B36PRI „PROGRAMOVÁNÍ“ 06
Fibonacciho posloupnost - historie Maatraameru (Chhandah-shāstra, the Art of Prosody, 450 or 200 BC) Leonardo Pisano (Leonardo z Pisy), známý také jako Fibonacci (cca 1175–1250) - králíci Henry E. Dudeney (1857 - 1930) - krávy „Jestliže každá kráva vyprodukuje své první tele (jalovici) ve věku dvou let a poté každý rok jednu další jalovici, kolik budete mít krav za 12 let, jestliže Vám žádná nezemře?“ Rok - počet krav (jalovic) 1 1 2 1 3 2 4 3 5 5 6 8 ….. 12 144 50 20 365 011 074 (20 miliard) Počet krav = počet krav vloni + počet narozených (odpovídá počtu krav předloni) fn = fn-1 + fn-2 A0B36PR1 - 07
Fibonacciho posloupnost - rekurzivně Platí: f0 = 1 f1 = 1 fn = fn-1 + fn-2 pro n > 1 Rekurzivní funkce: static int fib(int i) { if (i<2) return 1; return fib(i-1)+fib(i-2); } Rekurze je hezká - zápis „odpovídá“ rekurentní definici. Je ale i efektivní? A0B36PR1 - 07
Složitost výpočtu Fibonacciho čísla - rekurzivně Příklad pro fib(10): fib(10) fib(9) + fib(8) + fib(8) fib(7) fib(7) + fib(6) fib(7) + fib(6) fib(6) + fib(5) fib(6) fib(5) fib(5) + fib(4) + f50 20 365 011 074 (20 miliard) Složitost je exponenciální!!!! static int fib(int i) { if (i<2) return 1; return fib(i-1)+fib(i-2); } A0B36PR1 - 07
Fibonacciho posloupnost - iteračně Platí: f0 = 1 f1 = 1 fn = fn-1 + fn-2 ,pro n > 1 Iteračně: static int fib(int n) { int i, fibNMinus2=1; int fibNMinus1=1, fibN=1; for (i=2; i<=n; i++) { fibNMinus2 = fibNMinus1; fibNMinus1 = fibN; fibN = fibNMinus1 + fibNMinus2; } return fibN; fibNMinus2 fibNMinu1 fibN i 1 1 1 1 + 1 2 2 1 + 2 3 3 2 + 3 5 4 3 + 5 8 5 Složitost: 3*n A0B36PR1 - 07
Složitost výpočtu Fibonacciho čísla 2 Iterační metoda: 3*n Rekurzivní výpočet ~ 2n Podíl dvou po sobě následujících členů konverguje k hodnotě „zlatého řezu“ (Johannes Kepler - golden ratio): φ ≈1,6180339887498948482045868343656 Zlatý řez byl pokládán za středověku za ideální proporci mezi různými délkami Zlatý řez vznikne rozdělením úsečky na dvě části tak, že poměr větší části k menší je stejný jako poměr celé úsečky k větší části A0B36PR1 - 07 Pro zájemce
Další příklady rekurze - fraktály A0B36PR1 - 07 Pro zájemce
Příklad rekurze, základní schéma – součin public static void main(String[] args) { int x,y; ……; System.out.println(" " + souI(x, y) + souR(x,y)); } static int souI(int s, int t){ int souI=0; for (int i = 0; i < s; i++) souI=souI+t; return souI; static int souR(int s, int t) { int souR; if (s > 0)souR=souR(s - 1,t)+t; else souR = 0; return souR; } A0B36PR1 - 07
Shrnutí a perspektiva Naivní styl Procedurální styl Objektový přístup, malý úvod bude Návrhové vzory, další studium A0B36PRI „PROGRAMOVÁNÍ“ 06
Programovací styly Na příkladu programu simulujícího čítač si ukážeme programovací styly: naivní procedurální objektově orientovaný (viz přednáška „Úvod do objektově orientovaného programování") Příklad komunikace programu: Hodnota = 10 0) Konec 1) Zvětši 2) Zmenši 3) Nastav Vase volba: 1 Hodnota = 11 0) Konec 1) Zvětši 2) Zmenši 3) Nastav ... A0B36PRI „PROGRAMOVÁNÍ“ 06
Naivní styl Jednoduché úlohy lze řešit přímočaře: import java.util.*; public class Citac1 { final static int pocHodn = 0; static int hodn, volba; public static void main(String[] args) { Scanner sc = new Scanner(System.in); hodn = pocHodn; do{ System.out.prinln( "Hodnota = " + hodn ); System.out.println ( "0) Konec\n1) Zvětši\n2) Zmenši\n3) Nastav" ); System.out.print( "Vaše volba: " ); volba = sc.nextInt(); switch ( volba ) { case 0: break; case 1: hodn++; break; case 2: hodn--; break; case 3: hodn = pocHodn; break; default: System.out.println( "nedovolená volba" ); } }while (volba > 0); System.out.println( "Konec" ); A0B36PRI „PROGRAMOVÁNÍ“ 06
Procedurální styl Připomeňme hlavní zásady: Zadaný problém se snažíme rozložit na podproblémy Pro řešení podproblémů zavádíme abstraktní příkazy, které nejprve specifikujeme a pak realizujeme pomocí metod Aplikováno na "čítač" identifikujeme tyto dílčí podproblémy: komunikace s uživatelem, jejímž výsledkem je kód požadované operace provedení požadované operace s čítačem Řešení: public class Citac2 { final static int POC_HODN = 0; // třídní konstanta static int hodn; // třídní proměnná static void operace(int op) { // pro všechny 3 operace switch (op) { case 1: hodn++; break; case 2: hodn--; break; case 3: hodn = POC_HODN; break; } A0B36PRI „PROGRAMOVÁNÍ“ 06
Procedurální styl static int menu(){ Scanner sc = new Scanner(System.in); int volba; do { System.out.println( "0. Konec" ); System.out.println( "1. Zvětši" ); System.out.println( "2. Zmenši" ); System.out.println( "3. Nastav" ); System.out.print( "Vaše volba: " ); volba = sc.nextInt(); if (volba < 0 || volba > 3){ System.out.println( "nedovolená volba" ); volba = -1; } }while (volba < 0); return volba; A0B36PRI „PROGRAMOVÁNÍ“ 06
Procedurální styl Poznámky: public static void main(String[] args) { int volba; hodn = POC_HODN; do{ System.out.println("hodnota = " +hodn); volba = menu(); if (volba>0) operace(volba); } while (volba > 0); System.out.println("Konec"); } Poznámky: procedurální řešení čítače (jediná procedura pro všechny operace, třídní proměnná pro hodnotu, třídní konstanta udávající počáteční hodnotu) je nedokonalé (proč?) čítač je typ, pro který jsou definovány určité operace (realizované metodami) a jehož implementace (proměnná pro hodnotu) by neměla být volně přístupná tedy pro realizaci čítače je vhodnější objektově orientovaný styl (nebo lokální statická proměnná – viz úvod do jazyka "C"). A0B36PRI „PROGRAMOVÁNÍ“ 06
Shrnutí Další informace k této přednášce hledejte např. v: Herout, P.: Učebnice jazyka Java, Kopp, Č.Budějovice, 2010, str.114 – 128.
Procedurální programování A0B36PRI - PROGRAMOVÁNÍ Procedurální programování a rekurze Jazyk JAVA Konec České vysoké učení technické Fakulta elektrotechnická