1 of 45

Introduktion till programmering i Python

Föreläsning 12:

Analys av algoritmer, komplexitet, rekursion, beräkningsbarhet

Chalmers (DAT455)

Version 20220804

Aarne Ranta

2 of 45

Agenda

Sökalgoritmer: linjär och binär sökning

Komplexitet

Sortering

Rekursiva funktioner

Rekursiva datatyper

Beräkningsbarhet och stopproblemet

3 of 45

Sökalgoritmer: linjär och binär sökning

4 of 45

Sökproblemet

Hitta ett element och returnera ett värde, t.ex.

  • hitta ett ord i en ordbok och returnera dess översättning
  • hitta ett namn i en adresslista och returnera telefonnumret
  • (enklast:) hitta ett element i en lista och returnera dess ordningsnummer

Pythons dictionaries och index()-metoden kan användas för detta.

  • [2, 3, 6, 8].index(6) == 2

Men hur fungerar de internt? Hur skulle vi göra om vi inte hade dem?

5 of 45

Linjär sökning

def linsearch(x, ns):

counter = 0

for n in ns:

if n == x:

return counter

else:

counter += 1

raise ValueError

Räknare för element som man går igenom

  • boken använder ns[i] vilket också funkar

När man hittar en träff returnerar man räknarens värde

Annars inkrementerar man ränkaren

  • obs. syntax: lika med

counter = counter + 1

Om man gått igenom listan och inte hittat x rapporteras ValueError.

6 of 45

Binär sökning

def binsearch(x, ns):

low = 0

high = len(ns) - 1

while low <= high:

mid = (low + high)//2

item = ns[mid]

if x == item:

return mid

elif x < item:

high = mid - 1

else:

low = mid + 1

raise ValueError

Definiera lägsta och högsta gränsen för den del av listan som man söker i.

Gränserna kommer att närma sig varandra

  • men så länge de inte passerar varandra
    • kolla mellersta elementet
      • om det är lika med x, klart
      • om mindre, sök i listan under det mellersta
      • om större, sök i listan över det mellersta

Om elementet inte finns i listan, rapporteras ValueError

7 of 45

Vi testar sökalgoritmerna

>>> ws = fileWords('data/bible.txt')

>>> len(ws)

824192

>>> linsearch('Mary', ws)

633028

>>> binsearch('Mary', ws)

ValueError

>>> sws = sorted(ws)

>>> binsearch('Mary', sws)

101588

Läs orden från den engelska bibeln.

Så många ord (tokens).

Mary förekommer först ganska sent, i Nya Testamentet.

Binärsökning är meningslös om listan inte är sorterad.

Låt oss sortera den

Nu får vi en position i den alfabetiska ordningen - vi borde väl ha sparat den ursprungliga också… men det är inte huvudsaken nu.

8 of 45

En grafisk jämförelse av algoritmerna

Linjär sökning: rött

Binär sökning: blått

x-axeln: miljoner element i listan

y-axeln: processortid i sekunder

Vi ser att linjär sökning följer en rak linje:

  • med 3M element, ~ 0.3s
  • med 6M element, ~ 0.5s
  • med 8M element, ~ 0.9s

Men hur beter sig den binära sökningen?

9 of 45

Logaritmisk komplexitet

Låt oss säga att vi söker x bland 1000 element.

Steg 1: om x == 500, är vi klara. Annars går vi till en av halvorna, låt oss säga den övre.

Steg 2: om x == 750, klart. Annars

Steg 3: om x == 875, klart. Annars

Steg 4: om x == 938, klart. Annars

….

Steg 10: om x == 999, klart.

I det värsta fallet behöver vi 10 ~ log21000 steg. Vi säger att binär sökning har logaritmisk komplexitet.

10 of 45

Enkel mätning av processortid

import time

start = time.process_time()

code

end = time.process_time()

print(end-start)

Använd time-biblioteket

Kolla processortiden före körningen av koden

Kör koden

Kolla processortiden efter

Beräkna skillnaden

Det finns mera sofistikerade metoder, men denna är den enklaste.

11 of 45

Komplexitet

12 of 45

Ordo-notationen för komplexitet ("Big O")

logaritmisk: O(log n)

linjär: O(n)

log-linjär: O(n log n)

kvadratisk: O(n2)

kubisk: O(n3)

exponentiell: O(2n)

fakultet: O(n!)

13 of 45

Exempel på algoritmer av olika komplexiteter

logaritmisk: O(log n) binär sökning

linjär: O(n) linjär sökning

log-linjär: O(n log n) sortering (med bästa standardfunktioner)

kvadratisk: O(n2) sortering (med vissa enklare funktioner)

kubisk: O(n3) matrismultiplikation (den naiva metoden)

exponentiell: O(en) gå genom alla delmängder av en mängd

fakultet: O(n!) permutationer; determinant (Laplace-expansion)

14 of 45

Några till begrepp

Tidskomplexitet: antal steg eller längden av tid i funktion av datans storlek

Minneskomplexitet: mängd datorminne i funktion av datans storlek

Asymptotisk komplexitet: storleksordningen, dvs O-klassen

Värstafallskomplexitet: den maximala komplexiteten

Genomsnittlig komplexitet: genomsnittet för alla möjliga input-data

I teoretisk datalogi tittar man ofta på värstafallskomplexiteten, medan i praktiken kan den genomsnittliga komplexiteten vara viktigare.

15 of 45

Sortering

16 of 45

Hur ska man resonera?

Detta är något vi ofta gör i praktiken - t.ex. ordnar böcker i en bokhylla.

En naiv metod: selection sort.

  • gå genom alla böcker och ta isär den som är alfabetiskt först
  • gå genom de återstående böckerna och ta isär den alfabetiskt nästa
  • tills det är inga böcker kvar

Komplexitet: n + (n-1) + (n-2) + … + 1 = (n+1)(n/2) = (n2 +n)/2, därmed kvadratisk.

17 of 45

Selection sort

Zelle, s. 478

18 of 45

QuickSort

def quickSort(xs):

if xs:

pivot = xs[0]

return (

quickSort([x for x in xs[1:] if x < pivot]) +

[pivot] +

quickSort([x for x in xs[1:] if x >= pivot])

)

else:

return xs

Magiskt enkelt med rekursion och listkomprehensioner.

Om listan inte är tom:

  • ta fram första elementet
  • dela upp listan till två: de element som är mindre och de som är större
  • sortera dessa listor och sätt första elementet emellan

Värsta fall: O(n2)

Genomsnitt: O(n log n)

Ett exempel på söndra och härska ("divide and conquer")-algoritmer.

19 of 45

Pythons sorted()?

Timsort https://en.wikipedia.org/wiki/Timsort (av Tim Peters)

Relaterat till merge sort (boken 13.3.2, QA session)

Tidskomplexitet O(n log n) - både genomsnittlig och värsta falls

20 of 45

Jämförelse mellan quickSort och Pythons sorted()

Blått: quickSort()

Grönt: sorted()

x-axeln: antal element

y-axeln: processortid i sekunder

Inte så meningsfullt egentligen, eftersom vi har kört med bara en testmängd (ord i bibeln). "Uteliggaren" ("outlier") vid 2000 element behöver förklaring.

21 of 45

permutationSort() - den sämsta tänkbara...

def permutations(xs):

if len(xs) < 2:

return [xs]

else:

ps = []

for qs in permutations(xs[1:]):

for j in range(len(xs)):

p = (qs[:j] + [xs[0]] + qs[j:])

ps.append(p)

return ps

def permutationSort(xs):

def sorted(ys):

for i in range(len(ys)-1):

if ys[i] > ys[i+1]:

return False

return True

for p in permutations(xs):

if sorted(p):

return p

Bygg en lista av alla permutationer (dvs olika ordningar av element)

En hjälpfunktion som testar om en lista är sorterad

Vi går igenom permutationerna tills vi hittar en sorterad lista.

22 of 45

Rekursiva funktioner

23 of 45

Tre sätt att upprepa en beräkning

def factFor(n):

r = 1

for k in range(1, n+1):

r *= k

return r

def factWhile(n):

r = 1

k = 1

while k < n+1:

r *= k

k += 1

return r

def factRec(n):

if n <= 1:

return 1

else:

return n * factRec(n-1)

For-loop:

  • går igenom en lista
  • ackumulerar resultatet
  • garanterat att terminera (för ändliga listor)

While-loop:

  • upprepar så länge ett villkor håller
  • ackumulerar resultatet
  • terminerar endast när villkoret slutar hålla

Rekursiv funktion:

  • anropar sig själv med "mindre" argument
  • terminerar endast om den når ett basfall, dvs fall som inte är rekursivt

24 of 45

Några synpunkter om rekursiva funktioner

>>> start = time.process_time()

>>> x = factFor(800)

>>> end = time.process_time()

>>> end-start

0.0036279999999990764

>>> x = factWhile(800)

0.002654000000003265

>>> x = factRec(800)

0.002801999999999083

>>> x = factRec(999)

Traceback (most recent call last):

RecursionError: maximum recursion depth exceeded in comparison

>>> x = factRec(998)

>>> x

402790050127220994538240674597601587306681545756471103647447357787726238637266286878923131618587992…

De kan göra mer än while-loopar.

De kan också vara ungefär lika tidseffektiva.

  • men de kan kräva mera minne

I Python är de begränsade till rekursionsdjup ca. 1000, dvs. antal rekursiva anrop

  • detta är för att förhindra stack overflow, dvs använda mer minne än allokerat
  • många andra språk, t.ex. Haskell, har inga sådana begränsningar, och kan ofta optimera minnesanvändningen med hjälp av s.k. svansrekursion

25 of 45

Rekursiva datatyper

26 of 45

Träd

root

tree1

tree2

tree3

treen

Ett träd har en nod som är dess rot, samt grenar ("subtrees"), som själva är träd med varsin rot.

Objektet fortsätter neråt tills man når en nod utan grenar.

Exempel

  • filsystem
  • släktträd
  • syntaxträd

27 of 45

Exempel: den svenska kungafamiljen år 2016 (utan makar)

28 of 45

En klass för träd

class Tree:

def __init__(self, node, subtrees):

self.root = node

self.subtrees = subtrees

def getParts(self):

return self.root, self.subtrees

def depth(tree):

(n, ts) = tree.getParts()

if ts:

return 1 + max([depth(t) for t in ts])

else:

return 1

Klassvariablerna:

  • root-noden
  • grenarna (som själva kan vara träd)

Accessormetoden för att få fram roten och grenarna

Exempel: beräkna trädets djup med en rekursiv funktion

  • addera djupet av dem djupaste grenen
  • om inga grenar finns, använd 1

29 of 45

royal = Tree('CarlGustaf',

[Tree('Victoria',[Tree('Estelle',[]),Tree('Oscar',[])]),

Tree('CarlPhilip',[Tree('Alexander',[])]),

Tree('Madeleine', [Tree('Leonore',[]), Tree('Nicolas',[])])])

30 of 45

Exempel: syntaxträd

def atom(a):

return Tree(a, [])

def plus(x,y):

return Tree("+", [x, y])

def times(x,y):

return Tree("*", [x, y])

2 + 3 * 4

plus(atom(2), times(atom(3), atom(4)))

+

2

*

3

4

(2 + 3) * 4

times(plus(atom(2), atom(3)), atom(4))

*

+

4

3

2

31 of 45

Syntaxträd i en kompilator

källkod

parsning

syntaxträd

maskinkod

kodgenerering

2 + 3 * 4

plus(atom(2),

times(atom(3),atom(4)))

bipush 2

bipush 3

bipush 4

imul

iadd

Detta är en mycket förenklad bild, men visar hur man kan översätta aritmetiska uttryck med små heltal till kod i JVM Assembler (Java Virtual Machine). Uttrycken kan vara godtyckligt komplexa. Assemblern kan enkelt konverteras till binärkod rad för rad.

32 of 45

En funktion för kodgenerering

def postfix(tree):

f,ts = tree.getParts()

xs = []

for t in ts:

for s in postfix(t):

xs.append(s)

xs.append(str(f))

return xs

def jvm(tree):

def instr(f):

if f == "*":

return "imul"

elif f == "+":

return "iadd"

else:

return "bipush " + str(f)

instrs = map(instr, postfix(tree))

return '\n'.join(instrs)

En rekursiv funktion, som

  • tar fram roten och grenarna
  • genererar kod för grenarna
  • lägger till roten

Postfix betyder att roten (operatorn) kommer efter sina argument. Således:

2 + 3 * 4 → 2 3 4 * +

(2 + 3) * 4 → 2 3 + 4 *

Funktionen jvm() konverterar postfix-bitarna till olika instruktioner i maskinkoden.

Den skulle lika gärna kunna returnera binärkod direkt, t.ex. 01101000 i ställer för imul.

33 of 45

Beräkningsbarhet och stopproblemet

34 of 45

Beräkningsbarhet ("computability")

Finns det en algoritm för alla (precist formulerade) problem?

Matematisk version: finns det en Turing-maskin som kan lösa alla problem?

Svaret - från Turing själv! - är nej: det finns problem som är oavgörbara ("undecidable").

Ett av dessa är stopproblemet ("the halting problem"):

  • avgör om ett program P terminerar med ett input X

Skulle vara väldigt användbart i praktiken!

35 of 45

Stopproblemet: ett enkelt bevis

Zelle's bok, ss. 491-492:

Terminerar turing.py med input turing.py?

  • om ja, då ska terminates() returnera True
  • men då går programmet till en oändlig while-loop

  • om nej, då ska terminates() returnera False
  • då går programmet inte till while-loopen, utan terminerar

Detta är en motsägelse, som visar att terminates() är omöjligt att konstruera.

36 of 45

Andra icke-beräkningsbara problem

Att hitta bevis eller motbevis i predikatkalkylen (ett enkelt system för logik, som kan användas för att implementera matematiska bevis i datorn).

  • men det finns många specialfall som är avgörbara och viktiga i praktiken
  • samma gäller terminering av program, som man kan approximera

AI-kompletta problem: AI-problem som skulle kräva "obegränsad intelligens" för att lösa (inte lika seriöst som beräkningsbarhet, men en viktig insikt).

Exempel: översättning av naturliga språk.

37 of 45

Översättning som ett AI-komplett problem

Exempel: finsk "hän" till svensk "han" eller "hon".

  • man måste veta om "hän" refererar till en man eller en kvinna
  • detta kan kräva godtyckligt mycket förståelse av kontexten (vad som sagts tidigare i texten) samt faktakunskap om vad som kan sägas om män vs. kvinnor
  • i vissa fall är kanske "hen" rätt översättning, men inte alltid

38 of 45

translate.google.com 18/8/2020 (precis samma problem 3/8/2022)

39 of 45

Det finns hur mycket som helst att göra i programmering

  • för programmerare
  • för datavetare (soväl "computer scientist" som "data scientist")
  • för matematiker
  • för AI-forskare
  • för IT-experter i specialområden
  • för andra yrkesgrupper som måste lösa AI-kompletta problem

Och det kommer aldrig att bli färdigt!

40 of 45

Python-grammatiken för hela kursen

41 of 45

<stm> ::= <exp>,* = <exp>,*

| <exp> <assignop> <exp>

| for <name> in <exp>: <block>

| def <name> (<arg>,*): <block>

| <exp>

| return <exp>,*

| if <exp>: <block> <elses>?

| while <exp>: <block>

| pass

| break

| try: <block> <except>* except: <block>

| class <name> (<name>,*)?: <block>

| assert <exp> ,<exp>?

| import <name> <asname>?

| from <name> import <imports>

| raise <name>

<asname> ::= as <name>

imports ::= * | <name>,*

<elses> ::= <elif>* else: <block>

<elif> ::= elif exp: <block>

<except> ::= except <name>: <block>

<block> ::= <stm> <stm>*

<exp> ::= <exp> <op> <exp>

| <name>.?<name>(<arg>,*)

| <literal>

| <name>

| ( <exp> )

| [ <exp>,* ]

| <exp>[exp]

| <exp>[<slice>,*]

| lambda <name>*: <exp>

| { <keyvalue>,* }

| [ <exp> for <name> in <exp> <cond>? ]

| - <exp>

| not <exp>

<keyvalue> ::= <exp>: <exp>

<arg> ::= <name>

| <name> = <exp>

<cond> ::= if <exp>

<op> ::= + | - | * | ** | / | // | % | == | > | >= | < | <= | != | in | not in | and | or

<assignop> ::= += | -= | *=

<slice> ::= <exp>?:<exp>? |

Tilldelningssatser generaliseras på två sätt:

  • inte bara till variabler, utan t.ex. element i listor (detta gäller sedan föreläsning 5)
  • speciella operatorer som += kan användas som förkortningar

  • "exception" när något går fel

Slicing i arrays (fält) kan ha flera element

Tilldelning med aritmetiska operatorer

Slicing-uttryck för arrays

42 of 45

Lästips och övningsklassen

43 of 45

Ur boken

  • kapitel 13, förutom 13.3.2 och 13.4.1
  • föreläsningen går igenom QuickSort samt trädstrukturer, som inte finns med i boken

Övningsklassen:

  • boken 13.3.2: Merge sort (och andra sorteringsalgoritmer om ni vill)
  • boken 13.4.1: towers of Hanoi
  • boken 13.6: övningar (det finns många bra den här gången)
  • ni kan också leka med koden från GitHub, filerna
    • complexities.py, som visar olika algoritmer och enkla visualiseringar av deras komplexitet med matplotlib
    • trees.py, som visar Tree-klassen och enkel kodgenerering

44 of 45

Förberedelse för tentan

45 of 45

Gör labbarna först: utan dem får man inget betyg på Ladok.

Gamla tentor finns i https://chalmers.instructure.com/courses/19152/files/folder/gamla-tentor

Live-föreläsningen den 22/8 går igenom ytterligare en gammal tenta, https://docs.google.com/presentation/d/1ATCJtMdHpcV1rHu9o7vZ_GmrlonB5M6JPh0HLLZ_-a4/edit?usp=sharing

Tentan 24/8 kl 14-18

Omtentan 7/10 kl 14-18

Båda på Canvas som Assignment, ingen anmälan behövs.

Senare omtentor under terminen kommer att annonseras på Chalmers tentamensschema.