6 Rekursion
6.1 Rekursionens fyra principer
Problem löses genom:
1.
2.
3.
4.
förenkling med hjälp av "sig själv".
att varje rekursionssteg löser ett identiskt men mindre problem.
att det finns ett speciellt basfall som kan utvärderas direkt.
att problemets storlek minskar och slutligen når basfallet genom förenklingen.
Detta kan även uttryckas som att en rekursiv lösning:
1. måste ha ett basfall.
2. måste ha (minst) ett rekursionsfall.
3. i varje rekursivt anrop måste närma sig basfallet i diskreta steg. D v s, man måste gå en
förbestämd sträcka åt rätt håll varje gång. Det är alltså inte tillåtet (eller möjligt) att närma
sig med ett procentuellt steg varje gång.
6.2 Några vanliga användningsområden för rekursion
§
§
§
§
quicksort – en sökalgoritm
binärsökning – en sökalgoritm
Operationer på träd – en datastruktur.
Operationer på grafer – en datastruktur.
6.3 Söka i telefonkatalogen
6.3.1
Generell lösning (binärsökning):
1. Slå upp katalogen på mitten
2. Se vilken halva namnet är i
3. Slå upp den halvan på mitten
4. Upprepa 2-4 tills rätt sida hittats
Hela katalogen
Rätt halva
F.H.
Fel halva
R.H.
R.H.
F.H.
…och så vidare…
6.3.2
Rekursiv lösning
//Pre: 'first' är första indexet och 'last' är sista indexet i
//
arrayen 'katalog'
//Post: result = -1 om numret inte hittades, annars numret som
//
tilhör personen 'person'.
int hittaNummer(katalogType katalog[], int first, int last, string
person)
{
if(first > last)
// numret finns inte
return -1;
else
{
int mid = (first+last)/2;
// mitten på kat.
if(person == katalog[mid].namn) //numret finns
{
return katalog[mid].nummer;
}
else if(person < katalog[mid].namn)
return hittaNummer(katalog, first, mid-1,
person);
else
return hittaNummer(katalog, mid+1, last,
person);
}
6.4 Hitta största talet i en array
6.4.1
Iterativ lösning
//Pre: ?
//Post: ?
int storstaTalet(int* array, int arraySize)
{
int max = array[0];
for(int i=1; i < arraySize; i++)
if(max < array[i])
max = array[i];
return max;
}
6.4.2
Rekursiv lösning
//Pre: ? (Tänk speciellt på 'max')
//Post: ?
int storstaTalet(int* array, int i, int max, int arraySize)
{
if(i >= arraySize)
return max;
else if(max < array[arraySize])
max = array[i];
return storstaTalet(array, i+1, max, arraySize);
}
6.5 Utskrift av en sträng baklänges
6.5.1
Iterativ lösning
//Pre: ?
//Post: ?
void baklanges(char* ord)
{
int ord_length = strlen(ord);
for(int i = ord_length - 1; i >= 0; i--)
cout << ord[i];
}
6.5.2
Rekursiv lösning
//Pre: ?
//Post: ?
void baklanges(char* ord, int ord_length)
{
if(ord_length >= 0)
{
cout << ord[ord_length];
baklanges(ord, ord_length - 1);
}
}
Anmärkning
Ofta innebär rekursiva operationer att man måste ha med en eller flera extra parametrar
jämfört med dess iterativa motsvarighet.
Man vill generellt undvika att konstruera operationer med långa parameterlistor för användare
av operationen eftersom semantiken blir mer komplex.
Om en rekursiv operation har parametrar som måste eller bör döljas för användaren kan man
enkelt konstruera ytterligare en operation som en gränssnittsoperation med en mindre
parameterlista. Gränssnittsoperationen anropar den rekursiva operationen.
//Pre: ?
//Post: ?
void baklanges(char* ord)
{
int ord_length = strlen(ord);
baklanges(ord, ord_length);
}
Operationen ovan är en gränssnittsoperation till den rekursiva operationen baklanges.
Om de två operationerna ingår i en klass skulle gränssnittsoperationen vara publik och den
rekursiva operationen skulle vara privat.
På så sätt ger man användaren (klienten) ett enklare och tydligare gränssnitt. Dessutom blir
kontraktet för gränssnittsoperationen enklare, vilket speglar en enklare och tydligare semantik
(innebörd).
6.6 Beräkning av fakulteten
6.6.1
Iterativ lösning
//Pre: ?
//Post: ?
int fakultet(int n)
{
int fakulteten = 1;
for(int i = 1; i <= n; i++)
fakulteten = fakulteten * i;
return fakulteten;
}
6.6.2
Rekursiv lösning
//Pre: ?
//Post: ?
int fakultet(int n)
{
if(n==0)
//Basfall
return 1;
else
//Rekursionsfall
return n * fakultet(n-1);
}
6.7 Rekursiv list size
// privat operation, klienter får använda den publika
// gränssnittsoperationen nedan.
// pre: 'head' pekar på första noden i listam
// post: result = antalet element i listan.
int List::size(Node*& head)
{
if(head == 0)
return 0;
else
return 1 + size(head->next);
}
// Publik gränssnittsoperationen till ovanstående
// Pre: true
// Post: result = antalet element i listan.
int List::size()
{
return size(head);
}
6.8 Rekursiv list insert
// privat operation
// Pre: 'list' pekar på första noden i listan.
// Post: 'element' insatt i storleksordning i 'list'
void List::insert(Node*& list, Element element)
{
Node* newNode;
if( (list == 0) || (element < list->data) )
{
newNode = new Node;
newNode->data = element;
newNode->next = list;
list = newNode;
}
else
insert(list->next, element);
}
Anmärkning
Listan antas vara sorterad. Hur kan man skriva en operation som lägger till ett element på en
viss position?
6.9 Rekursiv list remove
// Pre: 'list' pekar på första noden i listan och 'element' finns i
//
listan.
// Post: Första förekomsten av 'element' finns inte längre i listan
void List::remove(Node*& list, Element element)
{
Node* nodeToRemove;
if(list->data == element)
{
nodeToRemove = list;
list=list->next;
delete nodeToRemove;
nodeToRemove = 0;
}
else
remove(list->next, element);
}
Anmärkning
Denna remove-operation tar bort ett element per värde. Hur kan man skriva en rekursiv
remove-operation som tar bort på position?
6.10 Igenkänning av palindrom
§
§
§
Ett ord är ett palindrom om det har samma bokstav först och sist och om ordet minus
första och sista bokstaven är ett palindrom.
En tom sträng är ett palindrom. (Basfall)
Ett enda tecken är också ett palindrom.
6.10.1 Pseudokod för palindromigenkänning:
bool isPal(ord)
om (ord är en tom sträng eller ett enda tecken)
return true;
annars
om (ordets första och sista bokstäver är samma)
return isPal(ordet minus första och sista bokstaven)
annars
return false;
slut isPal
6.11 Quicksort
Är en vanlig och mycket snabb algoritm för sortering av arrayer och linkande. Algoritmen
bygger på att dela upp den stora mängden värden i mindre delar, sortera dessa delar och sedan
sätta ihop delarna igen.
Man hittar först en pivot och sorterar alla element utifrån denna pivot i två halvor. Quicksort
anropas sedan rekursivt med båda halvorna (som inte behöver vara lika stora).
Stegen för quicksort är följande:
1. Om det finns något att sortera gör 2,3,4 annars inget.
2. hitta pivot
3. sortera i två halvor (s1 och s2)
4. anropa quicksort med halvorna
5. Sätt in pivoten på rätt ställe (mellan s1 och s2)
Exempel - Körning av quicksort
3
2
5
1
1. Inte tom, gör 2,3,4
2. Pivot väljs till 3 (först i mängden)
p
3
2
5
1
3. Dela mängden i två halvor efter pivoten
3
2
1
5
värdena i s1 är nu alla mindre än pivoten och s2 större.
4. anropa quicksort med s1 och s2
Första halvan:
2
1
1. Inte tom, gör 2,3,4
2. Hitta pivot 2 (först i mängden)
3. Dela mängden i två halvor efter pivoten
p
s1
2
1
4. anropa quicksort med halvorna (inget händer)
5. Sätt in pivoten mellan s1 och s2
1
2
Andra halvan:
5
1. Inte tom, gör 2,3,4
2. Hitta pivot 5 (enda elementet)
3. Dela mängden i två halvor efter pivoten (inte mycket att göra)
4. Anropa quicksort med halvorna (inget händer)
5. Sätt in pivoten mellan s1 och s2 (inget händer)
Fortsättning på huvudalgoritmen:
P
s1
3
2
s1
1
s2
5
Delmängderna s1 och s2 är nu sorterade:
3
1
2
5
5. Sätt in pivoten (3) mellan s1 och s2
1
2
3
5
Sorterad och klar.
6.12 Vanliga rekursionsapplikationer
•
•
•
•
•
•
Grammatikkonstruktion – de flesta språk definieras med hjälp av något pseudospråk
(oftast BNF) som är rekursivt. Detta förenklar konstruktionen av parsers, dvs
grammtikkontrollen i en kompilator
Träd är rekursivt definierade vilket gör att rekursiva operationer på träden är naturliga.
Grafer – s.o.
Länkade listor – s.o.
Många matematiska problem som kan förenklas till "mindre versioner av sig själva", ex
fakultet, exponent
Problem som kan lösas med hjälp av matematisk induktion för att bevisa något.