Logikprogrammering 21/10 Binära träd David Hjelm
Hur söker man i stora mängder data? Antag att vi har en lista med alla människor som bor i Sverige, nästan nio miljoner. Vi vill se om Anatolij Karpov finns i listan. För att testa detta använder vi oss av predikatet member/2. Hur lång tid tar detta? Det enda sättet att se om ett element finns i en lista är att börja från vänster och successivt kolla varje element. Alltså: Om Anatolij Karpov inte finns med i listan är vi tvungna att kolla vart och ett av de nio miljonerna namn i listan. Om Anatolij Karpov finns med i listan så måste vi ändå kolla i genomsnitt 4,5 miljoner namn.
Hur gör människor när de söker? Men, om man i t.ex. en telefonkatalog ska leta efter Karpov så är det ju dumt att börja från början. Det är vettigare att slå upp telefonkatalogen någonstans på mitten. Om man letar efter Karpov så kan jämföra namnen med bokstaven K för att se vilken halva av telefonkatalogen man ska fortsätta med. Sedan kan man slå upp den halvan på mitten för att se vilken fjärdedel man ska fortsätta med, och så vidare tills man antingen har hittat Anatolij Karpov eller har hittat (till exempel) Östen Karp och Andrej Karpov utan något namn emellan.
Krav på representationen av data Det finns två anledningar till att man kan göra så med en telefonkatalog. Den första är att namnen är sorterade i bokstavsordning. Den andra är att man direkt kan hitta ett namn någonstans i mitten på katalogen och sedan ett namn någonstans i mitten på en av halvorna, och så vidare. Visserligen kan man sortera namnlistan i bokstavsordning, men man kan inte direkt komma åt ett namn i mitten på listan. Vi måste ordna namnen på något annat sätt än i en lista, med andra ord måste vi ha en annan datastruktur.
Binära träd En lösning är att ha namnen i ett binärt träd. Namnen sparas som noder i trädet. Binära träd är ungefär som listor fast varje nod har två svansar i stället för en. På samma sätt som med listor så finns det två sätt på vilka ett binärt träd kan vara uppbyggt. Antingen så är det tomt. Det tomma trädet skriver vi som atomen nil. Eller så är det sammansatt av en rot (huvud) och två delträd (svansar). Detta skriver vi som den komplexa termen t(Vänster,Rot,Höger) där Vänster och Höger är de båda delträden och Rot är roten. Det är roten som innehåller elementet, och precis som med huvuden på listor kan roten vara vilken sorts term som helst.
Binära träd forts. Detta är ett exempel på ett binärt träd: t(t(nil,pär,t(nil,åsa,nil)),eva,t(nil,ada,t(nil,bo,nil))) Eftersom det är lite svårt att se strukturen på träd så kan man istället rita dem. Här är ovanstående träd, med det tomma trädet nil skrivet som * . eva / \ pär ada / \ / \ * åsa * bo / \ / \ * * * *
Binära träd - in/2 Nu kan vi definiera predikatet in/2 som kollar om ett element finns i ett träd. Det fungerar egentligen precis som member/2 för listor, fast eftersom det nu finns två ”svansar” så behövs det två rekursionsfall: in(X,t(_,X,_)). 1. det sökta elementet är roten in(X,t(V,_,_)):- in(X,V). 2. det sökta elementet finns i V in(X,t(_,_,H)):- in(X,H). 3. det sökta elementet finns i H Antingen så är det sökta elementet roten på trädet, eller så finns det i något av delträden. Observera att in(X,nil) aldrig lyckas och alltså fungerar som basfall.
Ordnade binära träd Problemet med det träd vi har sett hittills och predikatet in/2 är att vi inte har vunnit något mot att ha våra namn i en lista. Elementen i trädet är inte ordnade. Alltså måste vi söka igenom hela trädet för att se om ett element finns i det. Om trädet är ordnat, så kan vi räkna ut i vilket delträd vi ska leta. Då måste vi först slå fast vad det innebär att ett träd är ordnat. Det tomma trädet nil är ordnat Ett träd t(V,R,H) är ordnat om alla noderna i V är mindre än R, alla noderna i H är större än R och delträden V och H är ordnade
Ordnade binära träd - in/2 Vi definierar en ny version av in/2 för ordnade träd: in(X, t(_,X,_)). 1. det sökta elementet är roten in(X, t(Left,Y,_)):- 2. om det sökta elementet är mindre än X @<Y, roten - sök i det vänstra delträdet in(X, Left). in(X, t(_,Y,Right)):- 3. om det sökta elementet är större än X @>Y, roten sök i det högra delträdet in(X, Right). Eftersom vi har använt jämförelsepredikaten @< och @> så kan vi jämföra vilka termer som helst och inte bara tal.
Ordnade binära träd forts. Jämförelsepredikaten @< och @> gör att vi till och med kan jämföra på listor, så det nya predikatet in/2 fungerar bra på följande ordnade träd: ?- in([karpov,anatolij],t(nil,[kalin,niklas],t(nil, [karlsson,anders],t(nil,[karlsson,karl],t(nil, [karlström,knut],t(nil,[karp,östen],t(nil,[karpov, anatolij],t(nil,[karpov,andrej],nil)))))))). yes Men hur effektivt är det?
Ordnade binära träd forts. [kalin,niklas] / \ * [karlsson,anders] / \ * [karlsson,karl] / \ * [karlström,knut] / \ * [karp,östen] / \ * [karpov,anatolij] * [karpov,andrej] / \ * * Om vi ritar upp trädet så ser vi att vi ändå nästan måste gå igenom alla element för att hitta [karpov,anatolij]. med in/2.
Ordnade balanserade binära träd Det räcker inte med att trädet är ordnat för att man ska kunna söka effektivt. Man måste ju också, som med en telefonkatalog, direkt kunna komma åt det mittersta elementet. För att detta ska vara möjligt måste trädet vara balanserat. Följande gäller för ett balanserat träd: Det tomma trädet nil är balanserat. Trädet t(V,R,H) är balanserat om delträden V och H är balanserade samt om V och H har nästan lika många element. Med ’nästan lika många’ menas här att det kan få finnas ett mer element i det ena delträdet än det andra men inte fler. Att ett träd är balanserat betyder inte att det automatiskt är ordnat.
Ordnade balanserade binära träd forts. Om vi balanserar vårt träd så ser det istället ut så här: [karlström,knut] / \ [karlsson,anders] [karpov,anatolij] / \ / \ [kalin,niklas] [karlsson,karl] [karp,östen] [karpov,andrej] / \ / \ / \ / \ * * * * * * * * Och nu behövs det bara två jämförelser för att hitta [karpov,anatolij] i trädet.
in/2 för ordnat balanserat binärt träd | ?- in( [karpov,anatolij], t( t( t( nil, [kalin,niklas], nil), [karlsson, anders], t( nil, [karlsson,karl], nil ) ), [karlström,knut], t( t( nil, [karp,östen], nil ), [karpov, anatolij], t( nil, [karpov,andrej], nil ) ) ) ). 1 1 Call: in([karpov,anatolij],t(t(t(nil,[kalin,niklas],nil),[karlsson,anders], t(nil,[karlsson,karl],nil)),[karlström,knut],t(t(nil,[karp,östen],nil),[karpov,anatolij],t(nil,[karpov,andrej],nil)))) ? 2 2 Call: [karpov,anatolij]@<[karlström,knut] ? 2 2 Fail: [karpov,anatolij]@<[karlström,knut] ? 3 2 Call: [karpov,anatolij]@>[karlström,knut] ? 3 2 Exit: [karpov,anatolij]@>[karlström,knut] ? 4 2 Call: in([karpov,anatolij],t(t(nil,[karp,östen],nil),[karpov,anatolij], t(nil,[karpov,andrej],nil))) ? ? 4 2 Exit: in([karpov,anatolij],t(t(nil,[karp,östen],nil),[karpov,anatolij], t(nil,[karpov,andrej],nil))) ? ? 1 1 Exit: in([karpov,anatolij],t(t(t(nil,[kalin,niklas],nil),[karlsson,anders], t(nil,[karlsson,karl],nil)),[karlström,knut],t(t(nil,[karp,östen],nil),[karpov,anatolij],t(nil,[karpov,andrej],nil)))) ? yes
Olika typer av binära träd 1: Oordnat obalanserat binärt träd anna / \ eva linda / \ / \ * my * * / \ * nadja * * 2: Ordnat obalanserat binärt träd my / \ anna nadja / \ / \ * eva * * / \ * linda / \ * * 1: t(t(nil,eva,t(nil,my,t(nil,nadja,nil))),anna,t(nil,linda,nil)) 2: t(t(nil,anna,t(nil,eva,t(nil,linda,nil))),my,t(nil,nadja,nil))
Olika typer av binära träd forts. 3: Oordnat balanserat binärt träd linda / \ anna eva / \ / \ nadja * my * / \ / \ * * * * 4: Ordnat balanserat binärt träd linda / \ eva nadja / \ / \ anna * my * / \ / \ * * * * 3: t(t(t(nil,nadja,nil),anna,nil),linda,t(t(nil,my,nil),eva,nil)) 4: t(t(t(nil,anna,nil),eva,nil),linda,t(t(nil,my,nil),nadja,nil))
Effektivitet Hur många jämförelser krävs det om vi skulle försöka hitta ett namn i ett ordnat balanserat träd som innehåller namnen på alla människor som bor i Sverige, c.a. nio miljoner stycken? Antalet jämförelser är 2-logaritmen av antalet element i trädet. M.a.o. 2X=N där X är antal jämförelser och N är antalet element - i vårt fall 2X=9 000 000. Eller, mer informellt: vid varje jämförelse utesluter vi hälften av vad som är kvar, d.v.s. sökrymden halveras. När den har halverats så många gånger att vi bara har ett element kvar så är vi klara.
Effektivitet forts. Efter en jämförelse har vi 4 500 000 namn kvar Efter två jämförelser har vi 2 250 000 namn kvar Efter tre jämförelser har vi 1 125 000 namn kvar … Efter ungefär 23 jämförelser, har vi bara ett enda namn. Ungefär, därför att det inte alltid kommer att finnas lika många element i vänster och höger delträd. Men eftersom vårt träd är balanserat så blir det aldrig fler än 24. Om vi istället skulle ha hela jordens befolkning, ca. sex miljarder namn i ett träd så skulle det inte ta mer än maximalt 33 jämförelser att hitta ett namn.
Sammanfattning Ett binärt träd är antingen tomt eller bestående av en rot (som innehåller själva informationen) och två delträd. Delträden är också de binära träd. Vi noterar binära träd så här: Det tomma trädet: nil Sammansatt träd : t(V,R,H)där V och H är binära träd och R är roten. exempel: t(t(nil,pär,t(nil,åsa,nil)),eva,t(nil,ada,t(nil,bo,nil)))
Sammanfattning forts. I regel måste ett binärt träd vara ordnat för att det ska vara någon mening med det. Det tomma trädet nil är ordnat Ett träd t(V,R,H) är ordnat om alla noderna i V är mindre än R, alla noderna i H är större än R och delträden V och R är ordnade eva / \ ada pär / \ / \ * bo * åsa / \ / \ * * * * ett ordnat träd: t(t(nil,ada,t(nil,bo,nil)),eva,t(nil,pär,t(nil,åsa,nil)))
Sammanfattning forts. För att man ska kunna söka så effektivt som möjligt i ett binärt träd bör det vara balanserat. Det tomma trädet nil är balanserat. Trädet t(V,R,H) är balanserat om delträden V och H är balanserade samt om V och H har nästan lika många element. Med ’nästan lika många’ menas här att det kan få finnas ett mer element i det ena delträdet än det andra men inte fler.
Sammanfattning forts. in/2 kollar om ett element finns i ett ordnat binärt träd: in(X, t(_,X,_)). 1. det sökta elementet är roten in(X, t(Left,Y,_)):- 2. om det sökta elementet är mindre än X @<Y, roten - sök i det vänstra delträdet in(X, Left). in(X, t(_,Y,Right)):- 3. om det sökta elementet är större än X @>Y, roten sök i det högra delträdet in(X, Right).
På onsdag: Fler predikat på binära träd. In och uthantering