Lidt om træer - med en implementation i C#

Tags:    c#
<< < 123 > >>
Skrevet af Bruger #4522 @ 20.12.2008

Iteration gennem et binært træ


Når vi arbejder med lineære strukturer, som for eksempel en liste eller tabel (også kaldet array), så er der få åbenlyse iterationsmuligheder. Vi kan for eksempel starte med listens første element og så besøge alle elementerne i den rækkefølge de holdes i i listen. Vi kan også starte med listens sidste element og iterere den modsatte vej.

Nu og her arbejder vi dog ikke med lister eller tabeller, men med træer, og her er der ikke umiddelbart nogen åbenlyse iterationsmuligheder. Men hvis vi lægger hovedet i blød, og kigger lidt på træstrukturen, så kan vi godt finde frem til fire konsistente måder hvorpå vi kan iterere gennem strukturen på.

Lad os kigge på disse fire iterationsmuligheder. Udtrykstræet fra afsnittet "Hvorfor?" bruges til at illustrere de forskellige iterationer.

Preorder
I denne metoder besøges et element efterfulgt af elementerne i dets venstre undertræ. Vores udtrykstræs elementer bliver derfor besøgt i følgende rækkefølge: =, a, +, b, *, +, c, 1, 4.

Inorder
I denne metode besgøes først et elements venstre undertræ, derefter selve elementet, og derefter elementets højre undertræ. Vores udtrykstræs ellementer bliver besøgt i følgende rækkefølge: a, =, b, +, c, +, 1, *, 4. Dette svarer til det oprindelige udtryk blot uden paranteser. Dette er måden hvorpå man vil indtaste udtrykket på de fleste standard lommeregnere (blot skal man selvfølelig lige huske paranteserne).

Postorder
Her starter vi med at besøge et elements venstre undertræ, efterfulgt af dets højre undertræ, og til sidst selve elementet. I vores eksempel bliver det til følgende rækkefølge: a, b, c, 1, +, 4, *, +, =. Dette er måden hvorpå man vil indtaste vores oprindelig udtryk på en såkaldt RPN (reverse polisn notation) lommeregner som især kendes hos HPs lommeregnere.

Levelorder
I denne metoder besøges alle elementer på niveau i først, og derefter besøger vi elementerne på niveaui + 1. I vores eksempel bliver det derfor til følgende rækkefølge: =, a, +, b, *, +, 4, c, 1.

Vi vil nu kode de tre første iterationsmetoder (den fjerde følger nemt heraf, og lades være op til læseren).
For at iterere gennem vores træ kalder beder vi bare om en iterator fra træets rod. På lignende vis kan man starte en iteration fra et hvilket som helst undertræ.

Da vores træ implementerer IEnumerable skal vi implementere en GetEnumerator metode. Da vi ender med fire forskellige iteratorer må vi i denne metode træffe et valg for hvilken iterator der skal returneres. Derudoer vil vi tilføje fire specifikke metoder der kan bruges til at få fat i en iterator af eget valgt.

Vi starter med at kode preorder enumereringen.

Vi skal tilføje følgende metode til træ-klassen:

Fold kodeboks ind/udCSharp kode 


Og til element-klassen:
Fold kodeboks ind/udCSharp kode 

Koden er her:

Fold kodeboks ind/udCSharp kode 


Læg mærke til, at jeg har implementeret Dispose() eksplicit, så den kan gemmes fra den offentlige grænseflade, da vi ikke har bruge denne metode her.

Køretidskompleksiteten er O(n), da hvert element bliver lagt på og fjernet fra stakken én gang.

Lad os så kigge på implementeringen af inorder iterationen. Igen skal vi lige tilføje en metode til BinaryTree klassen:

Fold kodeboks ind/udCSharp kode 


Og i klassen BinaryTreeNode<T> tilføjer vi følgende metode:

Fold kodeboks ind/udCSharp kode 


Selve InOrderEnumerator<T> kommer her:

Fold kodeboks ind/udCSharp kode 


På grund af den måde inorder iterationen fungerer på, er vi tvunget til at starte med (i vores Reset metode) at putte alle elementerne fra roden og ned til dens venstremest afkom på stakken. Herfra arbejder vi os gennem træet, ved at undersøge om den nuværende knude har et højre undertræ (som ikke er blevet besøgt endnu), og såfremt det er tilfældet puttes det på stakken. Hvis ikke, bevæger vi os videre gennem træet ved at poppe et element af stakken.
Inorder er én af de mest almindelige metoder til at iterere gennem et træ på, hvorfor det er et godt bud på hvad der returneres af GetEnumerator. Jeg har dog en forkærlighed for RPN og derfor vælger jeg at lade GetEnumerator returnere en postorder enumerator. Derfor ændres BinaryTreeNode<T>-klassens metod GetEnumerator() til:

Fold kodeboks ind/udCSharp kode 


Vi tilføjer dog også følgende til samme klasse:

Fold kodeboks ind/udCSharp kode 


Til klassen BinaryTree tilføjes:

Fold kodeboks ind/udCSharp kode 


Og her kommer så selve enuemratoren:

Fold kodeboks ind/udCSharp kode 


Her indeholder vores stak de elementer som er forældre til de elementer som vi på et givet tidspunkt er ved at undersøge. Ellers er koden ligetil.

Jeg vil lade implementatioen af "level order" enumeratoren være op til læseren.

Det skal også bemærkes, at da træer er rekursive strukturer kan disse enuemratorer også implementeres ved brug af rekursion.

Tilføjelser


Vi har nu en fungerende implementation af et binært træ. Men der er masser at ekstra ting vi kunne tilføje. Hvad med f.eks. en metode de returnerer dybden af et element i træet? Eller en metoder der kan fortælle en om der er tale om et fuldt træ? Der er masse af muligheder.


Litteratur og videre læsning

Wikipedia har en god artikel om de forskellige iterationsmetoder der er i et træ.

En klassiker om datastrukturer og algoritmer er "Introduction to Algorithms" af Cormen et al. Derudover har Baileys "Java Structures" nogle spændende kapitler, bogen har dog nogle år på bagen hvorfor Java- koden ikke følger de moderne Java SDKs og idiomer.



<< < 123 > >>

Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (4)

User
Bruger #5620 @ 20.12.08 23:06
Nu har jeg godt nok aldrig kodet i C# før men den kode der virker på som alt for lang til en implementation af binær træer.

Og så bliver jeg lige nød til at spørge hvordan den der insert skulle virke for hvis jeg kaldte insert 2 gange på et tomt træ ville jeg efter min overbevisning få en exception smidt i hovedet, eller forestille du dig at man selv skulle sidde og rykke rundt med move funktionerne til man fandt en node med frie børn?
User
Bruger #4522 @ 21.12.08 00:07
Hej Nørden.

Insert indsætter en knude i et tomt træ. Man flytter så markøren ud af træet v.h.a. MoveLeft og MoveRight, og kalder da Insert.

User
Bruger #5620 @ 21.12.08 09:10
Måske ville det være på sin plads med et eller flere kode eksempler på hvordan du forestiller dig man rent faktisk brugte din kode.
Tror ikke at jeg er den eneste der ville få problemer med at bruge det :).

Du kunne f.eks lave eksempler på hvordan man bygger træet, sletter i det og bruger en eller flere af de der iteratorer, men det bare et forslag.

PS så synes jeg det en god artikel, men hvis du kan rette i den burde du evt, lave en eller flere(en per fil) bokse med all koden i tilsidst, ville gøre det lettere at kopier den.

User
Bruger #4522 @ 21.12.08 12:22
Hej Nørden.

Det har du helt ret i. Det gik op for mig da jeg læste din kommentar at jeg skulle have tilføjet et eksempel på brug af koden. En åbenlys mangel. Jeg vil opdatere artiklen med et eksempel inden længe.

Tak for kommentarene.
Du skal være logget ind for at skrive en kommentar.
t