Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
8. előadás Ismétlés Dinamikus adatszerkezetek: listák (egyszeresen vagy többszörösen láncolt), fák. Kétfelé ágazó fa: bináris fa Dinamikus adatszerkezetek - önhivatkozó adatstruktúrák: adatok és reájuk mutatni képes pointerek egy egységbe foglalva. Az ezek definiálására alkalmas adatszerkezet a C-ben a struktúra (lásd: Pascal RECORD), a struct A strukturák definiálásanak általános formai szabályait az alábbi ábra szerinti szintaktikai diagram adja meg:
Például számokból álló, egyszeresen láncolt lista felépítéséhez az alábbi struktúra definíciót használhatjuk: struct szl { int szam; struct szl *kovetkezo; /* típuscímke segítségével hivatkozunk a struktúránkra */ } elsosz, *elso, *futo;
Ennek segítségével oldjuk meg a következő feladatot: Előre ismeretlen mennyiségű nem zérus értékű egész számot kell a memóriában eltárolnunk; a számokat folyamatosan olvassuk addig, amíg egy nulla nem érkezik. A megoldáshoz vezető gondolatmenetünk a következő: statikus tömbbel nem oldható meg a feladat, mert elképzelhető, hogy a tömb definiált méreténél több szám érkezhet, a többletet biztosan nem tudjuk tárolni. Szóba jöhet a dinamikus allokálású tömb is, de ehhez is előre ismerni kellene a tárolandó számok számát, hogy végre tudjuk hajtani a memóriaallokációt. Alapötlet: minden egyes adattal együtt hozzunk létre egy mutatót is, amin keresztül elérhetjük az esetleg következő, újabb adatot - tehát dinamikusan hozzunk létre azonos számú adattároló helyet is és pointert is és az egészet szervezzük egy láncolt listába. Az utolsó listaelem pointere szokás szerint NULL értékű legyen. A kitűzött feladat megoldásához a fenti szl struktúrát (számok listája) fogjuk alkalmazni, a fent definiált változókkal. A feladatot az alábbi kódrészletteol oldjuk meg: struct szl elsosz, *eleje, *futo, *p; int n; elsosz.szam = 0; /* elsosz struktúra szam adatmezőjét választjuk ki a . operátorral */ elsosz.kovetkezo = NULL /* nincs következő elem, ezért kovetkezo mező értéke NULL */ eleje = futo = &elsosz; /* elsosz statikus struktúra az első számot tárolja, ez lesz a lista eleje */ /* do - while ciklussal 0-ig olvasunk, láncba fűzzük a beolvasott számokat */ do { scanf("%d",&n); if (n) { p = (struct szl*)(malloc(sizeof(struct szl))); if (p != NULL) /* sikeres memóriafoglalás a lánc egy új elemének */ { (*p).kovetkezo = NULL;
1/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
(*p).szam = 0; (*futo).kovetkezo = p; (*futo).szam = n; futo = p; /* futo-t átállítjuk az új listaelemre */ } } } while(n);
Dinamikus adatszerkezet kezelésénél gyakori fordulat, hogy az adott listaelemet vagy fa csomópontot reprezentáló, dinamikusan allokált struktúrára csak egy pointer mutat rá, a struktúra mezőit ezen keresztül érjük el. Ehhez inidrektáljuk a pointert, majd az eredményül kapott struktúra típusú kifejezésre alkalmazva a mezőkiválasztó operátort férünk hozzá a kívánt mezőhoz. Ezt lerövidítendő, használhatjuk a -> struktúra-pointer mezőkiválasztó operátort: a (*struktúramutató).struktúramező kifejezéssl teljesen ekvivalens a struktúramutató->struktúramező kifejezés. Ezt felhasználva tekintsük a listában tárolt számok kiírását végző kódrészletet: futo = eleje; while (futo != NULL) { printf("%d ",futo->szam); futo = futo->kovetkezo; }
Fésűs adatszerkezet Az eddigieknél összetetteb adatszerkezeteket is létrehozhatunk dinamikus adatszerkezetek formájában. A (lineáris) láncolt lista tulajdonképpen az egydimenziós tömbökre emlékeztet azzal a különbséggel, hogy a listaelemekhez csak szigorúan sorrendben (szekvenciálisan), a lánc mentén végighaladva fértünk hozzá. Akkor alakítottunk ki láncolt listát, ha ismeretlen számú adat gyors memórában tárolására volt szükségünk. Ez kiterjeszthető "két dimenzóra" is. Ekkor egy újabb belső szerkezetű láncot hozunk létre, amely listaszerűen pusztán az eddigi listáink kezdőcímeit tartalmazza. Ha ábrázoljuk egy ilyen szervezésű adatszerkezet sémáját, akkor egy fésűre emlékeztető rajzot kapunk, ezért az ilyen adatszerkezeteket fésűs adatszerkezeteknek nevezzük. Ezt szemlélteti az alábbi ábra:
Ehhez egy típust (struktúrát) kell definiálnunk a fésű fogai számára, ahol a tényleges adatokat tároljuk és egy másik típust (struktúrát) kell definiálnunk a fésű "nyele" számára. Természetesen a "nyél" lista elemeiben is tárolhatunk adatokat, tipikusan a fogakat alkotó listák egyes globális jellemzőt. Példa: a fogakban egy egyetemi tankörbe beosztott hallgatók adatait tároljuk (egy lista - egy tankör alapon), míg a fésű nyele egy évfolyamot
2/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
reprezentálhat, ahol az évfolyamot tankörökre osztottuk. Tankörönként tárolhatjuk pl. a tankörszintű tanulmányi átlagot, stb.
Típusdefiniálás A C nyelv használata során sok egyszerűsítésre ad módot az, hogy pl. struktúrák esetében nem kell mindig a struct típuscímke fordulattal élnünk, ha valahogy elérjük azt, hogy egy struktúrát (vagy akármilyen más adattípust) a fordító pontosan olyan módon kezeljen, mint az előre definiált adattípusokat. Ehhez az kell, hogy egy közöljük a fordítóval azt, hogy egy adatszerkezet (mondjuk egy tömb vagy egy struct) leírása egy új adattípus definíciójaként tekintendő. Ezt a typedef kulcsszó segítségével tehetjük meg, az alábbi szintaktikai séma szerint:
Rövid példaként egy olyan típus definícióját adjuk meg, amivel egy bináris fát lehet reprezentálni: typedef struct bf { char ch; struct bf *ti, *ta; } btree, *pbtree;
Ebben a példában btree egy struktúra típus, pbtree pedig egy struktúrára mutatni képes pointer típus azonosítója. Hogy lesz ebből bináris fa? Úgy, hogy úgy pointerezzük egymáshoz a memóriában dinamikusan létrehozott adatokat, hogy azok egy fát alkossanak. A fenti struktúra típus-definícióban szereplő két mutatómezővel akár egy kétirányú láncolt listát is felépíthetnénk. Az, hogy milyen típus azonosítókat és milyen struktúra mező azonosítókat használunk, tehát semmit sem jelent - csak a kód emberi olvasó általi érthetőségét segíti elő. Az alábbi ábra azt szemlélteti, hogy egy két pointermezőt tartalmaző önhivatkozó struktúrával akár ilyen, akár olyan dinamikus adatszerkezetet is felépíthetünk:
Felmerülhet a kérdés, hogy mire használhatjuk a fákat. Az előző előadáson pl. rendezésre/keresésre használtuk - ezek voltak a rendező, ill. kereső fák. De használhatjuk pl. dekódolásra is. Ehhez tekintsük az alábbi fát:
3/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
Ezen az ábrán - összhangban a btree típusdefiníciójával - az egyik irányba mutató pointert ti-vel, a másikba mutatót ta-val jelöltük. Nézzük, hogy juthatunk el a fa gyökeréből pl. az S betűhöz: ti-ti-ti utat kell bejárjuk. Az O-hoz pedig a ta-ta-ta utat. Ha megint az S-et szeretnénk elérni, az út a gyökérből megint csak ti-ti-ti. Talán már mindenki fel is ismerte az SOS vészjel Morsekódsorozatát. Ez a fa tehát egy Morse-dekódoló fa (ugyan a számjegyek kódjai hiányoznak belőle). Ezt felhasználva können írható egy Morse-dekódoló program. Ennek lényegi részét láthatjuk alább: pbtree morse_root, futo; char c, betu; morse_root = ..... /* valahogy a fenti tartalommal kitöltve létrehozzuk a dekódoló fát */ ... /* . és - karakterek kódolják a betüket, space határolja az egyes jelsorozatokat */ futo = morse_root; while (' ' != (c = getchar())) { switch(c) { case '.': futo = futo->ti; break; case '-': futo = futo->ta; break; } } betu = futo->ch; futo = morse_root; /* kezdjük előlről a következő kóddal */ ...
Az alábbi ábra egy bináris keresőfát szemléltet. Ebben pl. egy telefonkönyv adatait tároljuk és a fa az előfizetők családi neve mint kulcs szerint rendezett.
4/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
Dinamikus adatszerkezetek kezelése Láncolt listák jellegzetes műveleteit - új elem beszúrása, elem törlése - szemléltetik az alábbi ábrák:
5/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
6/9
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
Elem törlésénél fontos a free függvény meghívása - ez jelenti a tényleges, fizikai törlést és az allokált memóriaterület visszaadását az operációs rendszernek. Ne feledjük ezután a kérdéses pointer értékét NULL-lá tenni, jelezve, hogy az most már nem mutat sehová. Fenti példánk szerint tehát: torlendo = NULL; Példa a törlésre: typedef struct l { valami adat; struct l *kovetkezo; } lista_t, *lista_poi; ..... lista_poi torles(lista_poi els, torl) { if (torl == els) elso = torl->kovetkezo; else els->kovetkezo = torl->kovetkezo; free(torl); return els; } ..... elso = torles(elso, torlendo); torlendo = NULL;
Figyelem: torlendo-t a torles függvényen kívül tettük NULL-lá - a függvényhívás mechanizmusának tárgyalásakor ennek az okát tisztázzuk.
7/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
Fák kezelése: Keresés:
1. Indulás a gyökérből 2. Ha a kulcs egyezik, megvan az elem 3. Ha kisebb, továbblépés a baloldali részfába, ha lehet; ha nagyobb, továbblépés jobbra, ha lehet.
4. Ha már nem lehet a fában a rendezettség szerinti irányba továbblépni, az elem nincs benne a fában. Ha szükséges, az utolsóként bejárt csomópont megfelelő ágára felfűzhető - ezzel a keresett adatot fel is vettük a fába. Új elem beiktatása:
1. Üres a fa? Ha igen, új elemként felvesszük; ez lesz a gyökér. 2. Ha nem üres a fa, az elem helyét meg kell keresni - lásd fent. 3. Ha megtaláltuk az elemet a fában, nem kell tenni semmit, ha NULL-ba ütköztünk, akkor arra az ágra felfűzzük.Így a fa automatikusan rendezetten épül. Törlés - nem triviális: Utód nélküli levél: nincs gond, a foglalt memóriát felszabadítjuk, a reá mutató pointert-t NULL-lá tesszük. 1 utóda van: a törlendő elemre mutató pointer értéke felülírandó a törlendő elem egyetlen utódára mutató értékkel, majd a foglalt memóriát felszabadítjuk. A törlés logikai részét szemlélteti az alábbi ábra:
2 utóda van: A törlés után a rendezettségnek fenn kell maradnia. A törölt elem helyére kerülő elemnek a törölt elem minden baloldali utódánál nagyobb kulcsúnak, minden jobboldali utódánál kisebb kulcsúnak kell lennie.Lépések:
1. Baloldali részfa legjobboldalibb elemének megkeresése. 2. Ezt a törlendő elembe mozgatjuk át úgy, hogy a jobboldali mutatója a törlendő elem jobboldali eredeti részfájára mutasson, baloldali mutatója pedig a törlendő elem megmaradó baloldali részfájára mutasson.
8/9
2012-10-17 22:24
Programozás alapjai 1: 8. előadáson elhangzottak
http://www.eet.bme.hu/~pohl/vieea100/ea9.html
Fa teljes bejárása: typedef struct bf { valami adat; struct bf *bal, *jobb; } bifa, *pbifa; void fabejaro(pbifa aktualis_csp) { if (aktualis_csp->bal != NULL) fabejaro(aktualis_csp->bal); if (aktualis_csp->jobb != NULL) fabejaro(aktualis_csp->jobb); aktualis_csp->adat feldolgozása }
Megjegyzés: az aktuális csomópontban tárolt adat feldolgozása akár a bal/jobb részfák bejárása előtt, akár a részfák bejárása után is megtörténhet, attol függően, hogy éppen mi az algoritmikus igény.
9/9
2012-10-17 22:24