Dinamikus adatszerkezetek II. – Bináris fák
Tartalom
- Ismétlés – láncolt lista
- A bináris fák és algoritmusaik
- A bináris keresőfa: rendezett tárolás
- A fa reprezentációja C nyelven
- Keresés a fában: O(log n)
- A fa bejárása I. – rekurzió
- A fa bejárása II. – algoritmus
- A fa bejárása III. – inorder bejárás fordítva
- A postorder bejárás – fa felszabadítása
- A preorder bejárás I.
- A preorder bejárás II.
- Műveletek fákon – általában
- Műveletek – fák elemszáma
- Műveletek – levelek száma
- Műveletek – elemek adott szinten
- Keresőfa építése
- Példa – szavak statisztikája
- Fák alkalmazásai – hierarchia
- Fák alkalmazásai – további alakok
- Fák alkalmazásai – hatékonyság
- A tanult adatszerkezetek
- Kettős indirekcó
- Indirekció – cím szerinti paraméterátadás
- Kettős indirekció: lista építése
- Kettős indirekció: lista építése – használat
- Kettős indirekció – lista végéhez fűzés
- Kettős indirekció – lista végéhez fűzés 2.0
- Kettős indirekció – keresőfa építése
- Hash táblák
- Keresés O(log n)-nél gyorsabban
- A hasító táblázatok röviden
- A hasító függvény
- Ütközések kezelése – egymás után téve
- Ütközések kezelése – láncolt listával
- Hash tábla – további felhasználások
1 Ismétlés – láncolt lista
A láncolt listák előnye: nagyon gyors és egyszerű a méretüket megváltoztatni (beszúrás/törlés), így alkalmasak dinamikusan változó számú adat kezelésére.
Hátrányuk: csak lineáris keresés valósítható meg bennük.
Az ideális az lenne, ha olyan adatszerkezetünk lenne, ami
- gyorsan nyújtható, mint egy láncolt lista,
- gyors keresést biztosít, mint egy rendezett tömb (bináris keresés).
3 A bináris keresőfa: rendezett tárolás
Minden elemnek két gyereke lehet:
- az elemtől balra a nála kisebb, (kisebbek!)
- tőle jobbra a nála nagyobb (nagyobbak!)
Figyeljük meg, hogy a fenti szabály nem csak a közvetlen gyermekekre igaz! Például az 5-től balra található elemek a 3, 4, 1, 2 mind kisebbek nála. Az a tény, hogy ha egy állítás igaz egy elem bal/jobboldali gyermekére, akkor az igaz az összes az elemtől balra/jobbra elhelyezkedő elemre, egy nagyon fontos tulajdonsága a bináris fáknak.
A fa elemeinek megnevezéséhez a családfa analógiáját szoktuk használni. Minden csomópontból maximum két nyíl indul ki, a csomópont két gyereke felé. A csomópont maga ilyenkor a szülő csomópont. Leszármazottaknak nevezzük egy csomópont összes gyerekét, azok gyerekeit stb. (Vagyis a gyerekek a közvetlen leszármazottak.)
A fa gyökerének a szerkezet kezdőpontját nevezzük. Ez egy olyan elem, amelyiknek nincsen már szülője. A fa levelei azok a csomópontok, amelyeknek nincsen gyerekük.
A fa rekurzív adatszerkezet: egy elem bal oldali szomszédja is egy részfa, jobb oldali is egy részfa. Ezeknek természetesen további részfáik lehetnek.
A fa csomópontjainak megnevezéséhez használjuk a gráfelméletből vett szavakat is. A fa egy gráf: csúcsokból áll, amelyeket élek kötnek össze:
- az élek irányítottak, a nyíl egy elemtől a másikra mutat, visszafelé nem,
- két elem között csak egy út létezik – nincs kör a fában.
Egy csomópont szomszédai azok, amelyek egy éllel össze vannak kötve (vagyis az adott csomópont szülője és két gyereke). A bal oldali gyereket így nevezhetjük bal oldali szomszédnak, a jobb oldali gyereket pedig jobb oldali szomszédnak. Ilyen értelemben a szülő is szomszéd.
4 A fa reprezentációja C nyelven
typedef struct BinFa {
… // tetszőleges adat
struct BinFa *bal, *jobb;
} BinFa;
A fa egy csúcsát egy struktúra írja le, amelyben az adatokon túl van két ugyanilyen strukúrára mutató pointer: a bal, illetve jobb gyermek címei.
Érdekesség: nyelvi szinten nem látszik a különbség egy duplán láncolt lista és egy bináris fa között. A különbség abban áll, hogy másra használjuk az adatok mellé tett két pointert.
5 Keresés a fában: O(log n)
Az algoritmus:
- a gyökér elemtől indulunk,
- ha az aktuális elem nem létezik, akkor nincs a fában a keresett,
- összehasonlítjuk a keresett elemmel:
- ha pont az, akkor végeztünk,
- ha nagyobb, akkor balra megyünk tovább,
- ha kisebb, akkor jobbra megyünk tovább;
- folytatjuk a 2. ponttól.
Elvileg minden lépésben feleződik a még megvizsgálandó elemek száma, tehát a keresés O(log2n) időben fut le.
kell menni!
BinFa *keres(BinFa *gyoker, int adat) {
BinFa *mozgo=gyoker;
while (mozgo!=NULL && mozgo->adat!=adat) {
if (adat < mozgo->adat) mozgo=mozgo->bal;
else mozgo=mozgo->jobb;
}
return mozgo;
}
A while ciklussal addig megyünk, amíg a „mozgo” NULL nem lesz, vagy meg nem találjuk a keresett elemet. A „mozgo” NULL lehet, mert:
- a fa még üres és egy NULL pointert kaptunk argumentumként,
- a legutóbbi összehasonlítást egy levélben végeztük és továbbindultunk egy nemlétező gyermek felé.
A logikai rövidzár miatt az while ciklus fejében az ÉS kapcsolat második fele már nem értékelődik ki, ha „mozgo” értéke NULL. Ez nagyon fontos, hiszen egy NULL pointeren a mozgo->adat kifejezés futási idejű hibát okozna. A logikai rövidzárat pontosan az ilyen jellegű kifejezések miatt vezették be a nyelvbe.
A lehetséges visszatérési értékek:
- NULL, mert a fa üres volt és rögtön az első iteráció kilépett a while ciklusból,
- NULL, mert az elemet nem találta meg és egy levél nemlétező gyermekén állt meg a ciklus,
- a megtalált elem címe.
a. megvan
c. üres fa
b. nincs benne
A keresés lehetséges eredményei:
- a fa üres, a visszatérési érték: NULL,
- nincs a fában a keresett elem (pl. 10), visszatérési érték: NULL,
- megtaláljuk a keresett elemet, visszatérés az elem címével.
6 A fa bejárása I. – rekurzió
struct BinFa {
int szam;
struct BinFa *bal, *jobb;
};
A fa bejárása rekurzió nélkül csak nehézkesen oldható meg.
Ha kihasználjuk, hogy a fa egy rekurzív adatszerkezet, akkor egyszerű feladat!
7 A fa bejárása II. – algoritmus
Feladat: írjuk ki a fában tárolt számokat!
- Járjuk be az aktuális elem bal részfáját,
- Írjuk ki az aktuális csúcsban tárolt számot,
- Járjuk be az aktuális elem jobb részfáját.
A rekurzióban az a szép, hogy a leírás C-ben is egyszerű:
void sorban_kiir(BinFa *gyoker) {
if (gyoker==NULL) // leállási feltétel
return;
sorban_kiir(gyoker->bal); // 1
printf("%d ", gyoker->adat); // 2
sorban_kiir(gyoker->jobb); // 3
}
Leállási feltétel: üres részfához értünk:
- Vagy az egész fa üres,
- Vagy az adott részfa üres!
A függvény meghívódik az üres részfákra is!
Megtehetnénk, hogy a rekurzív hívások előtt ellenőrizzük, hogy van-e valami az adott részfában, pl.:
if (gyoker->bal!=NULL) sorban_kiir(gyoker->bal);Ettől azonban rövidebb nem lesz a kód, hiszen a függvény első sorában mindenképpen kell ellenőrizni, hogy a gyökér pointer NULL vagy nem NULL. (Hiszen az egész fa is lehet üres, és az üres fa is fa, amelyre a függvény hívható.)
8 A fa bejárása III. – inorder bejárás fordítva
A megismert algoritmust inorder bejárásnak nevezik, ugyanis a fa elemein növekvő sorrendben hajtja végre a művelet.
Megcserélve a bal és a jobb részfa bejárását, csökkenő a sorrend:
- Járjuk be a elem jobb részfát (nagyobb elemek),
- Dolgozzuk fel az aktuális elemet,
- Járjuk be a bal részfát (kisebb elemek).
9 A postorder bejárás – fa felszabadítása
Ha egy teljes fát szeretnénk felszabadítani, vigyázni kell: nehogy magunk alatt vágjuk a fát, azaz nehogy elveszítsük a hozzáférést elemekhez. Felszabadításkor mindig leveleket szabad csak törölni. Olyan bejárásra van szükség, amely először a leveleket járja be, majd azokat az elemeket, amik a korábbi levelek felszabadítása után levéllé válnak és így tovább.
Csak leveleket szabad felszabadítani!
- Járjuk be az aktuális elem bal részfáját,
- Járjuk be az aktuális elem jobb részfáját,
- Szabadítsuk fel az aktuális elemet.
Előbb a részfákat felszabadítjuk, utána mehet az aktuális elem is:
void felszabadit(BinFa *gyoker) {
if (gyoker==NULL) // leállási feltétel
return;
felszabadit(gyoker->bal); // 1
felszabadit(gyoker->jobb); // 2
free(gyoker); // 3
}
10 A preorder bejárás I.
Tegyük fel, hogy van egy fa a memóriában, amit szeretnénk fájlba menteni, majd onnan visszaállítani!
Ha ezt az inorder bejárással tennénk:
1, 2, 3, 4, …
1, 2, 3, 4, …
A fájlban sorrendben lesznek az elemek, az
újra felépített fába rendezetten, a legkisebb elemtől kezdve szúrjuk
be az elemeket. Ez azt jelenti, hogy az „1” lesz a gyökér, és minden
további elem nagyobb, mint az előző, tehát a fa egy láncolt
listává degradálódik. Így a keresés már
nem logaritmikus
időben fut!
11 A preorder bejárás II.
- Vegyük sorra az aktuális elemet,
- Járjuk be az aktuális elem bal részfáját,
- Járjuk be az aktuális elem jobb részfáját.
A fájl pointerét, amelybe írjuk az adatokat, természetesen át kell adni paraméterként mindenhol:
void fajlba_kiir(BinFa *gyoker, FILE *fajl) {
if (gyoker==NULL) // leállási feltétel
return;
fprintf(fajl, "%d ", gyoker->adat); // 1
fajlba_kiir(gyoker->bal, fajl); // 2
fajlba_kiir(gyoker->jobb, fajl); // 3
}
12 Műveletek fákon – általában
Sokféle kérdés feltehető egy fával kapcsolatban:
- Hány eleme van? Hány levele van?
- Mekkora egy adott szintjén lévő elemek száma?
- Milyen magas?
- Hányadik szintig van teljesen betöltve?
A fenti feladatokat rekurzív algoritmusokkal lehet könnyen megoldani.
A megoldás sémája
- A feladatot megoldjuk a bal részfára (rekurzív hívás)
- A feladatot megoldjuk a jobb részfára (rekurzív hívás)
- Számbavesszük az aktuális elemet
13 Műveletek – fák elemszáma
Feladat: számoljuk meg, hogy hány eleme van egy fának!
- Ha üres a fa (
NULLpointert kaptunk), térjünk vissza 0-val! - Különben vegyük az aktuális elem bal részfájának az elemszámát!
- Adjuk hozzá a jobb részfa elemszámát!
- Adjunk hozzá 1-et (aktuális elem)! Térjünk vissza így.
ha levél gyerekeire
(üres részfára)
hívjuk meg!
int elemszam(BinFa *gyoker) {
if (gyoker==NULL) return 0; // 1
return elemszam(gyoker->bal) // 2
+ elemszam(gyoker->jobb) // 3
+ 1; // 4
}
Valójában az történik, hogy a return utáni kifejezésben bejárjuk a fát.
- Meghívjuk az elemek balgyermekére sorozatban, amíg NULL pointerig nem jutunk (legbaloldalibb gyermek balgyermeke NULL).
- Ekkor a legutolsó függvény 0-val visszatér.
- Ehhez hozzáadjuk a legbaloldalibb gyermek jobbgyermekeinek a számát.
- Ha ez a gyermek egy levél volt, akkor onnan is 0-val térünk vissza, majd ehhez még egyet adunk, tehát megszámoltuk őt.
- Ezután eggyel feljebb lépünk, hiszen annak az elemnek a balrészfájára elvégeztük a műveletet és 1-et kaptunk.
- Ekkor annak is megszámláljuk a jobbrészfáját, majd hozzáadunk 1-et és újból feljebb lépünk.
- Akkor fejeződik be a függvényhívási sorozat, amikor a legjobboldalabbi elemet is megszámláltuk és ezzel visszajutunk a legelső függvényhívásba. Ott hozzáadunk 1-et az eredményhez – ezzel megszámlálva a gyökér elemet –, majd visszatérünk az elemszámmal.
14 Műveletek – levelek száma
Levelek száma: hasonló, de feltételhez kell kötni a számlálást:
- Ha üres a fa (NULL pointert kaptunk), térjünk vissza 0-val!
- Ha az aktuális elem levél, térjünk vissza 1-gyel!
- Különben vegyük az aktuális elem bal részfájában a levelek számát, adjuk hozzá a jobb részfa leveleinek számát, térjünk ezzel vissza!
int levelszam(BinFa *gyoker) {
if (gyoker==NULL) return 0; // 1
if (gyoker->bal==NULL && gyoker->jobb==NULL) // 2
return 1;
return levelszam(gyoker->bal) // 3
+ levelszam(gyoker->jobb);
}
Itt is bejárjuk a fát.
- Ha egy levélhez jutunk, akkor egyet adunk vissza, hiszen neki már nem lehetnek gyermekei, ahonnan egyéb érték érkezhetne. Ilyenkor függvényhívásra sincs már szükség (hiszen nincsenek részfák, amelyekben számolni kellene bármit is).
- Ha nem levélen állunk, akkor megszámláljuk a bal részfában a leveleket (rekurzívan meghívjuk a függvényt a bal gyermekre, majd ugyanezt megtesszük a jobb részfában és a kettő összegével térünk vissza.
- Üres fa, vagy nemlétező gyermek esetén 0-val térünk vissza.
Figyeljük meg, hogy csak akkor nem hívjuk meg a gyermekekre a függvényt, ha levélben vagyunk. Olyankor ha csak az egyik gyermek NULL, meghívjuk rá, tehát ilyenkor is az (1) feltétel fut le.
15 Műveletek – elemek adott szinten
Feladat: adott szinten hány elem van?
- Üres fa esetén a visszatérési érték 0.
- Ha az átvett szint értéke 0, akkor azt a szintet kell megszámolni: visszatér 1-gyel.
- Különben megszámolja a bal és jobb részfában a megfelelő elemeket. Ehhez a szintet eggyel csökkentve hívja magát.
int szint_elemei(BinFa *gyoker, int szint) {
if (gyoker==NULL) return 0; // 1
if (szint==0) return 1; // 2
return szint_elemei(gyoker->bal, szint-1) // 3
+ szint_elemei(gyoker->jobb, szint-1);
}
A fontos mozzanat itt az, hogy ennek a függvénynek nem csak, hogy van egy paramétere, de azt a paramétert a rekurzióban változtatja is. Hogy hány csúcs van az ötödik szinten, ahhoz azt kell összeadni, hogy hány csúcs van a bal- és a jobboldali részfában a negyedik szinten. Az ő gyökerükhöz képest negyedik szinten!
16 Keresőfa építése
Rekurzívan könnyű az új elem hozzáadása a keresőfához:
- Ha a fa üres, akkor gyökér lesz az új.
- Ha a gyökérnél kisebb az új, a bal oldali részfába kell beszúrni.
- Ha a gyökérnél nagyobb, a jobb oldaliba.
- Amúgy pedig már benne van.
Csakhogy közben változhat a fa gyökere pointer!
BinFa *gyoker = NULL; gyoker = beszur(gyoker, 5); gyoker = beszur(gyoker, 2); gyoker = beszur(gyoker, 7);
Megoldás: térjünk vissza vele, mint a listáknál.
BinFa *beszur(BinFa *gyoker, int adat) {
if (gyoker==NULL) { // üres?
BinFa *uj = (BinFa*) malloc(sizeof(BinFa));
uj->bal = uj->jobb = NULL; /* levél lesz */
uj->adat = adat;
return uj; /* vissza kell térni vele! */
}
if (adat < gyoker->adat) // kisebb?
gyoker->bal = beszur(gyoker->bal, adat);
else if (adat > gyoker->adat) // nagyobb?
gyoker->jobb = beszur(gyoker->jobb, adat);
else
; /* benne van */
return gyoker;
}
Fontos végiggondolni, mi történik az egyes mutatókkal. Tegyük fel, hogy a függvényt a következő formában hívták meg:
gyoker = beszur(gyoker, 5);
- Ha a fa üres, akkor
gyoker=NULL. Ilyenkor az 1-es feltétel igaz lesz, és keletkezik egy új elem. A paraméterként kapott gyökér pointert, amely egy lokális változó, felülírjuk az új elem címével. Végülis pedig majd visszatérünk ezzel, és az eredeti gyökér mutatót a hívás után az értékadás fogja átállítani. - Ha a fa nem üres, akkor a gyökerében van egy elem. Ennek értékétől függ, hogy az új elemet a bal- vagy a jobboldali részfába kell szúrni. Ha a gyökérnél kisebb a beszúrandó, oda kell kerülnie (2).
- Ha a jobb oldali részfába kerül az elem, akkor ugyanez a helyzet.
- Ha se nem kisebb, se nem nagyobb, akkor a gyökérelemben azt látjuk, amit amúgy is be kell szúrni. Ilyenkor simán visszatérünk, nincs teendő, hiszen az elem már szerepel a fában. A gyökér pointer változatlan.
Fontos mozzanat az utolsó: hogy visszatérünk a változatlan gyökér pointerrel. A hívó ugyanis az értékadást mindenképpen elvégzi, és ilyenkor azt kell biztosítani, hogy a teljes fa gyökér pointere ne változzon – ehhez pedig egyszerűen visszaadjuk ugyanazt a gyökér pointert, amit kaptunk.
Ha a gyökér létezik, és annak a bal oldali részfájába szúrunk be, akkor hasonlóan
megy minden. Ha ott NULL pointer van, akkor az felülíródik. Ha nem
NULL, akkor az ott meghívott függvény változatlan gyoker
pointerrel tér vissza, azaz az értékadásból
gyoker->bal=gyoker->bal lesz végül. Minden helyen ez lesz
a helyzet, kivétel ott, ahol valamelyik részfa üres fa!
Ebből következik a függvény hívásának módja a függvény belsejében is. Ha azt mondtuk, hogy a függvényt így kell használni:
gyoker = beszur(gyoker, 5);
Akkor a bal oldali részfába beszúrásnál ezt kell írnunk:
gyoker->bal = beszur(gyoker->bal, 5);
17 Példa – szavak statisztikája
kutya 2 cica 6 mérési 4 hiba 1
Feladat: készítsünk beolvasott szavakról statisztikát! Melyik, hányszor szerepel?
szostat.c
Megoldás: sorban haladunk a fájl szavain; ha az aktuális szó még nincs benne a fában, akkor betesszük és a darabszámot 1-re állítjuk; Ha már benne van, akkor megnöveljük a darabszámot.
A fák ideálisak erre a feladatra, hiszen gyors a beszúrás és az „eleme-e” művelet. Bónusz: ábécé rendben kapjuk meg az eredményt. A feladathoz szükséges adatstruktúra:
typedef struct SzoStat {
char szo[51];
int db;
struct SzoStat *bal, *jobb;
} SzoStat;
A szabványos bemeneten érkező szöveg statisztikájának az elkészítése:
a
scanf %swhitespace-ig olvas
int main() {
SzoStat *fa = NULL; // üres fa
char szo[51];
while (scanf("%s", szo) == 1)
fa = beszur(fa, szo);
kiir(fa);
felszabadit(fa);
return 0;
}
A szükséges módosítás a beszúró algoritmuson:
le kell kezelni azt az esetet, amikor az elem már benne van a
fában, és strcmp()-vel kell végezni a sztringek
összehasonlítását.
SzoStat *beszur(SzoStat *gyoker, char *szo) {
int er;
if (gyoker==NULL) {
SzoStat *uj = (SzoStat*) malloc(sizeof(SzoStat));
strcpy(uj->szo, szo);
uj->db = 1;
uj->bal = uj->jobb = NULL;
return uj; /* vissza kell térni vele! */
}
er = strcmp(szo, gyoker->szo);
if (er < 0)
gyoker->bal = beszur(gyoker->bal, szo);
else if (er > 0)
gyoker->jobb = beszur(gyoker->jobb, szo);
else
gyoker->db++;
return gyoker;
}
18 Fák alkalmazásai – hierarchia
Hierarchia tárolása.
Műveletek, pl. (2+3)*5.
Nincs szükség zárójelezésre!
Dekódoló fa.
Morze: ti = balra, tá = jobbra.
19 Fák alkalmazásai – további alakok
struct TriFa {
…
struct TriFa *bal,
*kozep,
*jobb;
};
háromágú fa
Az elvek ugyanazok, mint a bináris fánál. Pl. elemek száma:
- Ha NULL pointer, akkor 0.
- Egyébként be kell járni az összes részfát.
- Az elemek száma azok elemszámának összege + 1.
struct BinFa {
int adat;
struct BinFa *szulo;
struct BinFa *bal, *jobb;
};
szülőkre mutató pointerrel
Így egyszerűbb:
- Iteratív bejárás „jobbra tapogatózva”
- Törlés
Érdemes megfigyelni: nyelvileg nem különbözik a két struktúra egymástól. Csak máshogyan használjuk a pointereket!
A bináris fából törlés sem triviális művelet – főleg, ha a törlendő csúcsnak bal- és jobboldali szomszédja is van. Ilyenkor a fát át kell rendezni, ha a keresőfa tulajdonságot meg szeretnénk tartani. Ezzel is az Algoritmuselmélet tárgy fog foglalkozni.
20 Fák alkalmazásai – hatékonyság
Kiegyensúlyozott fa: bármely csúcspont részfáinak magassága közötti különbség legfeljebb egy. (A bal oldali nem ilyen.)
„Algoritmuselmélet” tárgy
Ha a fa nem kiegyensúlyozott, akkor a keresés lassabb.
Az ebben az előadásban bemutatott faépítő algoritmusok nem kiegyensúlyozott fát építenek. Az általuk épített fa kiegyensúlyozottsága attól függ, mennyire érkeznek szerencsésen a beszúrandó adatok. A kiegyensúlyozott építéshez összetettebb algoritmusok szükségesek – ezeket majd az Algoritmuselmélet nevű tárgy fogja bemutatni.
21 A tanult adatszerkezetek
Eddig az alábbi adatszerkezetekkel ismerkedtünk meg:
- tömbök,
- láncolt listák,
- bináris fák.
Mindnek megvannak az előnyei és a hátrányai. Alaposan ismerni kell a felépítésüket, működésüket, algoritmusaikat ahhoz, hogy képesek legyünk eldönteni, egy adott feladathoz melyik illik a legjobban.
| Elérés | Keresés | Beszúrás | Törlés | |
|---|---|---|---|---|
| Tömb | 1 | n | n | n |
| Lista | n | n | 1 | 1 |
| Fa | log n | log n | log n | log n |
Tömb
Előnye:
- gyors, tetszőleges sorrendű adatelérés (a leggyorsabb),
- csak annyi helyet foglalnak el a memóriában, amennyi a hasznos adat,
- hatékony rendező algoritmusok léteznek tömbökre.
Hátránya:
- az átméretezésük költséges – gyakran változó mennyiségű adathoz nem alkalmasak,
- a feltöltés közbeni rendezésük költséges.
Mikor használjuk: ha az adatok száma keveset változik és kritikus a rendkívül gyors adatelérés. A rendezett tömbök esetén lehetséges bináris keresés (O(log n) lépésszámmal), de a rendezés költséges művelet.
Lista
Előnye:
- egyszerű bővíthetőség (rendezetlennél a leggyorsabb),
- egyszerű törlés (nem kell mozgatni az elemeket),
- egyszerű (bár nem a leghatékonyabb) rendezve építés.
Hátránya:
- az elemek csak lineárisan kereshetőek.
Mikor használjuk: ha az adatok száma gyakran változik és a keresés nem időkritikus (pl. ha mindig a feltöltés (fordított) sorrendjében van szükség az elemekre: vermek, sorok).
Fa
Előnye:
- egyszerű bővíthetőség (a rendezettnél a leggyorsabb),
- egyszerű és hatékony rendezve építés,
- nagyon gyors keresés (kb. a tömbök sebességével megegyező).
Hátránya:
- egy elem törlése bonyolult feladat (át kell rendezni a fát).
Mikor használjuk: ha az adatok száma gyakran változik és a fontos rendezettség, illetve a gyors keresés. Előnyös halmazok, asszociatív tömbök megvalósítására.
23 Indirekció – cím szerinti paraméterátadás
C-ben csak érték szerinti paraméterátadás van. A függvények a paraméterek másolatát kapják.
A cím szerinti paraméterátadás mégis megoldható pointerrel:
void novel(int *pi) {
(*pi)++; // a mutatott integert
}
int x=5;
novel(&x); // x címe
printf("%d", x);
A listás, fás algoritmusaink megváltoztatják a lista eleje, fa gyökere mutatót. Ötlet: azt ugyanígy kellene átadni!
Ez azért lehetséges, mert a pointer ugyanolyan változó, mnint a többi. Vegyük példának a lenti függvényt. Ez két VALAMI-t cserél meg. Hogy a cseréket el tudja végezni, a függvény nem érték szerint várja a paramétereit (azaz a változók másolatát), hanem cím szerint (pointereket az eredeti változókra). A kódban VALAMI helyére bármilyen típust beírhatunk: kapunk egy függvényt, amely két adott típusú dolgot kell megcserélni.
*p1: egy VALAMI
void csere(VALAMI *p1, VALAMI *p2) {
VALAMI temp=*p1;
*p1=*p2;
*p2=temp;
}
Ha két int-et szeretnénk cserélni, akkor a VALAMI
helyre int-et írunk. Ha két int*-ot, akkor a VALAMI
helyére int* kerül.
void int_csere(int *p1, int *p2) {
int temp=*p1;
*p1=*p2;
*p2=temp;
}
int x=29; int y=17;
int_csere(&x, &y); /* x=17, y=29 */
void ptr_csere(int **p1, int **p2) {
int *temp=*p1;
*p1=*p2;
*p2=temp;
}
int *px=&x; int *py=&y;
ptr_csere(&px, &py); /* px=&y, py=&x */
24 Kettős indirekció: lista építése
A lista elejére beszúró függvény a múltkori módon:
ListaElem *elore_beszur(ListaElem *eleje, int adat) {
ListaElem *uj = (ListaElem*) malloc(sizeof(ListaElem));
uj->adat = adat;
uj->kovetkezo = eleje;
return uj;
}
A függvénynek meg kell tudnia változtatni a lista eleje mutatót:
A másik változata, ami az „eleje” pointer címét veszi át:
void elore_beszur_ptr(ListaElem **peleje, int adat) { // !
ListaElem *uj = (ListaElem*) malloc(sizeof(ListaElem));
uj->adat = adat;
uj->kovetkezo = *peleje; // *peleje – az első elem címe
*peleje = uj;
}
Kettős indirekció. A függvény belsejében:
peleje- Annak a pointernek a címe, amely a lista elejét tárolja. Ez
képződik a hívás helyén, amikor ott azt írjuk, hogy
&eleje. Ez amain()lokális váltózójának címe. *peleje- A lista elejének (első elemének, vagyis az első struktúrának) címe. Ez a hívó változójának értéke.
**peleje- Ez lenne a lista első eleme maga (a struktúra), de most nem használjuk.
25 Kettős indirekció: lista építése – használat
A múltkorinál a visszaadott mutatót be kellett másolni a változóba:
ListaElem *eleje=NULL; eleje=elore_beszur(eleje, 2); eleje=elore_beszur(eleje, 3);
A mostani függvény cím szerint kapja azt, ezért meg tudja tenni azt is:
megoldás!
ListaElem *eleje=NULL; elore_beszur_ptr(&eleje, 2); elore_beszur_ptr(&eleje, 3);
A nagy előny: így nem lehet kifelejteni az értékadást!
Figyeljük meg a használatok közötti különbséget:
| visszaadott eleje | címként átadott eleje | |
|---|---|---|
| előny | egyszerű | nem lehet elfelejteni a megváltozó eleje pointert (mivel szól a fordító*, hogy rossz az átadott paraméter típusa, ListaElem** helyett ListaElem*) |
| hátrány | a visszaadott pointer elfelejthető | kényelmetlen, mindenhova pluszba kell az & operátor |
* A rendes fordítók szólnak.
26 Kettős indirekció – lista végéhez fűzés
Beszúrás lista végére, és az „eleje” mutató címének átvétele:
void vegere(ListaElem **peleje, int adat) {
ListaElem *uj=(ListaElem*) malloc(sizeof(ListaElem));
uj->adat=adat;
uj->kov=NULL;
if (*peleje==NULL)
*peleje=uj; // eleje ptr változik
else {
ListaElem *mozog;
for (mozog=*peleje; mozog->kov!=NULL; mozog=mozog->kov)
; /* üres ciklus */
mozog->kov=uj; // az utolsó „következő”-je változik
}
}
Vegyük észre: van valahol egy ListaElem* mutató, amit be kell
állítani. Vagy az utolsó elemben, vagy kívül, a hívónál!
27 Kettős indirekció – lista végéhez fűzés 2.0
void vegere(ListaElem **peleje, int adat) {
ListaElem **pmozgo, *uj;
pmozgo=peleje; // pointer megkeresése
while (*pmozgo!=NULL)
pmozgo=&(*pmozgo)->kov; // !
uj=(ListaElem*) malloc(sizeof(ListaElem)); // új elem
uj->adat=adat;
uj->kov=NULL;
*pmozgo=uj; // a megtalált NULL pointer felülírása
}
Egy lehetséges módosított algoritmushoz az előző
felismerés adja az ötletet – nevezetesen az, hogy a lista végére szúrás
azt jelenti, hogy meg kell keresni a lista végén azt a NULL pointert,
amelyik lezárja azt. Mindegy, hogy ez a lista végi elemben van, vagy a lista
eleje pointer a NULL (üres lista esetén) – a feladat az,
hogy felülírjuk azt a mutatót az új elem címével.
A függvény elején a ciklus ezért ezt teszi: megkeresi
azt a NULL pointert. A pmozgo pointer ennek
a pointernek a címét tárolja. Először a lista eleje pointerre mutat,
utána pedig minden lépésben a mutatott listaelem következő pointerére
állítjuk át. Fontos az indirekciók számában a különbség: a pmozgo
pointer itt nem a listaelemre mutató pointer, hanem a listaelemre mutató
pointerre mutató pointer. Vagyis a listaelem címét tároló változó címe.
Így először a hívó eleje pointerének címét tárolja, utána
az első listaelem kov pointerének címét stb. Ezért van szükség
a címképző operátorra is a ciklustörzsben: *pmozgo a listaelemre
mutató pointer, (*pmozgo)->kov az abban tárolt „következő”
pointer, &(*pmozgo)->kov pedig annak a címe.
28 Kettős indirekció – keresőfa építése
A keresőfába beszúráshoz meg kell keresni
azt, hogy hol kell legyen a beszúrandó elemre mutató pointer.
Ha a fa üres lenne, akkor a legfelső, azaz a gyökérpointert
kellene úgy módosítani, hogy az az új elemre mutasson. Ha a 0-t szeretnénk beszúrni,
akkor az 1-es csomópont bal pointerét (amely jelenleg NULL
pointer) kell úgy módosítani, hogy az az új elemre mutasson. Ha a 4-est keressük,
az benne van a fában – a 3-as csomópont jobb pointere az, amely
rá mutat, és ilyenkor a beszúráshoz nem kell tenni semmit.
Azt keressük, hogy hol az a pointer, amely majd mutat rá.
Ez valamelyik csomópont bal/jobb pointere, vagy a gyökér pointere.
Ha van egy ilyen keresőalgoritmusunk. samely nem a keresett elemre mutató
pointert adja vissza, hanem a keresett elemre mutató pointer címét,
akkor könnyű a beszúrás. Ha nincs meg a keresett elem, ez akkor is
értékes választ ad: a visszaadott pointer egy olyan pointerre mutat,
amely értéke NULL, de ott kellene legyen a keresett elemre
mutató pointer. A beszúrás:
/* új csomópontot szúr a keresőfába.
* A gyökerpointert cím szerint veszi át. */
void beszur(BinFa **pgyoker, int adat) {
/* megkeresi a helyét */
BinFa **ptr=beszurashoz_keres(pgyoker, adat);
if (*ptr==NULL) { // ha még nincs
BinFa *uj=(BinFa*) malloc(sizeof(BinFa));
uj->adat=adat;
uj->bal=uj->jobb=NULL;
*ptr=uj; // beszúrás
}
}
A *ptr=uj; kifejezés
- üres fa esetén a függvényen kívüli, a fa gyökerére mutató pointert változtatja meg,
- nem üres fában valamelyik csomópont
balvagyjobbmutatóját változtatja meg, amely eddigNULLértékű volt, és ahová új levélként bekerül a beszúrandó elem.
Fontos, hogy ez a beszúró függvény cím szerint vegye át a gyökér címét is: BinFa **pgyoker.
Ha egy teljesen üres fába szúrunk be elemet, akkor az meg kell változzon:
BinFa *gyoker = NULL; beszur(&gyoker, 5);
Ugyanezért kell a kereső algoritmusnak is cím szerint átadni a pointert:
ha a fa üres, a kereső algoritmus a gyökérelem pointer címével kell visszatérjen
(annak címével amely egyelőre még NULL, és az ebben a példában
az egész, üres fa gyökere).
A kettős indirekció a keresésben:
/* Megkeresi az adott elem (leendő) helyét a fában.
* Egy olyan címet ad vissza, ahol a leendő elem címének
* kell tárolódnia, vagy ahol a megtalált elem címe tárolódik. */
BinFa **beszurashoz_keres(BinFa **pgyoker, int adat) {
BinFa **pmozgo=pgyoker;
while (*pmozgo!=NULL && (*pmozgo)->adat!=adat) {
if (adat < (*pmozgo)->adat)
pmozgo=&(*pmozgo)->bal;
else
pmozgo=&(*pmozgo)->jobb;
}
return pmozgo;
}
Mivel mutatók címével tér vissza, azok értékét felül tudjuk írni.
Emlékeztetőül itt a kétszeres indirekció nélküli keresés. Érdemes összehasonlítani – lényegében annyi a különbség, hogy egyikben mindenhol
mozgovan, másikban mindenhol*pmozgo. Ahol a lentiben konkrét érték van (mozgo), ott a fentiben cím, amelyet dereferálni kell (*pmozgo). Ahova a lentiben konkrét érték kerül (mozgo=), oda a fentiben cím kell (pmozgo=), ezért címet kell képezni (pmozgo=&...).BinFa *keres(BinFa *gyoker, int adat) { BinFa *mozgo=gyoker; while (mozgo!=NULL && mozgo->adat!=adat) { if (adat < mozgo->adat) mozgo=mozgo->bal; else mozgo=mozgo->jobb; } return mozgo; }
30 Keresés O(log n)-nél gyorsabban
Elem megtalálása
- Rendezetlen tömbben lineáris keresés: O(n)
- Rendezett tömbben bináris keresés: O(log n)
- Listában: O(n)
- Keresőfában: O(log n)
Vajon lehet ennél jobbat?
O(1) lépésben
„Algoritmuselmélet” tárgy
Honnan tudjuk, hogy hol keressük?
Onnan, hogy tudjuk, hova raktuk!
31 A hasító táblázatok röviden
Kulcs → érték párok tárolása
- Viszonylag nagy tömböt foglalunk
hash()függvény: kulcs→tömbindex (to hash = összekever, összezagyvál)
Jó hasító függvény?
- gyors,
- egyenletesen helyezi el az adatokat
A használat elve:
Adat hash_tabla[MERET]; // a hash tábla: tömb
printf("Telefon: %s\n", hash_tabla[hash("Taz")].telszam);
32 A hasító függvény
A kulcsot le kell képezni a 0 … MÉRET-1 tartományra. Példa hash függvények (nem túl jók):
int egesz_hash(int i) {
return i % MERET;
}
int sztring_hash(char *nev) {
return ((nev[0]-'A')*26+(nev[1]-'a')) % MERET;
}
Ütközések: ha két kulcs ugyanoda „hashelődik”:
kulcs % MÉRET == (kulcs + MÉRET) % MÉRET
Pl. a fenti függvénnyel Taz és Tapsi Hapsi.
33 Ütközések kezelése – egymás után téve
Az ütköző adatokat egymás után helyezzük el a táblában.
- Így az adatok elcsúszhatnak a helyükről.
- Az egymás utániaknál, ahol nem üres, lineárisan keresünk
- Törlésnél problémás: meg kell maradnia a törölt elemnek, csak megjelöljük, hogy törölt
- A tábla így töredezetté válik, időnként karbantantartást igényel
34 Ütközések kezelése – láncolt listával
Az ütköző elemeket láncolt listába tesszük:
Kevés ütköző elem → néhány elem a listákban. Gyors keresés!
typedef struct ListaElem {
char nev[51];
char telefonszam[20];
struct ListaElem *kovetkezo;
} ListaElem;
ListaElem *hashtabla[MERET]; /* listák tömbje */
char keresett[]="Tapsi Hapsi";
index=hash(keresett);
talalt=listakeres(hashtabla[index], keresett);
if (talalt!=NULL)
printf("Telefonszáma: %s\n", talalt->telefonszam);
else
printf("Nincs ilyen név!\n");
35 Hash tábla – további felhasználások
Google keresés?
- Kulcs: keresett szó
- Érték: weboldalak listája
- az ütközés természetes: egy szó több helyen megtalálható
Fájlcserélő: elosztott tábla
- Kulcs: fájl neve, érték: a fájl (vagy hivatkozás a fájlra)
- Hasító függvény kimenete: mely gépen tároljuk az adatot
- Pl. BitTorrentnél: fájl darabjai kinél találhatóak meg
