DA3009 Programmeringsteknik för datorlingvister, prgdl11
Komplexitetsanalys, sökning, sortering
- Komplexitetsanalys
- Sökning
- Linjärsökning
- Binärsökning
- Sorteringsalgoritmer
- Urvalssortering (Selection sort)
- Bubbelsortering (Bubble sort)
- Insättningssortering (Insertion sort)
- Damerna först
- Quicksort
- Mergesort (Samsortering)
- Divide and conquer
- Räknesortering (Distribution count)
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         |
10 | 10 s | 1 s | 10 s | 100 s | 17 min |
100 | 100 s | 2 s | 3 min | 2 tim | 1022 år |
10000 | 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 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?
-
cF(n)
anger en övre gräns för T(n)
då n
är stort.
- Vi behöver bara ta bara med den term som växer snabbast.
- Vi kan bortse från multiplikation med konstant.
- Det spelar ingen roll vilken logaritm vi använder.
Exempel:
T(n) = 10n2 + 100n + 10logn + 1000
säger vi är 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.
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 isåfall hitta alla?
Linjärsökning (Sequential search)
Algoritm:
- Sök igenom elementen i tur och ordning.
- Bryt när det sökta elementet påträffas eller när det uppenbaras att det sökta elementet
inte finns med.
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:
- Beräkna intervallets mittpunkt.
- Avgör om det sökta elementet finns i första eller andra halvan och fortsätt söka där.
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 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.
- Sök igenom listan efter minsta värdet. (N-1 jämförelser)
- Flytta det till första positionen. (Ett byte)
- Sök efter näst minsta värdet. (N-2 jämförelser)
- Flytta det till andra positionen. (Ett byte)
- - -
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:
- Byt första och andra om dom står i fel ordning. (En jämförelse, ett byte)
- Byt andra och tredje om dom står i fel ordning. (En jämförelse, ett byte)
- - -
- Bubbla igenom listan gång på gång tills inga byten sker.
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:
- Jämför med varje tidigare värde i listan.
- Om det nya värdet är mindre gör vi plats genom att flytta det tidigare värdet ett snäpp åt höger.
- Flytta fram så många värden som behövs för att sätta in nya värdet på rätt plats.
- Stoppa in det nya värdet.
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.
- Sätt ett pekfinger i var ände av listan!
- Rör fingrarna mot varandra tills vänstra fingret fastnat på en
herre och högra fingret på en dam!
- Låt damen och herren byta plats!
- 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!
- Bestäm vilka värden som ska kallas damer.
- Partitionera listan så att damerna kommer först.
- Sortera varje segment för sej.
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!
- Dela listan i två hälften så långa listor.
- Sortera varje halva för sej.
- Samsortera till ursprungliga listan.
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.
- Läs igenom filen och räkna hur många det finns av varje nyckelvärde.
- Dela in listan i lagom stora segment för denna distribution.
- Läs filen igen och lägg in varje värde i sitt segment.
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.