Java programozás 12. – Ciklusok

Ciklusok, avagy “na még egyszer”

Programozás esetén nagyon sok esetben előfordul az, hogy valamilyen tevékenységet (utasításokat) többször meg kell ismételni. Ilyenek a való életben is sokszor előfordulnak.

  • Szúrj be 5 üres sort a táblázatba.
  • Készíts 3 szendvicset
  • Dobj 3 kockával (egyszer dobunk, de 3 számot sorsolunk)
  • Írd le 100x, hogy nem felejtem el a házi feladatomat

Ezek a többször ismételt tevékenységek megegyeznek abban, hogy előre tudjuk, hányszor kell elvégezni őket. Persze olyan is előfordul, hogy addig kell végezni valamit, amíg lehet.

  • Hámozz meg 2 kg almát
  • Mosogass el
  • Készíts annyi szendvicset, amíg el nem fogy a felvágott

Ezeket a tevékenységeket is többször kell ismételni, de nem tudom hányszor. Lehetnek kisebb almák, abból többet kell hámozni, de ha nagyobbak, kevesebb darabból is kijön a 2 kg. Addig mosogatok, amíg van edény. Addig készítem a szendvicseket, amíg van mit rátenni.

A programozásban is ezek az elvek érvényesülnek. Ezek szerint 3 különféle ciklus típust különíthetünk el:

  1. Növekményes ciklus
  2. Elöl tesztelő ciklus
  3. Hátul tesztelő ciklus

A működés alapelve szerint az első különbözik a többitől. Ebben az esetben előre tudjuk, hogy hányszor akarjuk ismételni a teendőinket, míg az utóbbi két esetben az ismétlés darabszáma feltételhez kötött. Ettől a kép valójában kicsit árnyaltabb, mert a Java nagyon rugalmas a ciklusaival, de az alapelvek ezek.

Növekményes ciklus – for

Kezdjük az elsővel, a növekményes ciklussal. Az ilyen típusú ciklus a következőképp néz ki formailag:

for( ciklusváltozó beállítása; futási feltétel; ciklusváltozó növelése )
{
  utasítás1;
  utasítás2;
  ...
  utasításN;
}

Konkrét példával:

for( int i = 0; i < 20; i++ )
{
  System.out.println(i);
}

A for kulcsszó vezeti be a ciklust. Ezután jön a ciklus feje, ahol 3 dolog állítható be:

  1. a használandó ciklusváltozó kezdőértéke
  2. a futási feltétel, vagyis mikor kezdődjön újabb “kör”
  3. a ciklusváltozó növelése

A szép (vagy épp csúnya, de ez nézőpont kérdése) a dologban az, hogy ebből a 3 dologból semmi nem kötelező. Bármelyik, vagy akár mind elhagyható, az egyetlen megkötés, hogy a 3 részt elválasztó ; jelek mindegyike megmaradjon:

int i = 0;
for( ; ; )
{
  if( i == 20 ) break;
  System.out.println(i);
  i++;
}

Igaz, ez kicsit kifordítja a feladatot és eddig ismeretlen utasítást is tartalmaz, de látjuk azt, hogy lehetséges.

Nos, a for ciklus tehát alapesetben arra szolgál, hogy egyesével növelve egy változót valamilyen tevékenységet addig hajtsunk végre, ameddig azt az általunk megadott feltétel engedi. Példákon keresztül ez érthetőbb lesz:

Számoljunk el 1-től 50-ig és írjuk ki a képernyőre a számokat:

for( int i = 1; i <= 50; i++ )
{
  System.out.println(i);
}

Ha megnézzük a ciklusfejet, a következőket láthatjuk:

  1. a ciklusváltozót 1-től indítjuk
  2. addig megyünk, amíg el nem érjük az 50-et
  3. a ciklusváltozót egyesével növeljük

Persze ezt így is írhattam volna:

for( int i = 0; i < 50; i++ )
{
  System.out.println(i + 1);
}

0-tól megyek 49-ig, de mindig eggyel nagyobb számot írok ki.

Sorsoljunk ki 10 véletlen számot az [1;50] intervallumból és írjuk ki őket:

for( int i = 0; i < 10; i++ )
{
  System.out.println( (int)(Math.random() * 50) + 1 );
}

A fenti ciklusfej nagyon erős típusfeladat és egy alapszabályt láthatsz benne: Az i-t indítsd 0-ról és addig menj, amíg kisebb, mint az a szám, amennyiszer a ciklust futtatni akarod:

for( int i = 0; i < 10; i++ )  // 10-szer
for( int i = 0; i < 20; i++ )  // 20-szor
for( int i = 0; i < 100; i++ ) // 100-szor

Írjuk ki 2-től indulva 20 páros számot:

for( int i = 1; i <= 20; i++ )
{
  System.out.println(i * 2);
}

Itt annyi trükköt alkalmazok, hogy 1-től számolok el 20-ig (ennyi szám kell), de ezeket 2-vel szorozva mindig páros számot kapok. Persze ha az alapszabályt tekintem, amikor 0-tól indítom a ciklust, és a határ-1-nél állok meg, akkor így is írhatnám ugyanezt:

for( int i = 0; i < 20; i++ )
{
  System.out.println((i + 1) * 2);
}

És ha az 1-től indulva kell 20 páratlan?

for( int i = 1; i <= 20; i++ )
{
  System.out.println(i * 2 - 1);
}

A párosokból 1-et kivonva páratlanokat kapunk. Vagy 0-tól indulva:

for( int i = 0; i < 20; i++ )
{
  System.out.println(i * 2 + 1);
}

De csak hogy lássuk milyen rugalmas is a for ciklus, lássunk a párosokra egy másik megoldást:

for( int i = 2; i <= 40; i += 2 )
{
  System.out.println(i);
}

Na jó, kicsit csaltam. Tudom, hogy a 40 lesz az utolsó, viszont nem szorozgatok, hanem a ciklusváltozót most nem 1-gyel, hanem 2-vel növelgetem. Itt egy jó példa a += operátorra.
Egy szó, mint száz, a for ciklus egy rugalmas és hatékony eszköz akkor, ha előre tudom, hogy hányszor akarok valamit végrehajtani. De az már egyértelmű, miért olyan szerteágazó az egész, mert ugyanarra a problémára rengeteg fajta megoldást adhatok, és mindegyik tökéletesen megoldja a feladatot. Annyira árnyalatnyi különbségek vannak közöttük, hogy ezzel középiskolai szinten egyáltalán nem kell foglalkozni, a lényeg: helyes megoldást adjon.

Elöl tesztelő ciklus – while

Az elöl tesztelő ciklust jellemzően akkor használjuk, ha nem tudjuk előre, hogy hányszor kell az ismétlődő tevékenységet végrehajtani. Azzal nincs gond, ha ki kell sorsolni 10 számot egy intervallumból. Ez egy for ciklusnak megfelelő típusfeladat. De ha az a feladat, hogy egy adott intervallumból 10 darab páratlan számot kell sorsolni? Akkor ha véletlenül páros számot kaptál azt figyelmen kívül kell hagyni. Lássuk először a while ciklus általános alakját:

while( feltétel )
{
  utasítás1;
  utasítás2;
  ...
  utasításN;
}

Mint láthatod itt is van egy ciklusfej, ami a futási feltételt tartalmazza. Ez működési szempontból azt jelenti, hogy a ciklus akkor fut le (hajtja végre a ciklusmagot), ameddig a feltétel igaz. Természetesen arra figyelni kell, hogy a feltétel egyszer teljesülhessen, vagy a ciklusmagban szakítsuk meg a futást, nehogy végtelen ciklusba fussunk. Ez azt jelenti, hogy soha nem állhat le, mert vagy nem állítjuk meg, vagy a futási feltétel soha nem lehet hamis. A legegyszerűbb végtelen ciklus:

while( true )
{
  System.out.println("fut");
}

Lássuk akkor az előző példát, sorsoljunk ki 10 páratlan számot egy adott intervallumból [1;100]

int db = 0;
int szam;
while( db != 10 )
{
  szam = (int)(Math.random() * 100) + 1;
  if( szam % 2 != 0 )
  {
    System.out.println(szam);
    db++;
  }
}

Nézzük akkor sorban, mit is csinál ez a program:

  1. kell egy változó, ami azt számolja majd, hogy hány páratlan számot sorsoltunk, mert a párosokkal nem foglalkozunk
  2. deklaráltam egy szam nevű változót, ahol az aktuálisan kisorsolt számot tároljuk
  3. a ciklust futási feltétele az, hogy amíg nincs 10 páratlan szám, addig sorsolgasson
  4. a ciklusban sorsolok egy számot, és eltárolom
  5. miután kisorsoltam, megvizsgálom, hogy páratlan-e
  6. ha páratlan, akkor kiírom a sorsolt számot, és növelem eggyel a számlálót
  7. ha nem páratlan, akkor a ciklusmagból semmi nem hajtódik végre, mert nem megfelelő a szám és ismét próbálkozik egy sorsolással

Jöjjön egy másik jó példa az elöl tesztelő ciklusra. Számítsuk ki két egész szám osztóját. Nem, nem a prímtényezős alakra bontással oldjuk meg, hanem egy jól programozható megoldást adunk, mely a következő – egyébként már meglévő – algoritmust takarja:
Addig kell kivonni a két szám közül a nagyobból a kisebbet, amíg a két szám egyenlő nem lesz. Ha a két szám egyenlő, az az eredeti számok legnagyobb közös osztója. Az könnyen belátható, hogy megjósolhatatlan, hányszor kell az említett kivonást elvégezni, tehát for ciklust nem igazán alkalmazhatunk. (lehetne, de elég kitekert megoldás lenne)

int szam1 = 660;
int szam2 = 366;
while( szam1 != szam2 )
{
  if( szam1 > szam2 )
  {
    szam1 = szam1 - szam2 ;
  }
  else
  {
    szam2 = szam2 - szam1 ;
  }
}
System.out.println("A ket szam legnagyobb kozos osztoja: " + szam1);

Na, nézzük, mit is csinál ez a program:

  1. deklarálunk 2 változót a vizsgált számoknak
  2. kell egy ciklus azzal a futási feltétellel, hogy addig kell a kivonásokat ismételni, amíg a két szám nem egyenlő
  3. ha a szam1 a nagyobb, akkor abból vonjuk ki a szam2-őt
  4. fordított esetben a szam2-ből vonjuk ki a szam1-et
  5. amikor a ciklus befejeződik, akkor a két szám bármelyike (mivel egyenlőek) a legnagyobb közös osztót jelenti, amit ki is íratunk

Ha észrevetted, az is előfordulhat, hogy a ciklus egyszer sem fut le. Mi van akkor, ha a két szám alapból egyenlő? Akkor is kiírathatom bármelyiket, vagyis a ciklus utáni sorra lép a program és kiírja az egyiket. Az elöl tesztelő ciklusnál lehetséges, hogy a ciklus egyszer sem fut le.

Hátul tesztelő ciklus – do-while

A do-while ciklus az előzőleg ismertetett while-ra hasonlít abban a tekintetben, hogy ezt a fajta ciklust is akkor használjuk, amikor nem tudjuk előre, hogy hányszor kell egy utasítás sorozatot végrehajtani. Azonban mégis van egy fontos különbség a kettő között. Lássuk az általános alakot, akkor egyértelmű lesz:

do
{
  utasítás1;
  utasítás2;
  ...
  utasításN;
}
while( feltétel );

Mint a neve is mutatja, itt a ciklus feje hátul van (a feltétellel együtt) és a ciklusmagba tartozó utasítások elöl. Ez azt jelenti, hogy a ciklusmag 1-szer mindenképpen lefut, mert ez a ciklus először végrehajt, utána vizsgálja meg, hogy szükséges-e többször! Jellemzően olyan feladatoknál használjuk, amikor egyszer mindenképp végre kell hajtani valamit, de utána ellenőrizni kell, hogy amit kaptunk megfelelő-e, mert ha nem, csináljuk meg újra. Ilyen például az a számsorsolás, ami valamilyen feltételhez kötött:

Sorsolj ki egy páros számot a [10;50] intervallumból. Azt még egyszerűen megoldjuk, hogy az adott intervallumból sorsoljunk, de azzal a plusz feltétellel már nem tudunk mit kezdeni, hogy ez páros is legyen. Ezért addig sorsolunk, hogy a feltételnek megfelelő számot kapjunk:

int szam;
do
{
  szam = (int)(Math.random() * 41) + 10;
}
while( szam % 2 != 0 );

Nézzük akkor a programot részenként:

  • sorsolunk egy számot
  • ha a szám 2-vel osztva nem 0 maradékot ad (páratlan), akkor a ciklus újraindul, vagyis megint sorsol egyet
  • a ciklus akkor áll meg, ha a feltétel hamis lesz (páros)

Azért jó itt a do-while ciklus, mert mindenképpen sorsolnom kell egy számot ahhoz, hogy megvizsgálhassam, meg kell-e ismételni a sorsolást. Természetesen összetett feltételt is megadhatok. Mondjuk olyan számot sorsoljunk az adott intervallumból, ami 2-vel és 5-tel is osztható:

int szam;
do
{
  szam = (int)(Math.random() * 41) + 10;
}
while( !(szam % 2 == 0 && szam % 5 == 0) );

Itt a ciklus futási feltételeként a kiemelt sorban egy összetett feltételt láthatsz, ami azért nem biztos, hogy annyira egyértelmű, mint amilyennek elsőre tűnik. A ciklus ugye akkor működik, ha a feltétel igaz. De itt eredetileg két részfeltételünk van, 2-vel és 5-tel osztható szám kell. Az már nem jó, ha esetleg egyik, vagy az sem, ha mindkét részfeltétel hamis. Igen ám, de a ciklus futási feltételeként nem azt kell megadni nekünk, amilyen számra nekünk szükségünk van, hanem pont az ellenkezőjét. Azt kell megadni, hogy milyen szám nem jó nekünk! Nézzük akkor lépésenként:

  • szerkesszük meg azt a feltételt, ami nekünk megfelelő (ami összetett feltétel is lehet)
  • negáljuk az egészet

Természetesen itt is igaz, hogy ha akarjuk, egyszerűsíthetjük az összetett feltételt a már tanult módon:

while( !(szam % 2 == 0 && szam % 5 == 0) ); // 1. verzió
helyett
while( szam % 2 != 0 || szam % 5 != 0 );    // 2. verzió

Akkor milyen ciklust válasszunk?

Azt, hogy milyen ciklust válasszunk egy feladat megoldásához hosszú távon inkább a tapasztalatunk mondja majd meg. A programozás sajátossága, mint előzőleg már említettem, hogy ugyanazt a feladatot nagyon sokféle módon lehet helyesen megoldani. Gyakorlatilag a ciklusoknál is minden feladat megoldható mindhárom ciklussal, néha valóban árnyalatnyi különbségek vannak közöttük. Mégis, az alap támpontot a következők adják:

  • Ha tudom hányszor fusson a ciklus, akkor for ciklus.
  • Ha nem tudom hányszor fusson a ciklus ÉS lehet, hogy egyszer sem kell, akkor while ciklus.
  • Ha nem tudom hányszor fusson a ciklus ÉS egyszer mindenképpen kell, akkor do-while ciklus.

Maradjunk az egyik előző példánál, ahol a legnagyobb közös osztót kerestük. Ha emlékszel, addig vonjuk ki a nagyobb számból a kisebbet, amíg a két szám egyenlő nem lesz. Melyik ciklussal célszerű megoldani?

  • Mivel nem tudjuk hány kivonás kell, ezért ne for ciklus legyen.
  • Lehet, hogy a két szám már az elején egyforma, akkor egyszer sem kell kivonni a nagyobból a kisebbet, ezért do-while se legyen (mert az egyszer feltétel nélkül lefut).
  • Vagyis maradt a while ciklus.

Ha mondjuk tudom, hogy 10x kell futni a ciklusnak, mert 10 számot kell sorsolni, akkor adja magát, hogy for ciklus legyen, mivel abban egyszerűbb egy számlálót kezelni.

Saját tapasztalat szerint azt mondom, hogy talán a do-while ciklus a legritkább, és a for ciklus a leggyakrabb a programozásban. A for ciklust is inkább a tömbök miatt használjuk gyakrabban.

Következő lecke: Osztályok és objektumok

17 Replies to “Java programozás 12. – Ciklusok”

  1. Pingback: Java programozás 16. – Alap algoritmusok |

  2. Pingback: Java programozás 11. – Véletlen számok

  3. Egy fontos kérdésem lenne és remélem tudsz benne segíteni: mondjuk én szeretnék bekérni egy random számot legyen ez mondjuk az 5-ös, csinálnák egy do whilet 1 és 10 között ami addig megy amíg 5-ös nem lesz a szám, és én szeretném megkapni mondjuk azt hogy ehhez hányszor kellett sorsolnia a programnak akkor az hogyan nézne ki?

    • Az adatbekérést és a véletlen számok sorsolását ne keverjük. Ha jól értettem, akkor az lenne a feladat, hogy hányszor kell sorsolni ahhoz az 1-10 intervallumból egy számot, hogy 5-öt kapjunk?

      • hát végülis majdnem bekérünk egy számot ami 1 és 10 között van , majd a bekért szám tegyük fel hogy 5-öt írtunk be és hányszor kell sorsolnunk ahhoz hogy 5 legyen az a szám! 🙂

        • Scanner sc;
          sc = new Scanner(System.in);
          int szam;
          System.out.println(“Kérem az első tippet: “);
          int tipp1;
          tipp1 = sc.nextInt();
          do
          {
          szam = (int)(Math.random()*10)+1;
          }
          while( szam!=tipp1 );

          System.out.println(szam);

          }

          itt a kód hogy jobban értsd eddig jutottam.

          • Ebbe már csak egy számláló kellene, amit nullázol a do előtt, és a ciklusmagban minden sorsoláskor növelsz eggyel. A ciklus után pedig kiírod a számláló értékét.

            int db = 0;
            do
            {
            szam = (int)(Math.random()*10)+1;
            db++;
            }
            while( szam!=tipp1 );
            System.out.println(db);

  4. Szia, olyan kérdésem lenne, hogy ha én egy bizonyos eredményt kérek be, de az rossz, hogyan adhatom meg a próbálkozások számát, hogy három rossz után kilépjen a programból, míg jó esetén mondjuk kiírja, hogy helyes.

    • Az ilyen feladatokat megoldhatjuk mondjuk egy do-while-t, ahol kilépési feltételként vagy a ciklusban növelt számláló értékét ellenőrizzük (amit minden körben növelünk 1-gyel), hogy elérte-e a 3-at, ÉS azt is, hogy megfelelő-e a ciklusban megadott szám. Ilyenkor a kilépési feltétel a kétféle szempont alapján kicsit bonyolult is lehet.

      3 próbálkozás páratlan szám bekérésére:

      Scanner sc = new Scanner(System.in);
      int szam;
      int db = 0;

      do
      {
      System.out.print(“Adj meg egy paratlan szamot: “);
      szam = sc.nextInt();
      db++;
      }
      while( db != 3 && szam % 2 == 0 );

      if( szam % 2 == 0 )
      {
      return;
      }

      System.out.println(szam);

      Természetesen más ciklust is használhatunk. Ha előre tudjuk, hogy 3-szor próbálkozhat, akár for ciklust is használhatunk. Akkor 3 sikertelen próba után a ciklus úgyis megáll. A cikluson belül pedig elvégezzük az adatbekérést, és ha az eredmény megfelelő, akkor egy break-kel megszakítjuk a ciklust.

      Scanner sc = new Scanner(System.in);
      int szam = 0;
      for( int i = 0; i < 3; i++ ) { System.out.print("Adj meg egy paratlan szamot: "); szam = sc.nextInt(); if( szam % 2 != 0 ) { break; } } if( szam % 2 == 0 ) { return; } System.out.println(szam); Sőt, a for ciklust is lehet nem szokványos módon használni, ami azért pár hibalehetőséget is rejt magában: Scanner sc = new Scanner(System.in); int szam = 0; for( int i = 0; i < 3 && szam % 2 == 0; i++ ) { System.out.print("Adj meg egy paratlan szamot: "); szam = sc.nextInt(); } if( szam % 2 == 0 ) { return; } System.out.println(szam); Bizonyos helyeken az oktatók tudomásom szerint nem szeretik, sőt tiltják a break és continue utasításokat a ciklusok használatakor, mondván, hogy ezek ugrási utasításként rontják az átláthatóságot. Ezzel az utolsó példával a for ciklusban például ezt kerültem ki (összetett futási feltétellel), de nem hiszem, hogy sokan oldanák meg ilyen módon. Én sem tartom szerencsésnek. Az ilyen feladatra egy do-while talán ideálisabb megoldási mód.

      • Ahh csak most olvasom a válaszodat…. ennyire nem gondoltam végig… nagyon köszönöm a gyors és alapos válaszod, máris nézem, hogyan gondoltad… ezer hála! 🙂

  5. Kedves Csaba! Köszönet a remek leírásokért, nagy segítség a tanulásban. Itt azonban találtam egy elírást, a “A legegyszerűbb véletlen ciklus:” helyett a “A legegyszerűbb végtelen ciklus:” lenne a helyes. Szép napot!

Hozzászólás a(z) Mikó Csaba bejegyzéshez Válasz megszakítása

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük

*

Ez az oldal az Akismet szolgáltatást használja a spam csökkentésére. Ismerje meg a hozzászólás adatainak feldolgozását .