1 Listák és rekurzió
Egy láncolt lista egy első elemből, és egy abból kiinduló láncolt listából áll. Ezen gondolat alapján írjunk függvényeket, amelyek:
- Kiírják az egyszeresen láncolt listában tárolt adatokat.
- Kiírják az egyszeresen láncolt lista adatait visszafelé.
- Felszabadítják a listát.
Mikor érdemes használni a rekurziót a listáknál? Mikor nem?
Megoldás
#include <stdlib.h> #include <stdio.h> typedef struct Lista { int adat; struct Lista *kov; } Lista; /* csak hogy legyen teszt adat */ Lista *lista_letrehoz(void) { Lista *l = NULL; int j=20, i=-j; for (i=-j; i++ != 0; j*=17) { Lista* u=(Lista*)malloc(sizeof(Lista)); u->kov=l; u->adat=j%=23; l=u; } return l; } /* kiírja a listát előrefelé. a lista a legelső elemből, * és a többi részéből áll (ami úgyszintén egy lista). * üres listánál nem kell csinálni semmit. */ void lista_kiir_elore(Lista *l) { if (l==NULL) return; printf("%d ", l->adat); lista_kiir_elore(l->kov); } /* kiírja a listát visszafelé. az előző függvényhez * képest csak megcseréltük a két hátsó sort: előbb a * lista "többi részét" írjuk ki, csak utána az első elemet. * mivel ezt nem csak az első elemnél, hanem az összesnél így * csináljuk, ezért nem csak az első elem kerül a végére, * hanem az egész megfordul. */ void lista_kiir_hatra(Lista *l) { if (l==NULL) return; lista_kiir_hatra(l->kov); printf("%d ", l->adat); } /* felszabadítjuk a listát. előbb fel kell szabadítani * az ebből az elemből kiinduló listát, utána pedig ezt * az elemet. az ötlet hasonló, mint a hátrafelé kiírásnál. * */ void lista_felszabadit(Lista *l) { if (l==NULL) return; lista_felszabadit(l->kov); free(l); } int main() { Lista *l = lista_letrehoz(); lista_kiir_elore(l); printf("\n"); lista_kiir_hatra(l); printf("\n"); lista_felszabadit(l); return 0; }
A rekurziót csak a hátrafelé kiírásnál érdemes alkalmazni. Az előrefelé történő kiírás és a felszabadítás könnyedén megoldható ciklussal is, mivel a pointerek amúgy is előrefelé mutatnak a listában. Ilyenkor nem érdemes egy O(n) memóriaigényű megoldást választani az O(1) memóriaigényű helyett!
2 A teljes szöveg visszafelé
Az alábbi program beolvas egy szöveget fájl vége jelig, majd kiírja azt visszafelé. Elmélkedjünk rajta, miért!
#include <stdio.h> void forditva_kiir(void) { int c = getchar(); if (c!=EOF) { forditva_kiir(); putchar(c); } } int main() { forditva_kiir(); return 0; }
Írjunk meg ezt a programot úgy, hogy nem használunk rekurziót, hanem magunk építünk vermet!
Megoldás
A verem tároló lényege, hogy ha beleteszünk egy adatot, az mindig a tetejére kerül, és ha kiveszünk belőle egyet, akkor azt mindig a tetejéről vesszük ki. Amelyik legutoljára bekerült, az jön ki elsőnek (LIFO, last in, first out). Vermet egyszeresen láncolt listából nagyon könnyű csinálni. Ha mindig a lista elejére szúrjuk az új elemet, és mindig a lista elejéről vesszük el, ha ki kell venni egyet, akkor pont vermet kapunk. (Azért érdemes a lista elejét, és nem a végét kezelni ilyen módon, mivel a lista eleje mutató így épp arra mutat, amire kell, nem kell végiglépkedni a lista végéig.)
#include <stdio.h> #include <stdlib.h> typedef struct Verem { char karakter; struct Verem *alatta; } Verem; void verembe(Verem **v, char c) { Verem *uj=(Verem *) malloc(sizeof(Verem)); uj->karakter=c; uj->alatta=*v; /* alatta az eddigi legfelso lesz */ *v=uj; /* mostantol ez van a tetejen */ } char verembol(Verem **v) { Verem *elso=*v; /* az elsot fogjuk eldobni */ char adat=elso->karakter; /* kimentjuk a szamot belole */ *v=elso->alatta; /* a masodik lesz az elso */ free(elso); return adat; } int verem_ures(Verem **v) { return *v == NULL; } int main() { Verem *v=NULL; int c; printf("Kedves draga jo felhasznalo! Irj egy tetszolegesen hosszu szoveget, EOF-ig!\n"); while ((c=getchar()) != EOF) verembe(&v, c); printf("Kedves draga jo felhasznalo! A szoveged visszafele:\n"); while (!verem_ures(&v)) putchar(verembol(&v)); return 0; }
3 RPN számológép
RPN = reverse polish notation, inverz lengyel jelölésmód. Ez azt jelenti,
hogy a matematikai operátorokat és operandusaikat nem a szokásos módon, középen
a műveleti jellel írjuk: 5+7
, hanem a műveleti jelet a két
operandus után téve: 5 7 +
. Ennek
nagy előnye, hogy precedenciaszabályok és zárójelezés nélkül képes pontosan
meghatározni a műveleti sorrendet, mert mindig lehet tudni, hogy az operátor
előtt álló két szám (vagy épp két művelet eredménye) az adott operátor
operandusa. Pl.
(5 + 7) * 9 = 5 7 + 9 * \_\_/ \_\_/
Az összeadás az 5-re és a 7-re, a szorzás az összegre és a 9-re vonatkozik
5 + 7 * 9 = 5 7 9 * + \_\_/ \_____\_/
A szorzás a 7-re és a 9-re vonatkozik, míg az összeadás az előtte lévő szorzatra és az 5-re
Feladat: írjunk számológép programot, amelynek szóközzel elválasztva lehet megadni a számokat és műveleteket; és kiírja a képernyőre az eredményt.
Megoldás
A beérkező számokat el kell tárolnunk. Egészen addig, amíg egy
művelet nem érkezik; amikor is pedig a legutóbbi két számon el kell
végeznünk a műveletet. „Mindig a legutóbbi kettő”: ezt egy veremmel
lehet megvalósítani. Ha pl. látunk egy összeadás jelet, akkor a
verem tetejéről levett két számot összeadjuk, és az eredményt
visszatesszük a verem tetejére (hiszen az később egy további művelet
egyik operandusa lehet). Amikor a kifejezésnek vége van, elvileg egy
szám kell legyen a veremben, és az maga az eredmény. Pl. az
5 7 9 * +
kifejezés feldolgozása:
bejövő verem 5 → [5] 7 → [7 5] 9 → [9 7 5] * → [63 5] + → [68]
A lenti program nem tartalmaz hibakezelést, csak helyes bemenetek esetén működik jól!
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct Verem { double szam; struct Verem *alatta; } Verem; void verembe(Verem **v, double adat) { Verem *uj=(Verem *) malloc(sizeof(Verem)); uj->szam=adat; uj->alatta=*v; /* alatta az eddigi legfelso lesz */ *v=uj; /* mostantol ez van a tetejen */ } double verembol(Verem **v) { Verem *elso=*v; /* az elsot fogjuk eldobni */ double adat=elso->szam; /* kimentjuk a szamot belole */ *v=elso->alatta; /* a masodik lesz az elso */ free(elso); /* az elso meg nem kell */ return adat; } int main() { Verem *v=NULL; char szo[50]; double d; printf("Ird be a kifejezest, aztan adj egy fajl vege jelet!\n"); printf("Pl.: 3 4 +\n\n"); /* amig sikerul szot beolvasni */ while (scanf("%s", szo) == 1) { /* nezzuk, hatha kulcsszo */ if (strcmp(szo, "+")==0) { double tag1=verembol(&v); double tag2=verembol(&v); verembe(&v, tag1+tag2); } else if (strcmp(szo, "*")==0) { double tenyezo1=verembol(&v); double tenyezo2=verembol(&v); verembe(&v, tenyezo1*tenyezo2); } else if (strcmp(szo, "-")==0) { double kivonando=verembol(&v); double kisebbitendo=verembol(&v); verembe(&v, kisebbitendo-kivonando); } else if (strcmp(szo, "/")==0) { double oszto=verembol(&v); double osztando=verembol(&v); verembe(&v, osztando/oszto); } else { /* ha nem kulcsszo, probaljuk szamkent. */ if (sscanf(szo, "%lf", &d)==1) { /* ha szam, berakjuk a verembe */ verembe(&v, d); } else printf("Nem ertem: %s\n", szo); } } printf("%g\n", verembol(&v)); return 0; }
Az összeadás és a szorzás műveleténél elég lenne egy ilyen sort írnunk:
verembe(&v, verembol(&v)+verembol(&v));
Ez azonban nincs így a kivonásnál és az osztásnál. Azoknál ehhez hasonlót írni súlyos kódolási hiba lenne, mivel nem lehetne tudni, hogy előbb a kisebbítendőt (osztandót) vagy a kivonandót (osztót) venné ki a veremből a kódrészlet – hiszen a szabvány nem határozza meg, hogy melyik operandust kell előbb kiértékelni. A fenti módszerrel, külön utasításként leírva a veremből kivételeket azonban kényszeríthető a megfelelő kiértékelési sorrend. (Az összeadásnál és a szorzásnál, lévén kommutatív műveletek, teljesen mindegy, hogy melyik szám kerül ki a veremből először. Hiszen a+b=b+a, és a*b=b*a.)
4 Fésűs lista
Írjunk programot, amelyik tankörszámokat, NEPTUN kódokat, neveket és átlagokat olvas be egy fájlból, és eltárolja az így kapott névsorokat! A fájl egy sora így néz ki:
13 MZPERX 5.1 Köb Üki
Ez azt jelenti, hogy Köb Üki a 13-as tankörbe jár, MZPERX a kódja és 5.1-es az ösztöndíjátlaga.
Megoldás
A megoldásban egy fésűs listát kell alkalmazni: kívül a tankörök listája, és mindegyik tankör tartalmaz egy névsort (hallgatók listája).
Minden beolvasott hallgatónál előbb meg kell keresni a tankört (vagy létrehozni, ha még nincs), és utána abba a listába betenni. Mivel a feladat nem határozza meg a sorrendet, egyszerűen az elejére szúrjuk be az adatokat.
#include <stdio.h> #include <stdlib.h> /* egy hallgato adatai - lancolt listaban */ typedef struct Hallgato { char neptun[6+1]; /* a neptun kodja, max 6 */ char nev[30+1]; /* a neve, max 30 */ double atlag; /* az atlaga */ struct Hallgato *kov; } Hallgato; /* egy tankor adatai: egy nevsor */ typedef struct Tankor { int szam; /* sorszama */ Hallgato *nevsor; /* nevek */ struct Tankor *kov; } Tankor; /* beolvassa a megadott nevu fajlbol az adatokat. * visszater egy új Tankor listaval. */ Tankor *tklista_beolvas(char const *fajlnev) { Tankor *tklista = NULL; /* ez lesz a kimenet */ Hallgato temp; /* a fajlbol olvasashoz */ FILE *fp; int tk; fp = fopen(fajlnev, "rt"); if (fp == NULL) return NULL; /* %[^\n] - sztring enterig */ while (fscanf(fp, "%d %s %lf %[^\n]", &tk, temp.neptun, &temp.atlag, temp.nev)==4) { Tankor *iter; /* tankorlista bejarashoz */ Hallgato *ujh; /* uj hallgato */ for (iter=tklista; iter!=NULL && iter->szam!=tk; iter=iter->kov) ; /* ures, csak keres */ if (iter==NULL) { /* ha null, nem lett meg, uj kell */ iter = (Tankor*) malloc(sizeof(Tankor)); iter->szam = tk; iter->kov = tklista; iter->nevsor = NULL; tklista = iter; /* elejere */ } ujh = (Hallgato*) malloc(sizeof(Hallgato)); *ujh = temp; /* minden adatot bemasolunk! */ ujh->kov = iter->nevsor; iter->nevsor = ujh; /* elejere */ } return tklista; } /* kilistazza a tankort es a nevsorokat. */ void tklista_listaz(Tankor *tklista) { Tankor *tkiter; /* tankorok iteratora */ Hallgato *hgiter; /* hallgatok iteratora */ for (tkiter = tklista; tkiter!=NULL; tkiter=tkiter->kov) { printf("%2d. tankor -- \n", tkiter->szam); for (hgiter = tkiter->nevsor; hgiter!=NULL; hgiter=hgiter->kov) printf("%6s %-30s %g\n", hgiter->neptun, hgiter->nev, hgiter->atlag); } } void tklista_felszabadit(Tankor *tklista) { while (tklista != NULL) { Tankor *kov = tklista->kov; /* kimentjuk */ Hallgato *nevsor = tklista->nevsor; while (nevsor != NULL) { Hallgato *kov = nevsor->kov; /* kimentjuk */ free(nevsor); nevsor = kov; } free(tklista); tklista = kov; } } int main() { Tankor *tklista; /* az osszes adat listaja */ tklista = tklista_beolvas("nevek.txt"); tklista_listaz(tklista); tklista_felszabadit(tklista); return 0; }