• No results found

Föreläsning 13 och 14: Binära träd

N/A
N/A
Protected

Academic year: 2022

Share "Föreläsning 13 och 14: Binära träd"

Copied!
6
0
0

Loading.... (view fulltext now)

Full text

(1)

Föreläsning 13 och 14: Binära träd

o Binärträd och allmänna träd o Rekursiva tankar för binärträd o Binära sökträd

Binärträd och allmänna träd

Stack och kö är två viktiga datastrukturer man kan bygga av poster, där varje post pekar på en annan post. I kommande föreläsningar kommer vi att bekanta oss med släktträd som kan byggas med en enda pekare i varje post (faderspekaren).

Med två pekare i varje post kan man emellertid bygga mer intressanta träd, till exempel ett som beskriver en symfoniorkesters sammansättning. Här har posterna följande utseende.

class Node(object):

def __init__(self,word):

self.word=word self.down=None self.right=None

All systematisk uppdelning kan beskrivas med liknande träd, till exempel ett bokverks uppdelning i delar, kapitel, sektioner osv.

När man talar om binärträd brukar man tänka på en lite annorlunda bild. Vi antar att posterna har följande utseende.

class Node(object):

def __init__(self,tal):

self.tal=tal self.left=None self.right=None

(2)

Högst upp finns konstigt nog trädets root och dit har man alltid en pekare root.

Antalet nivåer i trädet avgör hur många poster det kan innehålla. Ett fullt träd med k nivåer innehåller (2k -1) poster. Exempel:

k=3 i vår bild ger högst 7 poster (det finns plats för två poster till under 9999). Man kan också säga att ett balanserat träd med N poster har cirka log2N nivåer.

Rekursiva tankar för binärträd

Dom flesta problem i samband med binärträd löser man enklast med en rekursiv tanke.

Fråga: Hur många poster finns det i trädet?

Rekursivt svar: Antalet poster i vänsterträdet plus antalet poster i högerträdet plus 1.

Men ett tomt träd har 0 poster.

Följande funktion (hos trädet) gör att antal(root) blir 5 för vårt träd.

def antal(p):

if p == None:

return 0

return antal(p.left) + antal(p.right) + 1

Om man ska skriva ut alla talen i trädet vill man oftast göra det i så kallad inordning (eng. inorder), d.v.s. vänster till höger.

Fråga: Hur skriver man ut trädet i inordning?

Rekursivt svar: Först skriver man ut vänsterträdet, sedan rot-talet, sist högerträdet.

Men ett tomt träd skriver man inte alls.

Följande funktion gör att write(root) skriver ut 1 17 666 4711 9999 för vårt träd.

def writeInOrder(p):

if p != None:

writeInOrder(p.left) print (p.tal,end=” ”) writeInOrder(p.right)

Om man kastar om de tre sista satserna får man ändå ut alla talen på skärmen men i andra ordningar. Preordning (eng. preorder) innebär att rot-talet skrivs först, sedan vänsterträdet och sist högerträdet. I vårt exempel blir ordningen 4711 17 1 666 9999.

Om vi återgår till orkesterträdet kan vi se att preordning faktiskt ger vettigare utskrift. Så här blir koden i det fallet.

writePreOrder(p):

if p != None:

print(p.word,end=” ”) writePreOrder(p.down) writePreOrder(p.right)

(3)

Utskriften blir då den naturliga. Om vi för tydlighets skull använder indragning av orden på lägre nivå blir utskriften:

Orkester Blås

Trä Bleck Stråk Vi Va Vc Kb Slag

Slutligen kan man skriva ut i postordning (eng. postorder) och det innebär att

vänsterträdet skrivs först, sedan högerträdet och sist roten. Det ger 1 666 17 9999 4711 i vårt exempel.

Binära sökträd

I vårt exempelträd ligger små tal till vänster och stora tal till höger. När det är på det sättet har man ett binärt sökträd, så kallat eftersom det går så snabbt att söka reda på en post i trädet. Låt oss säga att vi söker efter 666. Vår algoritm blir följande

Kolla först rot-talet.

Om talet är 666 har vi funnit vad vi söker.

Om talet är större än 666 letar vi vidare efter 666 i vänsterträdet.

Om det är mindre än 666 letar vi vidare i högerträdet. Men om vi når en None finns inte 666 i sökträdet.

Det här är ju nästan precis samma sak som binär sökning i en lista. I båda fallen blir antalet jämförelser cirka logN. Men binärträdet har två stora fördelar.

Insättning av ny post kräver bara logN jämförelser mot N/2 för insortering i en vektor.

Trädet kan bli hur stort som helst men vektorns storlek bestäms vid skapandet.

Enda problemet med binärträd är att de kan bli obalanserade, och då försvinner den snabba sökningen. Ett riktigt obalanserat sökträd med dom fem talen i exemplet har 1 i roten, 17 till höger, 666 till höger om det osv. Det blir bara en så kallad tarm. I vissa fall har binärträd en tendens att bli obalanserade med tiden, till exempel när nyfödda förs in i ett träd sorterat på personnummer och då alltid hamnar längst till höger.

Sökning i binärträd

All sökning utgår från roten och man kan tänka sej att man först sätter pekfingret på rot-talet. Det sökta talet jämförs med pekfingertalet och då är tre fall möjliga:

Om det sökta talet är mindre flyttar man pekfingret ner åt vänster.

Om det sökta talet är större flyttar man pekfingret ner åt höger.

Om talen är lika har man funnit vad man sökte.

Om man med satsen if exists(17)... vill kunna kolla om talet 17 finns i trädet kan man programmera exists() så här:

def exists(value):

# Börja titta från rotnoden...

return finns(value, root)

(4)

def finns(value, r):

while r != None:

if value < r.value:

r = r.left

elif value > r.value:

r = r.right else:

return True

return False

Om trädet är balanserat blir det inte så många tal man behöver jämföra med innan man kommit längst ner i trädet. Med N poster i trädet blir det cirka logN jämförelser.

Insättning i binärträd

Insättning av ett nytt tal i trädet går egentligen till på samma sätt som sökningen. Man söker efter talet och om man finner det skriver man bara ut att det är en dubblett. Men om man längst ner i trädet utan att ha funnit talet, d.v.s. man står vid en None-referens, skapar man en ny post med det nya talet i och låter den forna None-referensen referera till den nya posten i stället.

Ett avsnitt ur koden för put() ska alltså se ut så här:

if r == None:

r = BinNode(value) return r

Där BinNode är en nod anpassad för binärträd, d.v.s. den har en plats för ett värde och två pekare till eventuella vänster och höger barn. Här returnerar metoden en pekare till den nya posten. Om trädet är tomt tidigare ska pekaren root peka på den nya ensamma posten, därför måste det någonstans finnas ett anrop

root = insert(talet, root);

Hela put() blir:

def put(value, r):

if r == None:

r = BinNode(value) return r

elif value < r.value:

r.left = put(value, r.left) elif value > r.value:

r.right = put(value, r.right) else:

print ("Dubblett: ", value)

return r

Funktionen returnerar en referens till ett objekt av BinNode, nämligen en referens till topp noden (roten) i det träd där det nya talet har stoppats in.

Abstrakta datatstrukturer

Vår binärträdsklass blev så bra att vi vill låta alla andra klasser använda den. Vår lilla klass TestTree kan till exempel göra anrop av typen

tree = BinTree()

(5)

tree.insert(17) tree.write()

n = tree.nElements() if tree.exists(4711)):

...

Notera att referensen root inte förekommer och att man inte från huvudprogrammet kan avläsa att det finns poster med left- och rightreferenser eller liknande. Man säger att binärträdet är abstrakt, och det är så man bör programmera.

Tyvärr i Python finns inget enkelt sätt som gör att man kan hindra slutanvändaren att komma åt rootpekaren eller vänstra och högra delträdet (d.v.s. den interna datastrukturen som egentligen endast ska användas av konstruktören). I Java finns nyckelorden private och public, konstruktören kan bestämma vilka attribut och funktioner absolut inte användas av slutanvändaren d.v.s. de är interna och privata för datastrukturen och vilka anda funktioner och attributen får användas av slutanvändaren.

Men detta ska inte sätta stop för oss för en bra programmeringsstil. Vi antar att när användaren vill använda funktionen insert för att lägga till ett värde i binäraträdet då anropas en annan funktion som är dold för användaren som gör hela jobbet. Då skapar vi en funktion som heter __put och det är tänkt att användaren ska inte ha någon aning om att funktionen överhuvudtaget är definierat. Alltså användaren använder funktionen insert och datastrukturen använder funktionen __put och rootpekaren för att lägga till något i datastrukturen.Detta gäller också för funktioner write, skriv,

exists, __finns, nElements och __antalElementer.

class BinTree(object):

def __init__(self):

self.root=None

def exists(self,value):

# Börja titta från rotnoden...

return self.__finns(value, self.root)

#iterativ funktion

def __finns(self, value, r):

while r!=None :

if value < r.value:

r = r.left

elif value > r.value:

r = r.right else:

return True

return False

def insert(self,value):

self.root = self.__put(value, self.root)

#rekursiv funktion

def __put(self,value, p):

if p == None:

p = BinNode(value) return p

if value < p.value:

p.left = self.__put(value, p.left) elif value > p.value:

p.right = self.__put(value, p.right) else:

(6)

print("Dubblett: " , value) return p

def write(self):

self.__skriv(self.root) #rekursiv funktion

def __skriv(self, r):

if r == None:

return

self.__skriv(r.left) print(r.value)

self.__skriv(r.right)

def nElements(self):

return self.__antalElementer(self.root)

#rekursiv funktion

def __antalElementer(self,p):

if p == None:

return 0;

return 1+self.__antalElementer(p.left)+self.__antalElementer(p.right);

class BinNode:

def __init__(self, v):

self.value = v self.left=None self.right=None

Testprogrammet kan se ut som följer:

import sys

from BinTree import * tree = BinTree()

tal=input("Ange ett tal: ") while tal!="":

tree.insert(int(tal))

tal=input("Ange ett tal (avsluta ett Enter-slag):") tree.write()

print (tree.nElements(), " poster")

if tree.exists(int(input("Sök efter ett tal: "))):

print ("finns") else:

print ("Fanns!")

Spara binärträd

Om man under körningen bygger upp ett bra binärträd vill man troligen spara trädet på fil till nästa körning. Den metod som ligger närmast till hands är att använda

skriv(), men istället att skriva ut på skärmen så kan man skriva ut på en fil. Tyvärr kommer då trädposterna att ligga i ordning på filen, och det betyder att man får en långtarm ner åt höger när man senare bygger upp ett nytt träd från filen. Det riktiga är att skriva ut trädet i preordning på filen. Då kommer rotposten ut först på filen och blir därmed vid senare inläsning av rotpost även i det nybyggda trädet. Några sekunders eftertanke övertygar en om att hela trädet kommer att bli så som det var vid förra körningen.

References

Related documents

Redan idag produceras biogas från avfall som räcker till årsför- brukningen för 12 000 bilar.. Hushållens ansträngningar att sortera ut matavfall har alltså

På grund av det låga antalet individer och den korta uppföljningen kan detta dock inte tas som bevis för att simulatorn är ett tillräckligt känsligt instrument för att fånga

En undersökning i Adelaide visar att 31 % av fotgängarna kände sig osäkra när de delar gångväg med elsparkcyklister (större andel ju äldre fotgängare), och 29 % av

Utredningen konstaterar att nästan var femte cyklist i ett cykelfält som passerar en buss i anslutning till en busshållplats är inblandad i en interaktion där samspelet mellan

Efter Krimockupationen 2015 har säkerhet både vad avser yttre och inre hot ånyo börjat uppmärksammas i Sverige.. Det gamla totalförsvaret tog lång tid att demontera och det blir

Inspektionen för arbetslöshetsförsäkringen (IAF) har granskat förslagen i slutbetänkandet huvudsakligen utifrån myndighetens uppdrag att arbeta för att

• Klasser som är till hjälp för implemen- tationen bör ärvas private. • Man vill inte ha implementationsdetaljer i

• Konstruktor som är private ger möjlighet att förhindra användandet av t.ex. med delete eller då det hamnar utom räckvidd) anropas