DA3009 Programmeringsteknik för datorlingvister, prgdl11

Komplexitetsanalys, sökning, sortering


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 n antalet tal som ska sorteras.
Hur växer tidsåtgången T(n) för växande n?

            n             log(n)               nlog(n)               n2                 2n        
1010 s1 s10 s100 s17 min
100100 s2 s3 min2 tim1022 år
1000010000 s (ca 3 tim)4 s11 tim3 å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 T(n) nöjer vi oss med att ange ordoklassen O(n).



Definition 
T(n) är O(F(n)) om det finns positiva konstanter c och n0 sådana att 0<=T(n)<=cF(n) för n>=n0
Vad innebär detta? Exempel: T(n) = 10n2 + 100n + 10logn + 1000 säger vi är O(n2).

Sökning

Förutsättningar: Frågor:

Linjärsökning (Sequential search)

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

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

def sequentialSearch(alist, item):
    for x in alist:
        if x == item:
            return True
    return False

Binärsökning

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




11131320222528303132324862

111313202225                             

            202225                             

                    25                             

Binärsökning är O(log n) i värsta fallet:
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 n/2, andra varvet n/4, tredje varvet n/8. Vi är klara när det bara finns ett element kvar att titta på och då är n/2x=1 där x är antal varv. Vi två-logaritmerar bägge led och får att x=2log(n).
Här följer en funktion för binärsökning:

def binarySearch(alist, item):
"""Söker i "alist" efter "item". Returnerar True om den hittas, False annars"""
    first = 0
    last = len(alist)-1
    found = False

    while first <= last and not found:
        midpoint = (first + last)/2
        if alist[midpoint] == item:
	    found = True
        else:
            if item < alist[midpoint]:
                last = midpoint-1
            else:
                first = midpoint+1
    return found



Sortering

Sortering är en av dom vanligaste uppgifterna för ett program. Här följer en beskrivning av några sorteringsalgoritmer!

Urvalssortering (Selection sort)

Vi tänker oss att vi ska sortera en lista med N tal. Totalt krävs N(N-1)/2 jämförelser och N-1 byten, helt oberoende av hur pass osorterad listan är från början. Tiden är alltså i stort sett proportionell mot kvadraten på N. Man säger att komplexiteten är O(N2).
6 13 5 2 1 3 7 8 10 12 4 9 11

1 13 5 2 6 3 7 8 10 12 4 9 11

1 2 5 13 6 3 7 8 10 12 4 9 11
         
def urvalssortera(data):
    n = len(data)
    for i in range(n):
        minst = i
        for j in range(i+1,n):
            if data[j] < data[minst]:
                minst = j
        data[minst],data[i] = data[i], data[minst]

Urvalssorteringsanimation från Hong Kongs universitet.


Bubbelsortering (Bubble sort)

En något smartare metod än urvalssortering är denna: Totalt krävs i värsta fall N(N-1)/2 jämförelser och lika många byten. Men om listan är nästan sorterad från början räcker det med några få genomgångar och då blir bubbel snabbare än urval.

Bubbelsortanimation från universitetet i Oldenburg.




Insättningssortering (Insertion sort)

Denna metod känns särskilt naturlig om man får värden ett efter ett (t ex om dom läses in från en fil) och man vill sortera in dom i en lista. Här sorterar vi in ett värde i taget på följande vis: Om alla värden redan finns i listan sorterar vi in ett värde i taget, med början från det andra.

Komplexiteten är i allmänhet O(N2) men den är lite snabbare än urvalssortering i praktiken eftersom en flytt är snabbare än ett byte. För att sortera in ett nytt värde i en redan sorterad lista är insättning bäst.

13 6 5 2 1 3 7 8 10 12 4 9 11

6 13 5 2 1 3 7 8 10 12 4 9 11

5 6 13 2 1 3 7 8 10 12 4 9 11
         
def insattningssortera(data):
    n = len(data)
    for i in range(1, n):
        varde = data[i]
        plats = i
        while plats > 0 and data[plats-1] > varde:
            data[plats] = data[plats-1]
            plats = plats - 1
        data[plats] = varde

Animation av insättningssortering från Indian Institute of Technology i Kanpur.


Damerna först

Den enklaste sorteringsuppgiften är att sortera om en personlista med damerna först och den smarta algoritmen kallas damerna-först-sortering.
  1. Sätt ett pekfinger i var ände av listan!
  2. Rör fingrarna mot varandra tills vänstra fingret fastnat på en herre och högra fingret på en dam!
  3. Låt damen och herren byta plats!
  4. Upprepa från 2 tills fingrarna korsats!
Idén kan utvecklas till Quicksort, som är den snabbaste av alla sorteringsalgoritmer.



Quicksort

Damerna-först-algoritmen med två pekfingrar används i Quicksort. Först bestämmer man vilka (små) nyckelvärden som ska kallas damer. Resten kallas herrar och så utför man algoritmen. Listan delas alltså i två segment, det första med små värden, det andra med stora värden. Nu behöver man bara sortera segmenten var för sej. Det här är en rekursiv tanke!
def quicksort(data):
    sista = len(data) - 1
    qsort(data, 0, sista)

def qsort(data, i, j):
    pivotindex = (i+j)//2
    data[pivotindex], data[j] = data[j], data[pivotindex]
    k = partitionera(data, i-1, j, data[j])
    data[k], data[j] = data[j], data[k]
    if k-i > 1:
        qsort(data, i, k-1)
    if j-k > 1:
        qsort(data, k+1, j)

def partitionera(data, v, h, pivot):
    while True:
        v = v + 1
        while data[v] < pivot:
            v = v + 1
        h = h -1
        while h != 0 and data[h] > pivot:
            h = h -1
        data[v], data[h] = data[h], data[v]
        if v >= h: break
    data[v], data[h] = data[h], data[v]
    return v

Komplexiteten blir i allmänhet O(N log N). Det beror på att man kan dela listan på mitten log N gånger. Exakt hur snabb den är beror på hur man avgör vilka värden som ska vara damer. Om man tar det första värdet i listan och utnämner det och alla mindre värden till damer, så blir Quicksort mycket långsam för redan nästan sorterade listor! Det bästa är att ta ut tre värden - det första, det sista och något i mitten - och låta det mellersta värdet bestämma vad som är damer. Det kallas median-of-three. Man kan visa att komplexiteten då blir 1.4 N log N i genomsnitt.

Quicksortanimation från Aucklands universitet.

Merge Sort

Om man har flera sorterade småfiler är det lätt att samsortera dom till en fil. Det här kan man också göra med en lista om man har extrautrymme för att kopiera den till två andra hälften så långa listor. Det här ger en rekursiv tanke! Komplexiteten blir O(N log N), lika snabb som quicksort men kräver extra minnesutrymme. Om första halvan av listan a ska samsorteras med andra halvan kopierar man först över allting till hjälplistan b och sorterar sedan tillbaka från b till a.
def mergesort(data):
    if len(data) > 1:
        mitten = len(data)//2
        vensterHalva = data[:mitten]
        hogerHalva = data[mitten:]

        mergesort(vensterHalva)
        mergesort(hogerHalva)

        i, j, k = 0, 0, 0
    
        while i < len(vensterHalva) and j < len(hogerHalva):
            if vensterHalva[i] < hogerHalva[j]:
                data[k] = vensterHalva[i]
                i = i + 1
            else:
                data[k] = hogerHalva[j]
                j = j + 1
            k = k + 1
                
        while i < len(vensterHalva):
            data[k] = vensterHalva[i]
            i = i + 1
            k = k + 1
                    
        while j < len(hogerHalva):
            data[k] = hogerHalva[j]
            j = j + 1
            k = k + 1

Mergesortanimation från North Carolina State University.


Divide and conquer

En kanske inte så sympatisk härskarteknik som går ut på att så split mellan sina undersåtar för att rikta deras misstro mot varandra istället för mot härskaren kallas på engelska divide and conquer. (På svenska säger vi söndra och härska, men det passar inte lika bra här.)

Inom datalogi används detta uttryck för att beskriva en lösningsmetod där man delar upp ett problem i två eller flera delproblem och löser dessa var för sig. Delproblemen löses enklast med rekursion!

Quicksort och mergesort är två exempel på divide-and-conquer-principen.




Räknesortering (Distribution count)

Om man vet att det bara finns ett litet antal nyckelvärden, till exempel 100 olika, är distributionsräkning oslagbart snabbt. Det kräver att talen som sorteras in i listan hämtas från en annan lista eller fil. Komplexiteten blir O(N), men fungerar alltså bara om man har få nyckelvärden. Om man upprepar förfarandet för varje position (siffra eller bokstav) i de data som ska sorteras får man radixsortering.
3 4 3 3 5 3 5 3 5 4 5 3 4

3:6
4:3
5:4

3 3 3             4                        



Dansade sorteringsalgoritmer av AlgoRythmics på YouTube.