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

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

Hvorfor?


Før vi begynder at kigge på noget C# kode, er der et spørgsmål der trænger sig på. Hvorfor overhovedet bruge træer? C#s collection-bibliotek indeholder mange forskellige datastrukturer som lister, stakker, køer, hashsæt og "ordbøger" (dictionaries). Så er træer egentlig nødvendige? Er .NET Frameworket ikke nok?

I de fleste tilfælde: Jo. For de fleste programmeringsopgaver behøver du ikke træer; men der er nogle opgaver hvor et træ er den naturlig løsning. Hvis du f.eks. skal manipulere hierarkisk data, eller skal holde data på en måde så det bliver hurtigt at gennemsøge, er træer det oplagte valg. Derudover er træer en klassisk datastruktur i datalogi som enhver programmør bør have bekendtskab med.

Et klassisk eksempel på brugen af et træ er et såkaldt udtrykstræ som bruges til holde og evaluere et udtryk, f.eks. et C# udtryk. Tag for eksempel udtrykket "a = b + (c + 1) * 4;". Dette udtryk kan nemt repræsenteres og evalueres ved at bruge et træ. Se følgende illustration.



I ovenstående illustration er udtrykket opskrevet som et udtrykstræ. Udtrykket kan så evalueres ved at starte nedefra.

C# implementationen


Nu skal vi til at kigge på noget kode.

[Note: Denne del af artiklen er skrevet mens koden blev udviklet, og indeholder altså nogle af de tanker som jeg gjorde mig i løbet af denne proces. Derfor nævner jeg nogle af de ideer jeg fik undervejs, hvoraf nogle ikke blev til noget overhovedet, og andre endte med at blive virkeliggjort i koden.]

Min første idé er at lave et interface for et træ. Der findes forskellige typer af binære træer i datalogi (det vi har kigget på indtil nu er et standard binært træ), og tanken er at man kan bruge dette interface til at navigere rundt i forskellige typer af binære træer. En anden vigtig ting er navigationen rundt i træet. Da det er en datastruktur vi arbejder med vil det være naturligt at bruge den form for navigation som er indbygget i .NET frameworket. Her tænker jeg på at vi f.eks. kunne implementere IEnumerable interfacet, eller måske bare bruge iterators der så ordner det hele bag kulisserne. På den anden side er navigationen af et træ helt anderledes, end det er tilfældet ved en standard lineær datastruktur, så det kan være at det slet ikke passer til vores tilfælde. Lad os se hvad vi ender med, men nu er vi i hvert fald opmærksom på det. Under alle omstændigheder skal vi kunne navigere rundt i træet med en slags markør.

Derudover ønsker jeg at gøre træet generisk.

Mit forslag til et interface er:
Fold kodeboks ind/udCSharp kode 


Det er altid en smagssag hvad der skal være en metode og hvad der skal være en property. Der er dog nogle grundregler. For eksempel skal man være opmærksom på at get-properties udføres på uforudsigelige tidspunkter (f.eks. ved debugging), hvorfor de som grundregel ikke må være skyld i store såkaldte "side-effects". Derudover skal en property ikke være beregningsmæssig krævende.

Jeg har valgt at implementere IEnumerable<T> interfacet. Jeg vil til at starte med se om det kan implementeres med en simpel iterator, hvis det ikke er muligt vil jeg lave min egen IEnumerator<T> implementation.

Når et BinaryTree<T>-objekt oprettes er det tomt - uden nogle knuder. En knude indsættes i træet ved at kalde metoden Insert. Ved at bruge IsValid kan en bruger tjekke om markøren peger på en gyldig knude; såfremt det er tilfældet kan knudens data (værdi) hentes og sættes med Value-egenskaben.

Navigation rundt i træet sker ved at kalde MoveLeft, MoveRight og MoveUp. For at starte forfra, dvs. for at placere markøren ved roden, kaldes Reset-metoden.

Repræsentation af elementerne


I implementationen af vores binære træ, er ideen at have en reference til dens rod. Herfra har vi så adgang til dens data og dens relaterede elementer (børn og forældre [som i rodets tilfælde vil være tom]). Generelt repræsenterer vi et undertræ som en reference til dens rod, hvorfra vi så kan få fat i dens data og andre elementer.

Vi har derfor også brug for en klasse der kan bruges til at repræsentere et binært element. Umiddelbart vil jeg tro at sådanne en element-klasse kan bruges i de forskellige repræsentationer man lave af binære træer, og jeg tror ikke at et interface vil være nødvendigt her. Så vi kan "nøjes" med at lave en generisk klasse kaldet BinaryTreeNode<T>. Her kommer koden, og bagefter går jeg den igennem.

Fold kodeboks ind/udCSharp kode 


Det meste af koden er ligetil. Der er to konstruktører. Den ene bruges til at sætte både den data der gemmes i elementet samt dens børn. Den anden gemmer kun data i elementet, mens dens børn og dens forælder sættes til null.

I koden der sætter både venstre og højre barn, sikrer vi at de nye børns referencer til deres forælder er sat rigtigt; dette er meget vigtigt for at bevare træets konsistens. Derudover sørger vi for at et eventuelt tidligere barns forælder reference sættes til null (hvilket betyder at vi fjerner et enkelt element, eller muligvis et helt undertræ fra træet).

Jeg har valgt også at implementere IEnumerable<T>. Idden er at man kan starte en iteration gennem et undertræs elemtner ved at kalde GetEnumerator() på undertræets rod. Indtil videre kastes blot en System.NotImplementedException, men jeg vender tilbage til det senere i artiklen.

Implementation af BinaryTree<T>


Nu hvor vi har vores implementation af elementerne færdig (på nær vores "enumerators"), kan vi gå igang med at kode en klasse, som jeg vil kalde BinaryTree<T>, der implementerer vores IBinaryTree<T>-interface.

Det er gennem denne klasse at man arbejder med træet. Dens forskellige metoder gør dette ved at manipulere tre datamedlemmer af typen BinaryTreeeNode<T>: en der repræsenterer træets rod, en der repræsenterer den nuværende knude, dvs. den knude som man er nået til under en "gennemgang" af træet (dvs. vores markør), og til sidst en reference til den knude som markøren pegede på før den eventuelt vandrede ud af træet. Disse tre datamedlemmer er helt centrale i implementationen af træet. Her følger koden:

Fold kodeboks ind/udCSharp kode 


Endnu et vigtigt datamedlem er wentLeft som er en boolsk variabel som sættes til sannd såfremt markøren "vandrede" væk fra træet ved at gå til venstre. På denne måde kan vi i Insert(T value) finde ud af om den nye knude skal være et venstre eller højre barn.

I konstruktøren kalder vi Clear() som "nulsætter" hele træet. Bemærk at der er forskel på Clear() og Reset(). Hvor den første sletter hele træet og fjerner alle dets knude så et nyt kan opbygges, så sletter Reset() intet, men flytter blot markøren tilbage til roden.

I Remove() fjerner vi den bladknude som markøren peger på. Hvis markøren ikke peger på en bladknude kastes en NotSupportedException.

Metoderne MoveUp, MoveLeft og MoveRight bruges til at flytte markøren rundt i træet. Koden i metoderne sørger for at datamedlemmerne er korrekt sat.

De to GetEnumerator metoder kalder blot samme metode på træets rod (som jo er af typen BinaryTreeNode<T> der også implementerer IEnumerable<T>-interface).

Resten af koden, som er properties, er ligetil og bruges til at undersøge træets topologi.

Nu er det tid til at kigge på iteration gennem træet. Vi skal implementere en enumerator. Faktisk skal vi implementere flere da der er flere måder hvorpå man kan iterere gennem et binært træ. Så lad os kigge på det.




<< < 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