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.