11. gyakorlat: láncolt listák
Láncolt listák használata. Az óra célja az, hogy mindenki gyakorlatot szerezzen a listák használatában – különösen abban, hogyan kell a pointereket az egyes beszúrás, törlés stb. műveleteknél állítani. Ehhez minden esetben javasolt rajz készítése!
1 Milyen adatszerkezet?
Gondoljuk végig, az alábbi problémákhoz milyen adatszerkezetet érdemes használni.
Megoldások
- Egy program számokat olvas be, amelyek közül ki kell írni az átlagnál kisebbeket.
Nem tudjuk, hány szám lesz.
Ehhez a számokat el kell tárolnunk, mivel az összes szám ismeretében tudjuk meg az átlagot, és akkor kezdhetjük csak el a kiírást. Láncolt listát kell alkalmazni, ha nem szeretnénk minden számnál újra és újra nyújtani, lemásolni a tömböt.
- Egy program számokat olvas be, és a beolvasottak közül csak a prímszámokat
írja ki.
Ehhez nem kell eltárolni a számokat, így a kérdés értelmetlen.
- Számokat olvasunk be, és visszafelé sorrendben ki kell írni az átlagnál kisebbeket.
Láncolt lista. Két lehetőségünk van: duplán láncolt listát használunk, mert visszafelé is szeretnénk haladni rajta; vagy egyszeresen láncoltat használunk veremként, hiszen annak eleve adott ez a tulajdonsága, hogy fordított sorrendben látszanak a betett elemek.
- Számokat olvasunk be, maximum százezret. Ki kell írni az átlagnál kisebbeket.
Ehhez ugyan jó lenne a tömb, de ha arra számítunk, hogy jóval kevesebb szám lesz csak, akkor pazarlás a 100000 elem, és így jobb a láncolt lista. (Különösen igaz ez akkor, ha nem csak számok tárolandók, hanem valami nagyobb adatok.)
- „Lemmings” játékot írunk. Egy bejáraton besétálnak a lemingek, akik egy adott időt
töltenek a pályán, mindenféle dolgokat csinálva. A sok leming közül némelyek kimennek
a kijáraton, mások pedig feláldozzák magukat a többiekért.
Láncolt lista, mivel folyton változik a számuk. Érdemes duplán láncoltat csinálni, ugyanis gyakori művelet a törlés is, és az egyszerűbb duplán láncolt listán.
- Be kell olvasnunk egy tetszőlegesen hosszú sort a bemenetről (karakterek enterig).
Dinamikus tömb, amelyet időnként átméretezünk. A listás megoldás hatalmas pazarlás lenne (minden karakter mellé egy pointer!), és a keletkező adatszerkezetet nem tudnánk sztringként sem használni sehol.
- A programunk egy közlekedési társaság buszjáratait tárolja. Minden járatnak van egy
száma, illetve van két végállomása, és közte tetszőlegesen sok megálló.
Fésűs lista, azaz listák listája. A „külső” listában a buszjáratok vannak, amelyekhez járatszámok, és darabonként egy megállólista („belső” listák) tartoznak.
- Amőba játékot írunk. A 13×13-as pályára a játékosok felváltva
oésxjeleket tesznek, amelyek száma így változik. Nem tudjuk előre, mennyi lesz, hiszen lehet hogy hamar vége van a játéknak, lehet mindkét játékos ügyes, és szinte megtelik a pálya.Átverés, ez 13×13-as tömb. Ha a letettoésxbábukat listában tárolnánk (mindegyik mellé felírva, hogy mely (x;y) koordinátára kerültek), akkor borzasztóan elbonyolódna egy elem „indexelése”, és így pl. a játékállás ellenőrzése. - Beolvasunk számokat a billentyűzetről, és meg kell mondanunk, hogy melyik szám hányszor szerepelt.
Ez egy lista, legalábbis jelenlegi ismereteink szerint. Ha egy új szám jön, és még nincs a listában, akkor felvesszük; ha már benne van, akkor csak növeljük a hozzá tartozó számlálót. Mivel nem tudjuk, hány szám lesz, ezért a tömb itt nem jó.
- Beolvasunk karaktereket a billentyűzetről, és meg kell mondanunk, hogy melyik kisbetű (abc…z) hányszor szerepelt.
Ez viszont tömb. Mivel a számolandó elemek száma előre ismert és kicsi (mindössze
'z'-'a'+1=26darab), nem érdemes a listával bajlódni.
2 Számok listában
Írjunk programot, amely a billentyűzetről számokat olvas be, egészen 0-ig. Írjuk ki ezután a beolvasott számok közül azokat, amelyek az átlagnál kisebbek! A sorrend nem számít.
Megoldás
Tudjuk, hogy a számokat el kell tárolni, mivel csak a legutolsó szám után derül ki az, hogy melyeket kell kiírni. A „tetszőlegesen sok” miatt listát kell alkalmaznunk. Kérdés, hogy egyszeresen vagy kétszeresen láncolt kell legyen. Mivel a kiírás sorrendje nem kötött, válasszunk egy egyszeresen láncolt listát, annak is a legegyszerűbb beszúró függvényét: a lista elejére beszúrást!
A fentiek alapján a lista:
typedef struct Szam {
int szam;
struct Szam *kov;
} Szam;
A főprogram szinte nem különbözik attól, mintha tömbbel csinálnánk:
int main() {
Szam *lista=NULL, *iter;
int beolv;
double atlag;
scanf("%d", &beolv);
while (beolv!=0) {
lista=elejere(lista, beolv);
scanf("%d", &beolv);
}
atlag=listaatlag(lista);
for (iter=lista; iter!=NULL; iter=iter->kov)
if (iter->szam<atlag)
printf("%d ", iter->szam);
felszabadit(lista);
return 0;
}
Ha nem használnánk külön változót a beolvasott számnak, hanem egyenesen a listába szeretnénk beolvasni, akkor itt most nagy bajban lennénk. Ugyanis a beolvasás előtt már létre kellene hozni egy listaelemet, amit aztán ki kellene törölni, ha nullát olvastunk be.
A listába beszúrás: mindig az elejére tesszük az új számot (ezért amúgy fordított sorrendben lesznek benne):
/* uj elemet szur be a megadott lista elejere.
* visszater az uj, megvaltozott lista eleje
* mutatoval. a hasznalata:
* l = beszur(l, 5);
*/
Szam *elejere(Szam *lista, int szam) {
Szam *uj=(Szam*) malloc(sizeof(Szam)); /* 1 */
uj->szam=szam;
uj->kov=lista; /* 2 */
return uj; /* 3 */
}
A lista elemeinek átlaga végülis ugyanolyan, mintha tömbünk lenne. Csak itt meg is kell számolnunk az elemeket:
/* a megadott listan levo szamok atlagat adja */
double listaatlag(Szam *lista) {
Szam *iter;
double szum=0;
int db=0;
for (iter=lista; iter!=NULL; iter=iter->kov) {
szum+=iter->szam;
db++;
}
return szum/db; /* double/int oke */
}
A teljes program letölthető ide kattitva: szamoklistaban.c.
3 Madárnyelv számokkal és listával
Adott egy egész számokat tartalmazó lista. Írjunk be minden páros szám után egy nullát és a számot magát. Vagyis legyen a 2,3,4,5 listából a 2,0,2,3,4,0,4,5 lista.
Megoldás
Ez könnyű, hiszen beszúrni egy adott elem után könnyen tudunk. Végigmegyünk a listán, ha az páros, akkor beszúrunk utána egy nullát és saját magát. Vigyázat, két buktató is van! Ha ilyen sorrendben tennénk, akkor fordított lenne az eredmény – vagyis előbb saját magát, és utána a nullát kell beszúrni. (Ehhez inkább külön változókat használ a lenti kód.) Figyelni kell arra is, hogy a beszúrás után az iterátort léptessük, nehogy a következő körben megtaláljuk a nullát vagy a beszúrt számot (hiszen ezek is párosak). Kettővel léptetjük előre, ezáltal olyan állapotot előidézve, mintha a ciklusváltozó a beszúrt elemre mutatna.
/* A listában minden páros szám után beszúr egy nullát,
* és még egyszer magát a számot.
*/
void lista_202(Szam *lista) {
Szam *iter;
for (iter=lista; iter!=NULL; iter=iter->kov)
if (iter->szam%2==0) {
Szam *ujszam, *ujnulla;
ujszam=(Szam*) malloc(sizeof(Szam));
ujszam->szam=iter->szam;
ujszam->kov=iter->kov;
ujnulla=(Szam*) malloc(sizeof(Szam));
ujnulla->szam=0;
ujnulla->kov=ujszam;
iter->kov=ujnulla;
/* 2-t ugrunk, es utana meg jon a 3. ugras */
iter=iter->kov->kov;
}
}
Az 1-2. lépést, vagyis a szám és a pointer másolását összevonhatnánk egy lépésbe:
*ujszam = *iter;
Struktúra értékadással, ugyanis mind a számot, mind a pointert, vagyis az egész struktúrát másoljuk ott.
A teljes program letölthető innen: lista202.c.
4 Adott tulajdonságú elemek törlése
Adott egy szavakat tartalmazó lista. Töröljük ki belőle a négybetűseket!
Megoldás
Itt figyelnünk kell arra, hogy előfordulhat, a lista első elemét kell törölni. Ilyenkor pedig a lista eleje mutató is megváltozhat. A következő további esetek lehetségesek:
- Miután töröltük az első elemet, lehet hogy a második elem is négybetűs. Ekkor azt is törölni kell. Viszont az az első helyre került – vagyis az első elem vizsgálata nem csak egy feltétel, hanem egy ciklus. Amíg a lista elején négybetűs van, addig töröljük azt.
- Mindeközben elfogyhat a lista – hiszen lehet az is, hogy csak négybetűs szavakat tartalmazott. Sőt az is, hogy eleve üres volt.
Ez a ciklus így néz ki:
while (lista!=NULL && strlen(lista->szo)==4) {
Szo *torlendo=lista;
lista=torlendo->kov;
free(torlendo);
}
Itt kihasználjuk az && operátor rövidzár tulajdonságát. Ha a pointer
NULL, akkor már a lista->szo kifejezés ki sem értékelődik, hiszen az
egész kifejezés értéke csak HAMIS lehet. Ez fontos, hiszen ez biztosítja azt, hogy ne
dereferáljuk a NULL pointert! Emiatt a kifejezés két oldala nem cserélhető meg!
Ezután két eset lehetséges:
- A lista üressé vált, és nincs további teendőnk.
- A lista nem lett üres, amely esetben azonban az elején biztosan nem négybetűs szó van. Az már meg fog maradni, így a lista elejét mutató pointer nem módosul tovább.
Mivel minden törlendő elemet megelőző pointerét kell módosítani a törlésnél, így vagy
azt keressük meg, hogy mely elemeknél kell törölni a következőt, vagy lemaradó pointert használunk.
Alább a lemaradó pointeres megoldás látható.
Figyelni kell, ha egy adott elemet törlünk, akkor a lemaradó pointer nem mozdul,
hanem csak az iter! Amúgy pedig együtt mozognak. A programrész:
if (lista!=NULL) {
Szo *lemarado=lista;
Szo *iter=lista->kov;
while (iter!=NULL) {
Szo *kovetkezo=iter->kov;
if (strlen(iter->szo)==4) {
lemarado->kov=kovetkezo; /* 1 */
free(iter); /* 2 */
} else
lemarado=iter;
iter=kovetkezo; /* 3 */
}
}
A teljes program letölthető innen: torles11.c.
5 Megfordít
Fordítsunk meg egy listát az elemei átláncolása által! Írjunk egy programot, amely beolvas számokat 0-ig, és kiírja a keletkező listát eredeti sorrendjében (és ezáltal a számokat eredeti sorrendjükben), illetve kiírja a megfordítottat is.
Megoldás
Ez egyszerűbb, mint gondolnánk. Ha mindig a lista elejéről elveszünk egy elemet, és a megfordított lista elejére betesszük azt, akkor a lista éppen megfordul! Az eredeti lista szép lassan elfogy, és mikor ez megtörténik, akkor a keletkező lista kész.
/* Megfordit egy parameterkent kapott listat, es visszater
* a megforditott valtozattal.
* Vigyazat, az eredeti lista elveszik! A fuggveny nem uj
* listat epit, hanem az eredeti lista elemeinek felhasznalasaval
* epiti az uj listat. Igy a kapott pointert erdemes az eredeti
* lista eleje pointerbe visszairni:
* szamok = lista_megfordit(szamok);
*/
Szam *lista_megfordit(Szam *lista) {
Szam *eredeti=lista;
Szam *megforditott=NULL;
while (eredeti!=NULL) {
Szam *atrakott=eredeti, *kovetkezo=eredeti->kov;
atrakott->kov=megforditott; /* uj elejere be */ /* 1 */
megforditott=atrakott; /* 2 */
eredeti=kovetkezo; /* regibol kihagy */ /* 3 */
}
return megforditott;
}
A teljes program letölthető innen: listamegfordit.c.
