1 Tic-tac-toe – 2D tömb
3×3-as amőba (tic-tac-toe) játékot csinálunk. A 3×3-as pálya egyes állapotai lehetnek: üres (még nem rajzolt oda senki), kör és iksz.
- Hozzunk létre egy ilyet tároló adatszerkezetet!
- Rajzoljuk ki a pályát karakterekkel!
- Írjunk programrészt, amelyik ellenőrzi, hogy nyert-e az iksz játékos!
Megoldás
A pálya egyes celláinak tárolására egyik megoldás az,
hogy karaktereket használunk. A szóköz jelentheti az üres cellát,
az x
az egyik, az o
pedig a másik játékost.
A pálya ezen cellák két dimenziós tömbje – vagyis
tömbben tömb. Mivel egy sorban sok ilyen van; és a sorokból is sok van.
A programban meg kell vizsgálni, az egyes sorokban, oszlopokban stb. van-e az x játékosnak nyerő helyzete; ha bármelyikben igen, akkor az egész állás számára nyerő.
A pálya kirajzolása ciklusban ciklus. Vegyük észre: egy dimenziós tömb → ciklus, két dimenziós tömb (tömbben tömb) → ciklusban ciklus.
#include <stdio.h> int main() { typedef char Babu; typedef Babu Palya[3][3]; Palya p = { { 'o', 'o', 'o' }, { 'x', 'o', ' ' }, { 'x', 'x', ' ' }, }; int x, y; int nyert; /* kirajzolás */ printf("+---+\n"); for (y=0; y<3; y++) { printf("|"); for (x=0; x<3; x++) printf("%c", p[y][x]); printf("|\n"); } printf("+---+\n"); /* nyert-e az x? */ nyert=0; /* vizszintesen --- */ for (y=0; y<3; y++) if (p[y][0]=='x' && p[y][1]=='x' && p[y][2]=='x') nyert=1; /* fuggolegesen ||| */ for (x=0; x<3; x++) if (p[0][x]=='x' && p[1][x]=='x' && p[2][x]=='x') nyert=1; /* atlosan \ */ if (p[0][0]=='x' && p[1][1]=='x' && p[2][2]=='x') nyert=1; /* atlosan / */ if (p[0][2]=='x' && p[1][1]=='x' && p[2][0]=='x') nyert=1; if (nyert) printf("iksznek van nyero harmasa!\n"); else printf("iksznek nincs nyero harmasa.\n"); return 0; }
A megoldás kicsit kezdetleges. A karakteres ábrázolás hátránya, hogy az egyes cellák ezen kívül más értéket is felvehetnek. Ennek kiküszöbölésére is jó az ún. felsorolt típus, amelyről az előadáson lesz szó.
Ugyancsak, a nyerést ellenőrző programrészt legjobb lenne paraméteresen megoldani – vagyis egy függvényre lenne szükségünk, amely a pályát (tömböt) paraméterként kapja, és egy játékost is, akinek a nyerő állását ellenőrizni kell. (Hiszen ugyanígy működne a program, ha a kör játékos szempontjából kellene ellenőrizni a nyerő állást.) Erről is a következő előadáson lesz szó.
2 Időpontok
Írjunk programot, amely egy struktúrában időpontot tárol: óra, perc. Írjunk függvényeket ehhez:
ido_kiir(i)
: kiírja az időpontot óra:perc formában.ido_hozzaad(i, p)
: hozzáadp
percet azi
időponthoz, és visszatér az új időponttal. Pl. 15:15 + 45 = 16:00.ido_eltelt(i1, i2)
: megmondja, hány perc telt el a két időpont között, pl. 16:30-15:15 = 75 perc. (A paraméterek sorrendje a kivonásnál megszokott: kisebbítendő, kivonandó.)ido_kivon(i, p)
: kivonp
percet azi
időpontból, és visszatér az új időponttal. Pl. 15:45 - 30 = 15:15.
Megoldás
A 60 perc és a 24 óra kezelésére a percek hozzáadásánál nagyon jól
használható a maradékképzés. Pl. 16:55+10
esetén: :55+10 = :65
,
ami helyett a következő óra :05
kellene. 65%60
, vagyis
a 60-nal osztás maradéka pont a percet adja, 65/60
, maga az osztás pedig 1-et,
amennyivel az órát meg kell növelni. Az órát utána egyszerűen 24-gyel modulózzuk,
mert a napokkal már nem kell foglalkozni.
A kivonás nem megy ilyen egyszerűen, mert (5-10)%60 = (-5)%60 = -50
.
Ott sajnos külön kell kezelni a keletkező negatív értékeket.
#include <stdio.h> /* Időpontot tárol egy napon belül: óra és perc. */ typedef struct Ido { int ora, perc; } Ido; /* Kiírja a paraméterként kapott időpontot óra:perc formában. */ void ido_kiir(Ido i) { printf("%02d:%02d", i.ora, i.perc); } /* Hozzáad a megadott i időponthoz p percet, és visszatér * az így kapott időponttal. A hozzáadott percek száma azért * unsigned, mert negatív számra helytelenül működne a függvény. */ Ido ido_hozzaad(Ido i, unsigned p) { Ido uj; uj.perc=(i.perc + p)%60; uj.ora=(i.ora + (i.perc + p)/60)%24; return uj; } /* Kiszámolja, hány perc telt el i1-től i2-ig. A paraméterek * sorrendje a kivonásnál megszokott: i2-i1, rendre a * kisebbítendő és a kivonandó. Azzal a feltételezéssel ad * helyes eredményt, hogy a két időpont egy napon van. */ int ido_eltelt(Ido i2, Ido i1) { return i2.ora*60-i1.ora*60+i2.perc-i1.perc; } /* Kivon valahány percet a megadott időpontból, és az így keletkező * új időponttal tér vissza. Negatív p-re helytelenül működne, ezért * unsigned a paramétere. */ Ido ido_kivon(Ido i, unsigned p) { Ido uj; uj.perc = i.perc-p; uj.ora = i.ora; while (uj.perc < 0) { uj.perc += 60; uj.ora -= 1; } while (uj.ora < 0) uj.ora += 24; return uj; } int main() { Ido i1 = { 11, 50 }, i2 = { 12, 10 }, i3 = { 3, 30 }; printf("i1 = "); ido_kiir(i1); printf("\n"); printf("i2 = "); ido_kiir(i2); printf("\n"); printf("i3 = "); ido_kiir(i3); printf("\n"); printf("i2-i1 = %d\n", ido_eltelt(i2, i1)); printf("i1+195 = "); ido_kiir(ido_hozzaad(i1, 195)); printf("\n"); printf("i2-195 = "); ido_kiir(ido_kivon(i2, 195)); printf("\n"); printf("i3-240 = "); ido_kiir(ido_kivon(i3, 240)); printf("\n"); return 0; }
3 Római számok II.
Elevenítsük fel a római számok kiírása programot! Ott a számok értéke szerint csökkenő sorrendben haladtunk. Valahogy így:
… if (szam>=5) { printf("V"); szam-=5; } if (szam>=4) { printf("IV"); szam-=4; } while (szam>=1) { printf("I"); szam-=1; } …
Figyelmesen vizsgálva a problémát rájöhettünk arra, hogy bármelyik
feltétel kicserélhető ciklusra ebben a feladatban. Azért írtunk
feltételt a szam>=5
kifejezéshez, mert tudjuk, hogy
legfeljebb csak egyszer fog teljesülni – de akár írhattunk
volna ciklust is.
Ha mindegyik helyre ciklus kerül, akkor így néz ki a kód:
… while (szam>=5) { printf("V"); szam-=5; } while (szam>=4) { printf("IV"); szam-=4; } while (szam>=1) { printf("I"); szam-=1; } …
Ez a sorminta már csak arra vár, hogy ciklust csináljunk belőle. Írjuk át a programot!
Megoldás
Minden római számhoz (pl. V, egy sztring C-ben) egy érték tartozik (pl. 5, egy egész szám C-ben), ez struktúrába való. A sok római szám struktúrái pedig tömbbe. Használhatjuk azt a trükköt, mint a lenti bankautomatánál is, hogy egy oda nem illő értékkel megjelöljük a tömb végét. Mivel a rómaiak nem használtak 0-t, a tömb végét jelölheti egy olyan elem, ahol az érték 0.
#include <stdio.h> int main() { typedef struct Romai { char romai[5]; int ertek; } Romai; Romai szamjegyek[] = { { "XC", 90 }, { "L", 50 }, { "XL", 40 }, { "X", 10 }, { "IX", 9 }, { "V", 5 }, { "IV", 4 }, { "I", 1 }, { "", 0 } /* tömb végét jelző */ }; int szam; printf("Mi a szám? "); scanf("%d", &szam); if (szam<1 || szam>99) printf("Ez nekem már sok. Csak 99-ig tudom.\n"); else { int i; i=0; while (szamjegyek[i].ertek>0) { while (szam>=szamjegyek[i].ertek) { printf("%s", szamjegyek[i].romai); szam-=szamjegyek[i].ertek; } ++i; /* következő római szám */ } printf("\n"); } return 0; }
A programba épített táblázat egy inicializált tömb a megfelelő római jelekkel és a hozzájuk tartozó értékkel. Ezt a függvényen belül is létrehozhatjuk, mivel máshol nem használjuk. A struktúra definícióját egybe is építhetjük a tömb változó létrehozásával. Ilyenkor még az is lehetséges, hogy a struktúra névtelen, mivel sehol máshol nem kell hivatkozni név szerint a típusra:
struct { char romai[5]; int ertek; } szamjegyek[] = { { "XC", 90 }, { "L", 50 }, { "XL", 40 }, { "X", 10 }, { "IX", 9 }, { "V", 5 }, { "IV", 4 }, { "I", 1 }, { "", 0 } /* tömb végét jelző */ };
4 Bankautomata II.
Idézzük fel a múltkori bankautomatás feladatot! A feladatkiírás azt kérte, hogy írjunk egy programot, amely egy adott pénzösszeget a megadott névértékű bankjegyekre és érmékre bont le. Pl. 4200 Ft = 2×2000 Ft + 1×200 Ft.
Csavarjuk meg ezt! Az automata rekeszei végesek. Tároljuk el azt is, hogy melyik bankjegyből és érméből épp mennyi van! Írjuk meg a programot, amelyik úgy ad pénzt, hogy ezt figyelembe veszi!
Megoldás
Ez egy kiváló példa a struktúra használatára.
Összetartozó adat a rekeszben található címlet és a hozzá
tartozó darabszám, pl. hogy 20000 forintosból 20 darab van. Hogy
mindkettő egész szám (a forint és a darab), senkit ne jtévesszen
meg, ez nem tömb! Minden rekeszhez egy struktúra tartozik. A
rekeszekből viszont sok van, és egyformák: ezért a
rekeszeknek egy tömbje lesz. A használt adatszerkezet
struktúrák tömbje: struct Rekesz penzek[]
.
A ciklusban először egy osztással kiszámoljuk, hogy mennyi kellene az adott címletből. Utána pedig megnézzük, van-e annyi egyáltalán. Ha nincs, akkor csak kevesebbet adunk ki.
#include <stdio.h> int main() { typedef struct Rekesz { int ertek; int darab; } Rekesz; Rekesz penzek[]={ {20000, 20}, /* huszezresbol 20 db */ {10000, 0}, /* tizezres kifogyott */ {1000, 10}, {500, 50}, {20, 197}, {10, 123}, {5, 19}, {0, 0} /* nullaval jelzem a tomb veget */ }; int mennyit; int i; printf("Mennyit kene adni? "); scanf("%d", &mennyit); printf("Az automata kiadja:\n"); for (i=0; penzek[i].ertek!=0; i++) { int hany_db; hany_db=mennyit/penzek[i].ertek; /* ennyit kene */ if (penzek[i].darab<hany_db) /* nincs ennyi? */ hany_db=penzek[i].darab; /* jobb hijan... */ if (hany_db>0) { /* ha adunk ebbol (mert kell es mert van) */ printf("%d db %d Ft-os.\n", hany_db, penzek[i].ertek); mennyit-=hany_db*penzek[i].ertek; penzek[i].darab-=hany_db; /* innen kivesszuk. */ } } if (mennyit!=0) printf("Nem tudok rendesen adni! Kene meg: %d Ft\n", mennyit); return 0; }
A fenti algoritmus amúgy nem tökéletes. Pl. ha 6000-t kérünk, és van 5000-es és 2000-es, de nincs 1000-es, akkor ki akar adni egy 5000-est, és utána megáll – nem veszi észre, hogy 3 darab 2000-essel megoldható. A tökéletes megoldáshoz az ún. visszalépéses keresést kellene alkalmazni, amelyhez a tudnivalók majd később szerepelnek az előadáson.
5 Dátumok, öröknaptár
Írjunk programot, amely egy struktúrában dátumot tárol: év, hónap, nap. Kezeljék ezeket függvények:
datum_kiir(d)
: kiírja a dátumot év.hónap.nap formában.datum_ev_napja(d)
: megmondja, az év hányadik napja. Vegye figyelembe a szökőéveket! (Ehhez csak elő kell szedni a 4. gyakorlat feladatát – tekinthetjük azt akár kidolgozottnak is.)datum_kivon(d1, d2)
: megmondja, hány nap telt el d2-től d1-ig, ahol d1 a kisebbítendő, d2 a kivonandó.milyen_nap(d)
: megmondja, milyen napra esik az adott dátum. 1=hétfő, 7=vasárnap. 1900. január 1. hétfőre esett.
Megoldás
#include <stdio.h> /* a dátum típusunk */ typedef struct Datum { int ev, honap, nap; } Datum; /*kiírja a dátumot éééé.hh.nn formában */ void datum_kiir(Datum d) { printf("%4d.%02d.%02d", d.ev, d.honap, d.nap); } /* segédfüggvény: szökőév-e? */ int szokoev(int ev) { return ev%400==0 || (ev%100!=0 && ev%4==0); } /* megmondja, hogy az év hányadik napja */ int datum_hanyadik(Datum d) { /* hány egész hónapból adódó nap telt el eddig */ int honapok[]={ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int hanyadik, h; hanyadik=0; for (h=1; h<d.honap; ++h) hanyadik+=honapok[h-1]; hanyadik+=d.nap; if (szokoev(d.ev) && d.honap>2) hanyadik+=1; return hanyadik; } /* hány nap telt el d1-től d2-ig? * csak akkor működik helyesen, ha d2>d1. */ int datum_kivon(Datum d2, Datum d1) { int kulonbseg, ev; /* a különbség: amennyi különbség van a napok között */ kulonbseg=datum_hanyadik(d2)-datum_hanyadik(d1); /* plusz amennyi különbség van az évek között */ for (ev=d1.ev; ev<d2.ev; ev+=1) kulonbseg += szokoev(ev) ? 366:365; return kulonbseg; } /* megmondja, milyen napra esett az adott nap. * 1=hétfő, 2=kedd, 7=vasárnap. */ int milyen_nap(Datum d) { Datum viszonyitas = { 1900, 1, 1 }; /* hétfő */ /* megnézzük, hány nap telt el. modulo 7 miatt 0..6 * lesz az eredmény (7 nap egy héten), ahol 0 lesz * a hétfő, mert a fenti dátumhoz képest. */ return datum_kivon(d, viszonyitas)%7 + 1; } int main() { Datum ma={ 2013, 2, 4 }, eleje={ 2012, 9, 3 }; printf("Ma: "); datum_kiir(ma); printf(", a hét %d. napja.\n", milyen_nap(ma)); printf("A szorgalmi időszak kezdete: "); datum_kiir(eleje); printf(", ennyi nap telt el: %d.\n", datum_kivon(ma, eleje)); printf("%d. oktatási hét van.\n", datum_kivon(ma, eleje)/7+1); return 0; }
A datum_kivon()
függvény végzi a dátumok kivonását.
A működésének az a lényege, hogy kivonja egymásból azt a két számot,
amely a két dátum év kezdete óta teltelt napjainak száma; és ehhez
adja hozzá az egész eltelt évekből adódó 365 vagy 366 napokat. Ez
egy példán jól látszik. Ha a 2012.09.03→2013.10.06 eltelt
napokat kell kiszámolni, akkor a 10.06-ból 279, a 09.03-ból 247
adódik. Azaz 279-247=32 nap telik el szept. 3 és okt. 6 között.
Ehhez kell hozzáadni még egy évnyit. Ha a hónapok szerint visszafelé
megyünk (pl. 2012.09.03→2013.02.25, szeptember→február),
akkor az összeg első tagja
negatív, de ez utána korrigálódik a hozzáadott teljes év által.
(Mintha ugranánk egy évet előre, aztán visszajönnénk a megadott
dátumig.)
Megfigyelhetjük, hogy a honapok[]
tömbnek mindig az
első honap-1
elemét összegezzük. Megírhatnánk úgy is
a programot, hogy nem a hónapok napjainak számát, hanem ezeket
az összegeket tartalmazza a tömb: 0 (január), 31 (február), 59=31+28 (március)
stb. Így az összegző ciklust meg lehetne spórolni, de kicsit nehezebben
lenne követhető a forráskód.