Föreläsning 7: 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)
- Bubbelsortering (Bubble sort)
- Insättningssortering (Insertion sort)
- Damerna först
- Quicksort
- Samsortering (Mergesort)
- Söndra och härska
- Distributionsräkning (Distribution count)
- Radixsortering
Urvalssortering (Selection sort)
Vi tänker oss att vi ska sortera en vektor med N tal.
- Sök igenom vektorn efter minsta talet. (N-1 jämförelser)
- Flytta det till första positionen. (Ett byte)
- Sök efter näst minsta talet. (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 vektorn ä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):
min=i
for j in range(i+1,n):
if data[j]<data[min]:
min=j
temp=data[min]
data[min]=data[i]
data[i]=temp
|
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 vektorn 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 vektorn är nästan sorterad från början räcker det med några
få genomgångar, och då blir bubbel snabbare är urval.
Insättningssortering (Insertion sort)
Denna metod känns särskilt naturlig
om man hämtar talen ett efter ett från en fil och sorterar in dom i en
vektor.
- Jämför nya talet med sista talet i vektor.
- Om nya är större, lägg det sist i vektorn.
- Annars, putta ner sista talet ett pinnhål och jämför igen.
- Putta ner så många tal som behövs för att sätta in nya talet på rätt plats.
- Upprepa för varje nytt tal.
Insättning fungerar också om talen finns i vektorn från början. Efter
k insättningar är vektorsegmentet
v[1]...v[k]
ordnat och
v[k+1]
är det nya talet som ska sättas in.
"Insättningssortering av vektorn a"
def insattnssortera(a):
# Först finns bara a[0], sedan insätts a[1], sedan a[2] osv.
# Om a[j] som sätts in bör ligga på plats a[i] måste alla
# mellanliggande element a[i]...a[j-1] puttas ett steg.
for j in range(1,len(a)):
aj=a[j]
i=j
while i>0 and aj<a[i-1]:
a[i]=a[i-1]
i=i-1
a[i]=aj
Komplexiteten är i allmänhet O(N2) men för att sortera in
ett nytt värde i en redan sorterad vektor är
insättning den snabbaste algoritmen.
2 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
2 |
9 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
2 |
5 |
9 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
2 |
5 |
9 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
|
 
 
 
 
 
|
def sattin(x, data, sista):
i=sista
if x>data[i]:
data[i+1]=x
else:
while i>=0 and x<data[i]:
data[i+1]=data[i]
i-=1
data[i+1] = x
|
Damerna först
Den enklaste sorteringsuppgiften är att sortera om en personarray med
damerna först och den smarta algoritmen kallas
damerna-först-sortering.
- Sätt ett pekfinger i var ände av arrayen!
- 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. Vektorn 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 vektorn så att damerna kommer först.
- Sortera varje segment för sej.
def swap(data, first, second):
temp = data[first]
data[first] = data[second]
data[second] = temp
def quicksort(data): # metoden som anropas utifrån
qs(data, 0, len(data))
def qs(data, first, last):
pivot = partition(data, first, last)
swap(data, first, pivot)
if first+1 < pivot:
qs(data, first, pivot-1)
if pivot+1 < last:
qs(data, pivot+1, last)
def partition(data, first, last): # damerna först
x=data[first]
i=first
j=last-1
while True:
while j>=first and data[j]>x:
j=j-1
while i<last and data[i]<=x:
i=i+1
if i<j:
swap(data, i, j)
else:
return j
Komplexiteten blir i allmänhet O(N log N).
Det beror på att man kan dela vektorn på mitten log N gånger.
Exakt hur snabb den är beror på hur
man avgör vilka värden som ska vara damer. Ofta tar man det första talet
i vektorn och utnämner det och alla mindre tal till damer. Då blir
Quicksort mycket långsam för redan nästan sorterade vektorer! Det bästa
är att ta ut tre tal - 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.
Samsortering (Mergesort)
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 vektor om man har
extrautrymme för att kopiera den till två andra hälften så långa vektorer.
Det här ger en rekursiv tanke!
- Dela vektorn i två hälften så långa vektorer.
- Sortera varje halva för sej.
- Samsortera till ursprungliga vektorn.
Komplexiteten blir
O(N log N), lika snabb
som quicksort men kräver extra minnesutrymme. Om första halvan
av vektorn
a ska samsorteras med andra halvan
kopierar man först över allting till hjälpvektorn
b
och sorterar sedan tillbaka från
b till
a.
Detta förfarande kallas merge och programmeras lämpligen som en
egen metod.
def mergesort(a):
ms(a, 0, len(a))
def ms(a,left,right):
if left+1<right:
center = (left + right)//2
ms(a,left,center)
ms(a,center,right)
merge(a,left,center,right)
def merge(a,left,center,right):
b=len(a)*[None]
b[left:right]=a[left:right]
i=left
j=center
k=left
while i<center and j<right:
if b[i]<b[j]:
a[k]=b[i]
i=i+1
else:
a[k]=b[j]
j=j+1
k=k+1
if i==center:
while j<right:
a[k]=b[j]
j=j+1
k=k+1
else:
while i<center:
a[k]=b[i]
i=i+1
k=k+1
Söndra och härska
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 sedan på samma sätt - rekursion!
Quicksort och mergesort är två väldigt tydliga exempel på
divide-and-conquer-principen.
Distributionsräkning (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 vektorn hämtas från en annan
vektor eller fil.
- Läs igenom filen och räkna hur många det finns av varje nyckelvärde.
- Dela in vektorn 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 radix sort.
3 |
4 |
3 |
3 |
5 |
3 |
5 |
3 |
5 |
4 |
5 |
3 |
4 |
3:6
4:3
5:4
3 |
3 |
3 |
    |
    |
    |
4 |
    |
    |
    |
    |
    |
    |
Radixsortering
Kallas också hålkortssortering eftersom de användes i
de gamla hålkortsmaskinerna. Passar bäst när alla nycklar
har samma längd, som tiosiffriga personnummer. Först
sorteras korten i tio buntar efter
sista
siffran. Sen lägger man ihop buntarna och gör en ny
sortering efter näst sista siffran etc. Till slut är
faktiskt alla korten ordnade och det efter bara
O(N)
jämförelser, nämligen
kN, där
k
är antalet siffror.
Nu är det ju inte frågan om hålkortsbuntar längre utan
snarare om vektorer i minnet. Sorteringen kan då göras
med distributionsräkning, eftersom det i varje sortering
bara finns tio olika nyckelvärden.
Det går lika bra att sortera strängar, enda skillnaden
är att det finns tjugonio nyckelvärden i varje sortering.
En sträng kan ju uppfattas som ett tal skrivet med basen
29 och siffrorna A-Ö. Radix betyder just
bas i ett talsystem.
Sortering av större mängder data
Alla metoderna ovan förutsätter att de data som ska sorteras
kan lagras i primärminnet. Om så inte är fallet får man ta
till
extern sortering men det ingår inte i den här kursen.
Tentatal 000603: NIX-Telefon
För konsumenter som inte vill ha marknadsföringssamtal: Ring 020-27 70 00!
Föreningen för konsumentskydd vid marknadsföring per telefon
har startat ett register dit den som inte vill bli uppringd av
telefonförsäljare kan anmäla sig.
Till att börja kommer kontrollen att ske genom att företaget sänder
sin telefonlista till nix och får tillbaka en lista där de nixade numren
markerats. Vilka av följande metoder kan föreningen använda sig av? Vilken är bäst?
- Binärträd
- Bloomfilter
- Distributionsräkning
- Hashtabell
- Insättning
- Quicksort
Ett framtida mål är att kontroll också skall kunna ske över internet.
Då måste kontrollen ske snabbt men man vill också försäkra sig om att
ingen ska kunna få ut en lista över alla nixade telefonnummer.
Vilken metod passar bäst för internet-kontrollen?
Svar:
I början ringde många och anmälde sig till NIX-registret.
Då måste det gå lätt att lägga till nya. Då gäller följande:
- Binärträd går snabbt att söka i men man måste se till att
det inte blir obalanserat när nya telefonnummer läggs till.
- Bloomfilter är besvärligt att stoppa in nya nummer i, eftersom ...
- Distributionsräkning fungerar inte (telefonnumren är ju unika).
- Hashtabell se bloomfilter.
- Insättning är praktiskt när man vill stoppa in nya nummer
(om man dessutom väljer att söka med binärsökning går det snabbt).
- Quicksort måste göras om för hela vektorn för varje nytt nummer.
Bloomfilter är det bästa alternativet för den framtida internet-kontrollen.
Man kan räkna med att många har registrerat sig så snabb
sökning är nödvändig. Dessutom har bloomfilter fördelen att
ingen användare kan få ut telefonnummerlistan.