Föreläsning 3: Komplexitetsanalys, sökning, rekursion

Mål Läs i kursboken

Komplexitetsanalys

Sökning

Rekursion

3. Analysis (Länkar till en externa sida.)

6.2. Searching (Länkar till en externa sida.)

6.3. The Sequential Search (Länkar till en externa sida.)

6.4. The Binary Search (Länkar till en externa sida.)

5. Recursion (Länkar till en externa sida.) (utom 5.12)

  • Komplexitetsanalys
  • Linjärsökning
  • Binärsökning
  • Testning
  • Rekursion


Denna föreläsning handlar om algoritmer.


Komplexitetsanalys

Det är intressant att se tidsåtgången för en algoritm. Denna anges ofta som funktion av indatas storlek, som av tradition kallas n. Exempel: För sortering är LaTeX: nn antalet tal som ska sorteras.
Hur växer tidsåtgången LaTeX: T\left(n\right)T(n) för växande LaTeX: nn?

https://www.desmos.com/calculator Links to an external site.

n log(n) n*log(n) n2 2n
10 s 1 s 10 s 100 s 17 min
100 s 2 s 3 min 2 tim 1022 år
10000 s (ca 3 tim) 4 s 11 tim 3 år

Vi analyserar oftast värsta fallet (det är i regel enklare att räkna på) men man kan också räkna på medelfallet.

Istället för att ange den exakta tidsfunktionen LaTeX: T\left(n\right)T(n) nöjer vi oss med att ange ordoklassen LaTeX: O\left(n\right)O(n).

Definition

   LaTeX: T\left(n\right)T(n) är LaTeX: O\left(F\left(n\right)\right)O(F(n)) 

om det finns positiva konstanter LaTeX: cc och LaTeX: n_0n0 sådana att

LaTeX: 0\le T\left(n\right)\le c\cdot F\left(n\right)0T(n)cF(n)  för LaTeX: n\ge n_0nn0

Vad innebär detta?

  • LaTeX: c\cdot F\left(n\right)cF(n) anger en övre gräns för LaTeX: T\left(n\right)T(n) då LaTeX: nn är stort.
  • Vi behöver bara ta bara med den term som växer snabbast.
  • Vi kan bortse från multiplikation med konstant.

Exempel:

LaTeX: T\left(n\right)=10n^2\:+\:100n\:+\:\log_{10}n\:+1000T(n)=10n2+100n+log10n+1000    säger vi är    LaTeX: O\left(n^2\right)O(n2)

  • För små indata är analys onödig - använd den enklaste algoritmen!
  • Inläsning från fil tar längre tid än åtkomst av redan inlästa värden.
  • Minnesåtgång kan också vara relevant.

 

Uppgift: Matrismultiplikation

Vad är tidskomplexiteten för matrismultiplikation?


Sökning

Förutsättningar:

  • Vi söker efter något element i en lista med n element.
  • Det element vi söker efter karakteriseras av en söknyckel (eng key) men kan också ha andra data.

Frågor:

  • Vad ska hända om det sökta elementet inte finns med?
  • Kan det finnas flera element med samma nyckel? Vill man i så fall hitta alla?

Linjärsökning (Sequential search)

Den enklaste sökningen: bryter så fort den hittar det sökta elementet.

Algoritm:

  1. Gå igenom varje element i listan:
    • Jämför elementet med nyckeln, och
    • ...returnera True om dom är lika
  2. Returnera False om hela listan gåtts igenom utan att elementet hittats.

Linjärsökning är LaTeX: O\left(n\right)O(n) (i värsta fallet måste vi titta på alla LaTeX: nn elementen).

Här följer en funktion för linjärsökning i en lista.

def exists(the_list, key):
    for x in the_list:
        if x == key:
            return True
    return False

Uppgift: Förbättra funktionen

Den här funktionen talar bara om huruvida elementet finns med i listan.  Hur ska den modifieras för att returnera den plats i listan elementet finns? 


Binärsökning

Om listan är sorterad är den snabbaste sökningsalgoritmen binärsökning. Algoritm:

  1. Beräkna intervallets mittpunkt.
  2. Är det nyckeln? Avbryt, det sökta elementet är funnet.
  3. Annars: Avgör om det nyckeln finns i första eller andra halvan och fortsätt söka där (upprepa från punkt 1)
  4. Om halvan du söker i krympt till ingenting: Avbryt. Det sökta elementet fanns inte med.
11 13 13 20 22 25 28 30 31 32 32 48 62
11 13 13 20 22 25
20 22 25
25


Binärsökning är LaTeX: O\left(\log_2n\right)O(log2n) i värsta fallet. Varför det?

Vi söker bland n element och undrar hur många varv sökningen kan ta?

Antal element att söka bland halveras i varje varv, så första varvet får vi LaTeX: \frac{n}{2}n2, andra varvet LaTeX: \frac{n}{4}n4, tredje varvet LaTeX: \frac{n}{8}n8. Vi är klara när det bara finns ett element kvar att titta på och då är LaTeX: \frac{n}{2^x}=1n2x=1 där LaTeX: xx är antal varv. Vi två-logaritmerar bägge led och får att LaTeX: x=\log_2nx=log2n.
Här följer en funktion för binärsökning:

def binary_search(the_list, key):
low = 0
high = len(the_list)-1
found = False

while low <= high and not found:
middle = (low + high)//2
if the_list[middle] == key:
found = True
else:
if key < the_list[middle]:
high = middle - 1
else:
low = middle + 1
return found

Binärsökning är en knepig algoritm att implementera korrekt. Hur testar vi att koden ovan fungerar?

Testning

Här är några förslag! Testa:

  1. normalfallet, t ex att söka efter 14 i listan [9, 14, 23, 54, 92, 104]
  2. att söka efter ett tal som inte finns med, t ex 15
  3. att söka i en tom lista []
  4. att söka efter vänstraste elementet 9 ...
  5. ... och högraste elementet 104.
  6. att söka efter ett element bortom vänstra gränsen, t ex 8 ...
  7. ... och bortom högra gränsen, t ex 105.

Rekursion

Rekursiv kommer från latinet och betyder återlöpande. Om man i definitionen av ett begrepp använder begreppet självt så är definitionen rekursiv. Rekursiva tankar kan också användas för problemlösning.

  • Rekursiv tanke:   reducerar problemet till ett enklare problem med samma struktur
  • Basfall:  ett fall som inte leder till rekursivt anrop

Rekursiv bild

Sifferexempel

Triangeltalet S(N) är summan av de N första heltalen. S(4)=1+2+3+4
Fråga: Vad är värdet på S(N)?
Rekursivt svar: S(N) = S(N-1) + N ... men S(1)=1.
Här följer en rekursiv funktion för beräkning av triangeltalet:

    def S(n):
        if n == 1: 
            return 1
        else:
            return S(n-1) + n

 Fråga: Vilken siffersumma har heltalet n?

Rekursivt svar: Sista siffran plus siffersumman om man stryker sista siffran i n, ...men noll har siffersumman noll.

def siffersumma(n):
      if n == 0:  
         return 
else: return n%10 + siffersumma(n//10)

Fråga: Hur många siffror har heltalet n?
Rekursivt svar: En siffra mer än om man stryker sista siffran i n, ...men tal mindre än tio är ensiffriga.

def antalsiffror(n)
      if n...

Hur fungerar det?

När man skriver egna rekursiva funktioner bör man lita på att det rekursiva anropet fungerar - man behöver inte analysera anropsgången för varje fall. Men för att förstå varför rekursion kan vara extra minneskrävande är det vara bra att känna till hur programspråken hanterar rekursiva anrop.

  • För varje anrop skapas en aktiveringspost som innehåller data för anropet, t ex parametrar, lokala variabler och anropspunkt.
  • Aktiveringsposten pushas på en stack.
  • När det rekursiva anropet är klart poppas aktiveringsposten från stacken, varefter föregående anrop ligger överst på stacken.
s(1)
s(2)
s(3)
s(4)
huvudprogram

Rekursiv binärsökning

Binärsökning är lätt att göra rekursivt! Basfallet är att listan är tom, dvs har noll element.

 

Rekursiv binärsökningsfunktion:

 

def binsok(listan, nyckel):
    if len(listan) == 0:
        return False
    else:
        mitten = len(listan)//2
        if listan[mitten] == nyckel:
            return True
        else:
            if nyckel < listan[mitten]:
                return binsok(listan[:mitten], nyckel)
            else:
                return binsok(listan[mitten+1:], nyckel)

Några saker att minnas

    • En rekursiv funktion kan alltid omformuleras utan rekursion, men om det finns flera rekursiva anrop i funktionen kan det vara besvärligt.

 

  • För vissa problem är en rekursiv funktion mycket enklare att formulera och ger kortare kod än utan rekursion. Ofta måste man gå via den rekursiva lösningen i tanken även om man gör en icke-rekursiv lösning.