Listor TDDD73 Funktionell och imperativ programmering i Python Föreläsning 7 Peter Dalenius Institutionen för datavetenskap 2014-09-23
Översikt Ytterligare exempel på dubbelrekursion Inbyggda metoder hos listor Exempel som använder listmetoder Tupler (konstanta listor) Dictionaries Binära sökträd 2
Ytterligare exempel på dubbelrekursion Vi vill ha en funktion remove som kan ta bort alla förekomster av ett element ur en lista. Listan kan innehålla underlistor, och dessa ska också gås igenom. Lösningen ska egentligen bygga upp en ny lista utan det givna elementet, snarare än att fysiskt plocka bort det. Vi tänker oss i första hand en rekursiv lösning och gör följande fallanalys. Hur kan man tänka sig att olika listor börjar och hur ska vi behandla dem? Tom lista Första elementet är en underlista Första elementet är det vi ska ta bort Första elementet är något annat Tom lista Bearbeta både underlistan och resten Fortsätt med resten Bygg ihop och fortsätt med resten 3
Rekursiv lösning def remove(seq, x): if not seq: return [] elif isinstance(seq[0], list): # return [remove(seq[0], x)] + remove(seq[1:], x) # elif seq[0] == x: return remove(seq[1:], x) else: return [seq[0]] + remove(seq[1:], x) 4
Iterativ lösning def remove(seq, x): result = [] for elem in seq: if elem isinstance(elem,!= x: list): # result += [elem] [remove(elem, x)] # elif elem!= x: result += [elem] return result Det är svårt att få till en helt igenom iterativ lösning som klarar av godtyckligt djupa strukturer. 5
Listmetoder >>> s = [4711, 17, 496] >>> s.append(42) >>> s [4711, 17, 496, 42] >>> s.index(17) 1 >>> s.insert(1, 17) >>> s [4711, 17, 17, 496, 42] >>> s.count(17) 2 >>> s.remove(17) >>> s [4711, 17, 496, 42] >>> s.reverse() >>> s [42, 496, 17, 4711] >>> s.sort() >>> s [17, 42, 496, 4711] >>> s.pop() 4711 >>> s [17, 42, 496] >>> s.pop(1) 42 >>> s [17, 496] 6
Listmetodernas felhantering >>> list = [1, 2, 3, 4] >>> list.remove(42) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: list.remove(x): x not in list >>> list.pop(42) Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: pop index out of range >>> list.insert(42, 42) >>> list [1, 2, 3, 4, 42] 7
Ta bort element ur en lista >>> list = ['one', 2, 3, 4, 5, 6] >>> del list[0] >>> list [2, 3, 4, 5, 6] >>> del list[1:3] >>> list [2, 5, 6] 8
Exempel Vi vill ha en funktion remove som tar bort alla förekomster av ett element ur en rak lista, d.v.s. vi struntar i eventuella underlistor. Funktionen ska vara destruktiv, d.v.s. den ska inte bygga upp en ny lista som resultat, utan ska verkligen plocka bort de element vi inte vill ha med med hjälp av någon av metoderna remove() eller pop(). >>> list = [1, 2, 3, 4, 2, 5] >>> remove(list, 2) >>> list [1, 3, 4, 5] Vi ska titta på flera olika sätt att lösa problemet. 9
Lösning 1 Räkna hur många gånger elementet förekommer och anropa sedan remove() så många gånger. def remove(seq, x): for i in range(seq.count(x)): seq.remove(x) Fördelar: Lite kod att skriva. Använder inbyggda metoder. Nackdelar: Kommer att gå igenom listan flera gånger. 10
Lösning 2 Gå igenom listan element för element. Anropa pop() för varje element som matchar. Nackdelar: Fungerar inte! Eftersom vi tar bort element kommer listan att vara för kort när loopen kommer till vad den tror är slutet. def remove(seq, x): for i in range(len(seq)): if seq[i] == x: seq.pop(i) 11
Lösning 3 Gå igenom listan element för element, baklänges. Anropa pop() för varje element som matchar. Fördelar: Fungerar. Eftersom vi räknar baklänges kommer elementen försvinna från den del av listan vi är klara med. def remove(seq, x): for i in range(len(seq)-1,-1,-1): if seq[i] == x: seq.pop(i) Nackdelar: Lite bökigt att räkna baklänges (även om man fixar det i anropet till range). 12
Lösning 4 Gå igenom listan element för element, men kolla mot längden på listan varje varv. Anropa pop() för varje element som matchar. Fördelar: Kan upplevas som mer lättläst än de övriga lösningarna, eftersom den är mer rakt på. Nackdelar: Vi måste beräkna längden av listan en gång varje varv. Vi måste själva initiera och uppdatera i. def remove(seq, x): i = 0 while i < len(seq): if seq[i] == x: seq.pop(i) else: i += 1 Ny konstruktion while som också kan användas för upprepningar. 13
Tupler En tupel är en konstant lista. När den väl är skapad är den oföränderlig. Tupler ser ut som och funkar som listor, men begränsas av vanliga parenteser istället för hakparenteser. >>> tp = (1, 2, 3) >>> tp[1] 2 >>> len(tp) 3 >>> for elem in tp: print(elem, end="*")... 1*2*3 14
Tupler >>> tp[1] = 'hello' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> tp.append(42) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'tuple' object has no attribute 'append' >>> tp2 = 'a', 'b', 'c' >>> tp2 ('a', 'b', 'c') >>> 1, (1,) 15
Tupler Även om tupler i sig är oföränderliga (eng. immutable) så kan vi skapa nya tupler utifrån gamla. En tupel kan också innehålla data som i sig går att förändra. >>> (1, 2) + (3, 4) (1, 2, 3, 4) >>> struktur = (['a', 42], ['b', 37]) >>> struktur[0][1] = 4711 >>> struktur (['a', 4711], ['b', 37]) 16
Tupel eller lista? Eftersom tupler aldrig kan ändras och alltså alltid har samma storlek och innehåll, så kan de interna rutinerna göras snabbare. Tupler är alltså mer effektiva och lämpar sig för tillfällen när man har konstana sekvenser, t.ex. months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', \ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') En användbar konvention är att använda tupler när man har strukturerade data där informationen på olika positioner har olika roller, och listor när man har sekvenser av likadana data. date = (2012, 9, 11) dates = [(2012, 8, 27), (2012, 8, 29), (2012, 9, 3)] 17
Dictionaries Dictionaries (eller dictar) är listor av par som man kan använda som uppslagningstabeller. Varje par består av en nyckel och ett värde. Genom att ange nyckeln kan man slå upp värdet. I övrigt funkar de ungefär som vanliga listor. >>> dict = {'a': 45, 'b': 39, 'c': 19} >>> dict['a'] 45 >>> dict['d'] = 4711 >>> dict {'a': 45, 'c': 19, 'b': 39, 'd': 4711} >>> len(dict) 4 18
Dictionaries >>> 'b' in dict True >>> 'q' in dict False >>> dict['q'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'q' >>> for elem in dict: print(elem, end="*")... a*b*c*d* 19
Dictionaries Nycklarna kan vara vilka värden som helst som är oföränderliga (eng. immutable). Samma nyckel kan inte förekomma två gånger. Ordningen mellan paren så som de visas om vi tittar på dicten är egentligen ointressant och Python lämnar inga garantier för i vilken ordning de lagras. >>> {1: 4, 'a': 47, (1, 2): 'hejsan'} {'a': 47, 1: 4, (1, 2): 'hejsan'} En mer allmän benämning på den här typen av datastruktur är hashtabell. Grundtanken är att det finns en algoritm som för varje nyckel beräknar ett hashvärde som används som internt index för att snabbt kunna slå upp det värde vi letar efter. 20
Sammansatta datatyper Datatyp Innehåll? Muterbar? Indexering? str Text Nej Heltal list Vad som helst Ja Heltal tuple Vad som helst Nej Heltal dict Vad som helst Ja Nästan vad som helst 21
Binära sökträd (BST) Träd Gren/Subträd Nod 8 Inre nod Löv 3 10 Nyckel Rot 1 6 14 Barn Nivå 4 7 13 Djup Tomt träd 22
Binära sökträd (BST) Binärt, d.v.s. varje nod har maximalt två grenar under sig. Ordnade så att nyckeln i en viss nod är större än alla nycklar i vänstra grenen, och mindre än eller lika med alla nycklar i högra grenen. Ofta lagras mer än enbart nyckeln i noden. Ett sätt att organisera datamängder för att lätt kunna söka reda på information (om man känner till nyckeln). 1 6 8 3 10 14 4 7 13 23
Rekursiv sökning i ett binärt sökträd Finns x i trädet t? Om t är ett tomt träd finns inte x i det. Om t är ett löv kollar vi om t = x. Om x < nyckeln i roten av t, fortsätt söka i vänster gren. Om x > nyckeln i roten av t, fortsätt söka i höger gren. Annars har vi en träff! 8 3 10 1 6 14 4 7 13 24
Hur får vi in dem i Python? Det finns inga inbyggda binära sökträd i Python. De är en abstrakt datatyp (ADT). Om vi vill kunna arbeta med binära sökträd måste vi implementera dem själva, d.v.s. skriva funktioner som kan göra allt det vi vill göra med binära sökträd. För att kunna göra det måste vi bestämma ett sätt att representera binära sökträd med hjälp någon av de inbyggda datatyperna i Python. 25
Representation av binära sökträd Det tomma trädet representeras av en tom lista [ ]. Löv representeras av heltal. Inre noder representeras som en lista på formen [vänster gren, nyckel, höger gren]. Detta är endast ett bland många möjliga sätt att representera binära sökträd i Python. 26
Exempel på representation 8 3 10 1 6 14 4 7 13 [[1, 3, [4, 6, 7] ], 8, [[], 10, [13, 14, []]]] 27
Primitiva funktioner De primitiva funktionerna hjälper oss att implementera representationen. De är var för sig ganska enkla, men fungerar som ett gränssnitt gentemot andra funktioner som vill arbeta med binära sökträd. def är_tomt_träd(träd): return isinstance(träd, list) and not träd def är_löv(träd): return isinstance(träd, int) def skapa_träd(vänster_träd, nyckel, höger_träd): return [vänster_träd, nyckel, höger_träd] def vänster_subträd(träd): return träd[0] def höger_subträd(träd): return träd[2] 28
Rekursiv sökning i ett binärt sökträd Finns x finns i trädet t? Om t är ett tomt träd finns inte x i det. Om t är ett löv kollar vi om t = x. Om x < nyckeln i roten av t, fortsätt söka i vänster gren. Om x > nyckeln i roten av t, fortsätt söka i höger gren. Annars har vi en träff! def sök(träd, x): if är_tomt_träd(träd): return False elif är_löv(träd): return träd == x elif x < nyckel(träd): return sök(vänster_subträd(träd), x) elif x > nyckel(träd): return sök(höger_subträd(träd), x) else: return True Funktionen nyckel plockar fram nyckeln ur roten av ett träd. Funktionen sök känner inte till representationen, utan arbetar enbart med de primitiva funktionerna. 29
Exempel på sökning def sök(träd, x): if är_tomt_träd(träd): return False 8 elif är_löv(träd): return träd == x 3 10 elif x < nyckel(träd): return sök(vänster_subträd(träd), x) 1 6 14 elif x > nyckel(träd): return sök(höger_subträd(träd), x) else: 4 7 13 return True 30
www.liu.se