Fabejárás
Az informatikában a fabejárás (vagy fakeresés, fában keresés) egyfajta gráfbejárás, és arra a folyamatra utal, hogy feldolgozzuk (ellenőrizzük és/vagy módosítjuk) egy fa adatszerkezet minden egyes csúcsát (csomópontját) pontosan egyszer. Az ilyen algoritmusokat a csúcsok bejárásának sorrendje szerint osztályozzuk. A következő algoritmusok bináris fához készültek, de általánosíthatók más fákhoz is.
Típusai
A láncolt listáktól, egydimenziós tömböktől és más lineáris adatszerkezetektől eltérően, amelyeknél a bejárás konvencionálisan lineáris sorrendben megy végbe, a fánál a bejárás többféleképpen történhet. Lehetséges mélységi, vagy szélességi bejárás. Mélységi bejárásra három általános módszer létezik: in-order (gyökérközepű) pre-order (gyökérkezdő), post-order (gyökérvégző). [1] Ezeken az alapvető bejárásokon kívül számos más, összetett vagy kevert séma is létezik. Ilyenek például a korlátozott mélységű keresések, mint az iteratív, mélyülő mélységi keresés. Az utóbbi, a szélességi keresés végtelen fák bejárására is használatos, lásd lejjebb.
Adatszerkezetetek fabejáráshoz
A fa bejárása során az összes csúcs feldolgozása valamilyen módszer szerint kerül sorra. Mivel egy adott csúcsból egynél több más csúcs is elérhető (nem lineáris adatszerkezet), így szekvenciális (nem párhuzamos) bejárást feltételezve néhány csúcs feldolgozását el kell halasztani - valamilyen módon tárolni kell a későbbi feldolgozáshoz. Ez gyakran verem (LIFO) vagy sor (FIFO) segítségével történik. Mivel a fa öndefiniáló (rekurzívan definiálható) adatszerkezet, a bejárás érthető módon megvalósítható rekurzióval (mag-rekurzió segítségével). Ebben az esetben a későbbiekben feldolgozandó csúcsok implicit módon tárolódnak a hívásveremben.
Mélységi keresés könnyen megvalósítható verem segítségével, akár rekurzív módon is (hívásveremmel), míg a szélességi keresés sor segítségével végezhető el.
Mélységi keresés bináris fában
A következő kereséseket mélységi keresésnek (depth-first search, DFS) nevezzük, mivel a keresőfát a lehető legtovább mélyítjük minden egyes csúcsnál a következő testvércsúcs feldolgozása előtt. Bináris fa esetében a keresést minden egyes csúcshoz való hozzáférésként értelmezve az algoritmus a következő: [2] [3]
A bináris fa bejárásának általános rekurzív algoritmusa a következő:
Ugrás egy szinttel lejjebb az N csúcshoz (N a rekurzió argumentuma). Ha N létezik (nem üres), a következő három művelet végrehajtása a megfelelő sorrendben: | |
(L) | N bal részfájának rekurzív bejárása |
(R) | N jobb részfájának rekurzív bejárása |
(N) | Az aktuális (N) csúcs feldolgozása. |
Ugrás egy szinttel feljebb N szülő-csúcsához. |
A példákban az (L) művelet többnyire (R) előtt kerül végrehajtásra, de (L) előtt szintén lehetséges, lásd (RNL).
Pre-order - gyökérkezdő (NLR)
- Az aktuális csúcs adatainak feldolgozása.
- Bal oldali részfa bejárása a pre-order függvény rekurzív meghívásával.
- Jobb oldali részfa bejárása a pre-order függvény rekurzív meghívásával.
- Az pre-order bejárás topologikusan rendezett, mivel a szülő csúcs feldolgozása megtörténik, mielőtt annak bármelyik gyermek csúcsa sorra kerül.
In-order - gyökérközepű (LNR)
- Bal oldali részfa bejárása a in-order függvény rekurzív meghívásával.
- Az aktuális csúcs adatainak feldolgozása.
- Jobb oldali részfa bejárása a in-order függvény rekurzív meghívásával.
- Bináris keresőfában, amelyben a csúcsok úgy vannak rendezve, hogy minden csúcs értéke nagyobb a bal oldali részfában lévő összes értéknél, és kisebb a jobb oldali részfában lévő összes értéknél, az in-order bejárás növekvő sorrendben dolgozza fel a csúcsokat. [4]
Fordított in-order - gyökérközepű (RNL)
- Jobb oldali részfa bejárása a fordított in-order függvény rekurzív meghívásával.
- Az aktuális csúcs adatainak feldolgozása.
- Bal oldali részfa bejárása a fordított in-order függvény rekurzív meghívásával.
- Bináris keresőfában a fordított in-order bejárás csökkenő sorrendben dolgozza fel a csúcsokat.
Post-order - gyökérvégző (LRN)
- Bal oldali részfa bejárása a post-order függvény rekurzív meghívásával.
- Jobb oldali részfa bejárása a post-order függvény rekurzív meghívásával.
- Az aktuális csúcs adatainak feldolgozása.
A bejárás sorrendjében felsorolt csúcsok listáját a fa kiegyenesítésének (szekvencializációnak) nevezik. A felsorolt bejárási minták egyike sem határozza meg egyértelműen a bejárt fát. Egy – minden csúcs értékében különböző – fa egyértelműen meghatározható az in-order, és (tetszőlegesen) a pre- vagy a post-order kiegyenesítések együttesével. Ezzel szemben A pre- és post-order a fa szerkezetét nem adja meg egyértelműen. [5]
Általános fa
Bármely fának a mélységi bejárásához a következő műveleteket kell rekurzív módon végrehajtani minden egyes csúcson:
- Pre-order művelet végrehajtása.
- Minden egyes i (i=1-től a gyermekek számáig):
- i feldolgozása, ha létezik.
- In-order művelet végrehajtása.
- Post-order művelet végrehajtása.
A problémától függően a pre-order, az in-order, vagy a post-order művelet érvénytelen lehet, vagy csak egy adott csúcs feldolgozása szükséges. Ezek a műveletek tehát nem kötelezőek. Ezenkívül a gyakorlatban egynél több pre-order, in-order, vagy post-order műveletre lehet szükség. Például, amikor beszúrás történik egy ternális fába, a csúcsok összehasonlításakor egy pre-order művelet kerül végrehajtásra. Utólag egy post-order műveletre lehet szükség a fa kiegyensúlyozásához.
Szélességi bejárás / szint-sorrend
A fák bejárhatók szint szerinti sorrendben, ahol minden egyes, az adott szinthez tartozó csúcsot feldolgozunk, mielőtt a következő szintre lépünk. Ezt a módszert szélességi bejárásnak/keresésnek (BFS) nevezik, mivel a keresőfát minden szint esetében a lehető legjobban kiszélesítjük a következő szintre lépés előtt.
Egyéb típusok
Vannak olyan fabejárási algoritmusok is, amelyek nem tartoznak sem a mélységi, sem a szélességi kereséshez. Az egyik ilyen algoritmus a Monte Carlo-fakeresés, amely a legígéretesebb lépések elemzésére koncentrál, és a keresési fa kibővítését véletlenszerű mintavételére alapozza.
Alkalmazási lehetőségek
A pre-order bejárás felhasználható prefix-kifejezés (lengyel forma) elkészítésére kifejezésfákból: a kifejezésfa pre-order bejárásával. Például, az ábrázolt aritmetikai kifejezés fáját pre-order sorrendben bejárva a "+ * A - B C + D E " eredményt kapjuk.
Bináris fa post-order bejárása postfix-kifejezést (fordított lengyel jelölés) generál. A ábrázolt aritmetikai kifejezés fáját post-order sorrendben bejárva az "A B C - * D E + +" eredményt kapjuk; ez utóbbi könnyen átalakítható gépi kóddá, hogy a kifejezést egy verem-géppel ki lehessen értékelni.
Az in-order bejárást nagyon gyakran használják a bináris keresőfáknál, mivel a fában tárolt értékeket a keresőfa összehasonlító algoritmusának megfelelő sorrendben adja vissza.
Csúcsok törlése vagy felszabadítása közben végzett post-order bejárás törölheti vagy felszabadíthatja a teljes fát. Ezáltal a csúcs törlődik gyermekei törlése után.
Bináris fa másolása szintén post-order művelet-sorrendben történik, mivel így a csúcs gyermekére való hivatkozás közvetlenül a gyermek-csúcs rekurzív létrehozása után kap értéket. Ez azt jelenti, hogy a szülő létrehozása nem fejezhető be az összes gyermek létrehozásának befejezése előtt.
Implementációk
Mélységi bejárás
Pre-order
preorder(csúcs) ha (csúcs == null) return feldolgoz(csúcs) preorder(csúcs.bal) preorder(csúcs.jobb) |
iteratívPreorder(csúcs) ha (csúcs == null) return s ← üres verem s.rárak(csúcs) amíg (nem s.Üres()) csúcs ← s.levesz() feldolgoz(csúcs) //a jobb oldali csúcs kerül be először a verembe, így a bal oldali dolgozódik fel elsőként ha csúcs.jobb ≠ null s.rárak(csúcs.jobb) ha csúcs.bal ≠ null s.rárak(csúcs.bal) |
In-order
inorder(csúcs) ha (csúcs == null) return inorder(csúcs.bal) feldolgoz(csúcs) inorder(csúcs.jobb) |
iteratívInorder(csúcs) s ← üres verem amíg (nem s.Üres() vagy csúcs ≠ null) ha (csúcs ≠ null) s.rárak(csúcs) csúcs ← csúcs.bal egyébként csúcs ← s.levesz() feldolgoz(csúcs) csúcs ← csúcs.jobb |
Post-order
postorder(csúcs) ha (csúcs == null) return postorder(csúcs.bal) postorder(csúcs.jobb) feldolgoz(csúcs) |
iteratívPostorder(csúcs) s ← üres verem utolsóFeldolgozottCsúcs ← null amíg (nem s.Üres() vagy csúcs ≠ null) ha (csúcs ≠ null) s.rárak(csúcs) csúcs ← csúcs.bal egyébként aktuálisCsúcs ← s.levesz() // ha a jobb oldali gyermek létezik és a // bejárás éppen a bal oldali gyermek felől történik ha (aktuálisCsúcs.jobb ≠ null és utolsóCsúcsFeldolgoz ≠ aktuálisCsúcs.jobb) csúcs ← aktuálisCsúcs.jobb egyébként feldolgoz(aktuálisCsúcs) utolsóFeldolgozottCsúcs ← s.levesz() |
A fenti implementációk mindegyikéhez a fa mélységének megfelelő méretű verem szükséges. (Rekurziónál ez a hívásverem, iteratív algoritmusoknál tetszőleges verem). A rosszul kiegyensúlyozott fák esetében ez számottevő nagyságú lehet. Iteratív implementációknál kikerülhetjük a verem használatát úgy, hogy nyilvántartunk egy szülő csúcsra mutató hivatkozást minden egyes csúcsban, vagy a fűzött fát alkalmazunk (következő bekezdés).
Morris in-order bejárás fűzött fában
Fűzött bináris fában minden olyan bal oldali (gyermekre mutató) hivatkozás, amely egyébként null lenne, az in-order sorrend szerinti előző csúcsra mutat (ha létezik), és minden jobb oldali hivatkozás, amely egyébként null lenne, az in-order sorrend szerinti következő csúcsra mutat (ha létezik).
Előnyök:
- Futásidő-, és memóriaigényes rekurzió elkerülése. Nem szükséges hívásverem.
- A csúcsnak van hivatkozása a szülő csúcsára.
Hátrányok:
- A fa bonyolultabb.
- Egy időben csak egy bejárás hajtható végre.
- Hajlamosabb hibára, amikor egy gyermek csúcs sincs, és mindkét hivatkozás a szülő csúcsra mutat.
A Morris-bejárás egy fűzött fabejárás implementációja in-order sorrendben: [6]
- Hivatkozások létrehozása az in-order sorrend szerinti következő csúcsra.
- Adatok kiírása ezen hivatkozások segítségével.
- Változtatások visszavonása, az eredeti fa visszaállítása.
Szélességi bejárás
Az alábbi egy egyszerű soron alapuló szint-sorrendű bejárás pszeudokódja, ami az adott mélységben a csúcsok maximális számának megfelelő helyet igényel. A mélység legfeljebb a csúcsok száma / 2 lehet. Az ilyen típusú bejárás helytakarékosabban implementálható iteratív mélyülő mélységi bejárás használatával.
szintSorrend(gyökér) q ← üres sor q.sorba(gyökér) amíg nem q.Üres() csináld csúcs ← q.sorból() feldolgoz(csúcs) ha csúcs.bal ≠ null akkor q.sorba(csúcs.bal) ha csúcs.jobb ≠ null akkor q.sorba(csúcs.jobb)
Végtelen fák
Bár a bejárást általában a véges számú csúcsokkal (és ennélfogva a véges mélységgel és véges elágazási faktorral) végezzük, ezt a végtelen fák esetében is megtehetjük. Ez nagy jelentőséggel bír a funkcionális programozásban (különösen lusta kiértékelésnél), mivel bár a végtelen adatstruktúrák gyakran könnyen meghatározhatók és felhasználhatók, a kiértékelésük végtelen időt igényel. Léteznek véges fák, amelyek túl nagyok az explicit reprezentációhoz, mint például a sakk vagy a go játék fája, így ezeket érdemes végtelenként elemezni.
A bejárás alapvető követelménye, hogy minden csúcs feldolgozásra kerüljön. Végtelen fák esetében az egyszerű algoritmusok ezt gyakran nem teljesítik. Például egy végtelen mélységű bináris fa esetén a mélységi keresés a fa egyik oldalán (konvencionálisan a bal oldalon) halad lefelé, egyetlen levélcsúcsot sem érint (nem is fog), és soha nem dolgozza fel meg a többi csúcsot. Ezzel szemben a szélességi (szint-sorrendű) bejárás gond nélkül bejár (végtelen idő alatt) egy végtelen mélységű bináris, vagy bármilyen véges elágazási faktorral rendelkező fát.
Ezzel szemben, tekintve egy 2 mélységű fát, ahol a gyökér csúcsnak végtelen sok gyermeke van, és ezeknek a gyerekeknek két gyermeke van, a mélységi keresés minden csúcsot feldolgoz, mivel amint egy gyermek csúcs összes gyermek csúcsa feldolgozásra kerül, a következőre gyermek csúcsra lép (feltételezve, hogy a bejárás nem post-order, amely esetben a gyökér csúcs sosem kerül feldolgozásra). A szélességi keresés viszont soha nem fogja elérni az unokákat, mivel a gyermek csúcsok feldolgozásán nem jut túl.
Pontosabb futásidő-analízis végtelen sorszámok segítségével végezhető; Például a fenti 2 mélységű fa szélességi bejárása ω · 2 lépésből áll: ω az első szint, majd még egyszer ω a második szint.
Tehát az egyszerű mélységi vagy szélességi bejárások nem haladnak végig a teljes végtelen fán, és így nem is hatékonyak nagyon nagy fák esetében. A kevert módszerek azonban bármely (megszámlálhatóan) végtelen fát bejárnak, lényegében átlós irányban ("átlós" - mélységi és szélességi kombinációja).
Vegyünk egy végtelen számú elágazással rendelkező, végtelen mélységű fát. A gyökércsúcs jelölése legyen (), a gyermekcsúcsoké (1), (2), …, ezek gyermekeié, (1, 1), (1, 2), …, (2, 1), (2, 2), …, stb. A csúcsok így kölcsönösen egyértelműen megfeleltethetők pozitív egész számokból álló (vagy üres) sorozatoknak. Ezek megszámlálhatók, és sorba rendezhetők elemeik összege, ezen belül lexikográfiai sorrend szerint (véges számú sorozat elem-összege egy adott összeg, így minden bejegyzés elérhető – formálisan: egy adott természetes számnak véges számú (2n – 1, n ≥ 1) kompozíciója létezik). Ez meghatároz egy bejárást:
0: () 1: (1) 2: (1, 1) (2) 3: (1, 1, 1) (1, 2) (2, 1) (3) 4: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) (4)
stb.
Ezt értelmezhetjük egy végtelen mélységű bináris fa leképezéseként, melyen szélességi bejárást végezhetünk: cseréljük ki a szülő csúcs „lefelé” menő éleit a második és további gyermek-csúcsainak esetében „jobbra” menő élekre az első gyermektől a másodikig, a másodiktól a harmadikig, stb. Így minden lépésnél indulhatunk lefelé ( (, 1)-et a sorozat végére beszúrva), vagy jobbra (hozzáadva egyet az utolsó számhoz) (kivéve a gyökércsúcsot, ahonnan csak lefelé indulhatunk). A végtelen bináris fa egy csúcsa és egy sorozat így a következőképpen feleltethető meg: a sorozat elemeinek összege mínusz egy megegyezik a csúcs gyökértől való távolságával, és megegyezik a végtelen bináris fában az n - 1 mélységben lévő 2n−1 számú csúccsal (2 a fa bináris tulajdonsága miatt).
Jegyzetek
- ↑ Lecture 8, Tree Traversal. (Hozzáférés: 2015. május 2.)
- ↑ http://www.cise.ufl.edu/~sahni/cop3530/slides/lec216.pdf
- ↑ Preorder Traversal Algorithm. (Hozzáférés: 2015. május 2.)
- ↑ Wittman: Tree Traversal (PDF). UCLA Math. [2015. február 13-i dátummal az eredetiből archiválva]. (Hozzáférés: 2016. január 2.)
- ↑ Algorithms, Which combinations of pre-, post- and in-order sequentialisation are unique?, Computer Science Stack Exchange. (Hozzáférés: 2015. május 2.)
- ↑ Morris (1979). „Traversing binary trees simply and cheaply”. Information Processing Letters 9 (5). DOI:10.1016/0020-0190(79)90068-1.
Irodalom
- Dale, Nell. Lilly, Susan D. "Pascal Plus Data Structures". D. C. Heath and Company. Lexington, MA. 1995. Fourth Edition.
- Drozdek, Adam. "Data Structures and Algorithms in C++". Brook/Cole. Pacific Grove, CA. 2001. Second edition.
- http://www.math.northwestern.edu/~mlerma/courses/cs310-05s/notes/dm-treetran
Fordítás
Ez a szócikk részben vagy egészben a Tree traversal című angol Wikipédia-szócikk ezen változatának fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.
További információk
- Storing Hierarchical Data in a Database with traversal examples in PHP
- Managing Hierarchical Data in MySQL
- Working with Graphs in MySQL
- Sample code for recursive and iterative tree traversal implemented in C.
- Sample code for recursive tree traversal in C#.
- See tree traversal implemented in various programming language on Rosetta Code
- Tree traversal without recursion