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áadppercet aziidő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): kivonppercet aziidő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.
