Algoritmer och kodningsteknik

Inledning

En algoritm är en beskrivning av hur man stegvis utför en beräkning eller löser en uppgift. Läs wikipedias beskrivning av algoritm. De typiska beståndsdelarna av en algoritm är

Tanken med denna lektion är att presentera några standardalgoritmer och diskutera olika möjliga implementationer av dem. I verkligheten behöver man sällan själv skriva dessa algoritmer men de är ändå mycket väl värda att studera som modellexempel på design och analys av algoritmer samt, i vårt sammanhang, kodningsteknik.

Metoderna i denna lektion är placerade i en klass med namnet Algorithms. De är alla skrivna som klassmetoder dvs deklarerade med static och de få all sin information via parametrar (om inte annat explicit sägs).

Exempel

Undersök om ett visst tal är ett primtal

Ett primtal är ett heltal som bara är jämnt delbart med 1 och sig själv. En uppenbar algoritm för att undersöka om ett tal n är ett primtal är då att gå igenom alla tal j mellan 2 och n-1 och se om n är jämnt delbart med något av dessa.

Det är dock inte nödvändigt att kontrollera alla dessa tal. Eftersom n inte kan vara jämnt delbart med något tal större än n/2 kan man halvera antalet tal att jämföra med.

Det går att begränsa antalet tal ännu mer. Om n inte är ett primtal så kan det skrivas som en produkt p*q av två heltal där det ena måste vara mindre än eller lika med kvadratroten ur n. Detta är en betydande förbättring - för att till exempel undersöka om ett tal av storleken 1 000 000 räcker det med 1000 försök i stället för 500 000.

Hur kan man se om två tal n och j är jämnt delbara?

I Java är det enklast att använda operatorn % som returnerar resten vid en division. (Ett annat alternativ är att utnyttja heltalsaritmetiken: Uttrycket n/j*j == n är true om n är jämnt delbarat med j.)

Vi får då följande metod:

public static boolean isPrime(int number) { int limit = (int) Math.sqrt(number); for (int i=2; i <= limit; i++) { if (number%i == 0) { return false; } } return true; }

Som synes returnerar metoden false så fort en division som går jämnt ut hittas. Om hela loopen gås igenom så betyder det att ingen division som går jämt ut hittats och true returneras.

Metoden ovan har ett "stilbrott": Man bör alltid kunna se på ett loopvillkoret hur loopen ska sluta. Vi skulle kunna åstadkomma detta t ex med koden

int limit = (int) Math.sqrt(n); boolean prime = true; for (int i=2; i<=limit && prime; i++) { if (n%i == 0) { prime = false; } } return prime;

men denna kod är knappast enklare eller lättare att förstå.


Euklides algoritm

En av de först kända algoritmen är Euklides algoritm för att finna största gemensamma delare till två heltal. Läs om algoritmen i wikipedia! Artikeln innehåller en rekursiv implementation i programmeringsspråket Python. Här följer en en iterativ implemention i Java:
public static int sgd(int a, int b) { assert a > b && b >= 0; while (b != 0) { int c = a % b; a = b; b = c; } return a; }

(Satsen assert anger villkor för att algortmen ska fungera. Om dessa inte är uppfyllda avbryts programmet. Läs mer om assert i lektion 8!)


Hitta största (eller minsta) värdet i en array

Den typiska sättet att hitta det största värdet är att börja med att ansätta ("gissa") ett största värde och sedan gå igenom alla värden och uppdatera gissningen för varje större värde man hittar.

Vad ska man börja med att gissa på? Man måste börja med ett tal som säkert är mindre än eller lika med det maximala värdet. Om man vet att alla tal är positiva skulle man kunna börja med 0. Om man inte vet något om talen kan skulle man kunna börja med Integer.MIN_VALUE som är det minsta möjliga heltalet. Både enklast och bäst är det dock att börja med att gissa att det första talet är störst.

Vi ger här två varianer av lösning: en med en "vanlig" for-sats och en med "for-each"-satsen:

public static int max(int[] a) { assert a.length > 0; int m = a[0]; for (int i = 1; i < a.length; i++) { if (a[i] > m) { m = a[i]; } } return m; }
public static int max(int[] a) { assert a.length > 0; int m = a[0]; for (int x : a) { if (x > m) { m = x; } } return m; }

Om man bara ska söka i en del av arrayen (t ex de n först elementen) eller vill veta index för det största elementet så passar inte for-each-loopen.

Övning: Skriv en metod som returnerar index för det största elementet i en array

Att leta efter största (eller minsta) värdet i en array kan lösas genom att sortera arrayen och sedan se vilket som kom på första eller sista plats. Sortering är dock en betydligt mer tidsmässigt krävande och kodmässigt mer komplicerad operation varför den givna algoritmen är att föredra.


Se om ett visst värde finns i en array

Det enklaste sättet att se om ett visst värde finns i en array är att använda så kallad linjär sökning. Det betyder att man tittar på elementen i tur och ordning. Om man upptäcker elementet avbryter man och noterar att det sökta elementet finns. Om man gått igenom hela arrayen utan att hitta det noterar man att det inte finns.

public static boolean contains(int x, int[] array) { for (int a : array) { if (a == x) { return true; } } return false; }

Instick i en sorterad arraylist

Antag att vi har en ArrayList<Integer> a med heltal lagrade i storleksordning med det minsta först och att vi vill lägga in ett talx i listan så att den fortfarande är sorterad. Vi enkelhetens skull betecknar vi antalet lagrade element med n (det är alltså a.size()). De redan lagrade värdena finns på index 0 till n-1

bild

Det nya elementet ska alltså få ett index mellan 0 och n. Om vi vet vilket index i det nya elementet ska placeras på kan vi använda metoden add(i, x) för att lägga in det.

Om vi börjar från vänster (index i = 0) och letar så är det två villkor som säger att vi inte funnit platsen än:

  1.   i < n   och
  2.   x > ai

Om något av villkoren inte är uppfyllt har vi funnit platsen. Det kan vara först som fall (a), någonstans inuti som fall (b) eller efter sista lagrade elementet som fall (c).

Det ger följande algoritm för att hitta platsen:
int i = 0; while (i < a.size() && x > a.get(i) ) { i++; } a.add(i, x);
Observera
  1. Vi behöver inga specialfall. Algoritmen fungerar även för en från början tom arraylist (om a.size() == 0 ) liksom att det nya elementet ska in efter alla lagrade element.
  2. Ordningen mellan villkoren i while-satsen är väsentlig. Om i == a.size() är det illegalt att göra a.get(i) (ger ett index out of bounds exception). Java-språket är dock konstruerat så att om sanningsvärdet kan bestämmas utifrån det första villkoret i ett and- eller or-uttryck så undersöks inte det andra villkoret.

Instickssortering med en arraylist

Genom upprepad användning av ovanstående algortim kan man producera en arraylist med sorterade värden. Exempel:
public static ArrayList<Integer> sort(ArrayList<Integer> a) { ArrayList<Integer> result = new ArrayList<Integer>(); for (int x : a) { En metod som tar emot en arraylist, och skapar en ny arraylist med samma värden men i sorterad ordning
int i = 0; while (i < result.size() && x > result.get(i) ) { i++; } result.add(i, x); Koden från ovastående insticksalgoritm.
} return result; } Returnera en referens till det skapade arraylist-objektet.

Instickssortering i en array

I ovanstående sortering utnyttjade vi egenskapen att det går att lägga in ett nytt element var som helst i en arraylist. Arraylister är implementerade med hjälp av arrayer. Operationen a.add(i,x) innebär att elementen från och med plats i och uppåt måste flytta ett steg. Detta sköts internt av arraylist-koden.

Vi ska här studera hur man kan göra en instickssortering på plats i en array. Vi ska alltså inte skapa en ny array utan ordna om värdena i den givna arrayen så att den blir sorterd.

Antag att vi från början har denna osorterade array:

Array

Vi delar in arrayen i två delar: den vänstra delen som innehåller sorterade element och den högra delen med osorterade element. Från början innehåller den vänstra delen bara ett element:

Array

Idéen i algoritmen är att successivt utöka den sorterade med ett element åt gången genom att infoga det första av de osorterade bland de sorterade så att dessa förblir sorterade.

Efter ett antal steg kan det alltså se ut så här:

Array

Observera att de element som finns på plats 0 - 4 är precis samma element som fanns på plats 0 - 4 från början men att de nu är sorterade.

Vi ska nu titta på hur det går till att utvidga den sorterade delen skall inkludera talet 9 (elementet på plats 5).

Vi börjar med att kopiera talet 9 till en annan variabel. Platsen 5 blir då "ledig" även om talet 9 ligger kvar tills vi lagt något annat där: Array
Vi jämför sedan det uttagna talet med det som ligger på plats 4: Array
Eftersom 9 är mindre än 12 flyttar vi 12 till 9-ans gamla plats. Plats 4 blir då ledig. Array
Vi jämför sedan 9 med talet på plats 3: Array
Eftersom 9 är även är mindre än 11 flyttar vi 11 till plats 4: Array
Jämför talet 9 med elementet på plats 2: Array
Eftersom 9 är större än 8 har vi hittat platsen för talet 9. Lägg in det: Array

Efter dessa steg är den sorterade delen utvidgad till att omfatta elementen 0 till 5.

Om vi antar att arrayen heter a och att den sorterade delen består av elementen på plats 0 till i-1 kan ovanstående formuleras algoritmiskt på detta sätt:
int x = a[i]; int j = i-1; while ( x < a[j]) { a[j+1] = a[j]; j--; } a[j+1] = x;

Observera att algoritmen fungerar om x skulle råka vara större än talet på plats i-1. Om däremot x är minst dvs mindre är a[0] så kommer vi få ett negativt index. För att hantera det måste vi kontroller att j inte är negativt innan vi jämför a[j] med x:

int x = a[i]; int j = i-1; while ( j >= 0 && x < a[j]) { a[j+1] = a[j]; j--; } a[j+1] = x;

För att göra en fullständig sortering låter vi i löpa från ett till det högsta indexet. Vi förpackar algoritmen i en metod där vi skickar arrayen och antalet använda element som parametrar.

public static insertionSort(int a[], int n) { for (int i= 1; i < n; i++) { int x = a[i]; int j = i-1; while (j >= 0 && x < a[j]) { a[j+1] = a[j]; j--; } a[j+1] = x; } }

Vi har valt att skicka med antalet element som ska sorteras som en parameter. Metoden blir då mer flexibel än om vi låter metoden använda a.length. Om vi således önskar en hel array sorterad anropar vi med insertionSort(a, a.length).

Instickssorteringen hör till gruppen "enkla sorteringsalgoritmer". Andra algoritmer i den gruppen är urvalssortering och bubbelsortering. Alla dessa karakteriseras av att tiden växer proportionellt mot kvadraten på antalet element.

Övning: Gruppen AlgoRythmics illustrerar metoden så här. Den är dock aningen annorlunda implementerad. Skriv om koden så att den exakt följer deras implementation!

Gruppen "avancerade sorteringsalgoritmer" innehåller bl. a. mergesort, quicksort och heapsort. För dessa växer tiden typiskt proportionellt mot   n·log(n)   som är en betydande förbättring mot   n2  för stora värden på n (antalet element som ska sorteras).


Binär sökning

Om en array är sorterad går det att mycket effektivt om ett visst värde finns med eller ej. Idéen är mycket enkel:

Lokalisera elementet som ligger i mitten på arrayen.
Om det sökta värdet är mindre än värdet i mitten så
         leta vidare på samma sätt i den halva som innehåller de små elementen
annars
        leta vidare i på samma sätt i den halva som innehåller de stora elementen.

Eftersom antalet element att söka bland halveras varje steg blir arbetet proportionellt mot log2 n där n är antalet element att söka bland. Således krävs det ca 10 försök att leta bland 1000 element och 20 försök att leta bland 1 000 000.

Även om idéen är enkel så är implementationen inte alldeles trivial. Här är en version:

public static boolean binSearch(int x, int[] a, int n) { int low =0; int high = n; while (low < high + 1) { int mid = (high+low)/2; if (x < a[mid]) { high = mid; } else { low = mid; } } return x==a[low]; }
För att inse att ovanstående program verkligen är korrekt kan man resonera så här:
  1. Om det sökta värdet x finns i arrayen måste dess index i uppfylla relationen 0 ≤ i < n.
  2. Omedelbart före while-loopen gäller således att lowi < high
  3. I loopen ändras low eller high på ett sätt så att relationen lowi < high bevaras. Man säger att relationen är en loop-invariant.
  4. Så länge som loop-villkoret är sant kommer relationen low < mid < high vara uppfylld. Det betyder att intervallet som vi söker i hela tiden blir kortare. Loopen kommer således att avbrytas.
  5. När loopen stannat är lowhigh + 1

Tillbaka

Valid CSS!