10 Meer over functies In hoofdstuk 5 hebben we functies uitgebreid bestudeerd. In dit hoofdstuk bekijken we drie andere aspecten van functies: recursieve functies dat wil zeggen, functies die zichzelf direct of indirect aanroepen, functiepointers en functies met een variabel aantal argumenten. 10.1 Recursieve functies Zoals we al in hoofdstuk 5 hebben gezien, kan een functie elke andere functie aanroepen. Programmeurs maken van deze mogelijkheid gebruik om programma's hiërarchisch op te bouwen, waarbij de functie main subfuncties F1, F2, ... aanroept om deeltaken te laten uitvoeren; deze subfuncties roepen weer subfuncties Gl, G2, ... aan om nog eenvoudiger taken te laten uitvoeren, enzovoort (zie de hoofdstukken 5 en 7). Een met functionele decompositie gemaakt programmaontwerp kan dankzij de mogelijkheid van elkaar aanroepende functies rechtstreeks worden geïmplementeerd. Dit is niet de enige manier om functies te gebruiken. Een functie kan ook zichzelf aanroepen. Zo'n functie noemen we dan recursief. Voor veel programmeerproblemen bestaat een oplossing die met directe of indirecte recursie kan worden geformuleerd. Recursieve oplossingen zijn in het algemeen elegant en sluiten op een natuurlijke manier aan op het probleem. Recursie wordt vaak gebruikt in toepassingen waarin de oplossing kan worden geformuleerd in termen van het successievelijk toepassen van dezelfde oplossing op delen van het oorspronkelijke probleem. Dat komt vaak voor bij zoeken sorteerproblemen met betrekking tot recursief gedefinieerde datastructuren (zie de hoofdstukken 11, 12, 15 en 16). Vaak is een recursieve oplossing een alternatief voor een iteratieve. Laten we eens een functie bekijken die de faculteit van een getal berekent. De faculteit van een positieve integer n, geschreven als n!, is gedefinieerd als het produkt van de gehele getallen van 1 tot en met n. De faculteit van nul wordt als speciaal geval behandeld en is per definitie gelijk aan 1. We hebben dus: n! = n* (n - 1) * (n - 2) * ... *3*2*1 voor n > 1 en 0! = 1 Dus: 5! = 5*4*3*2*1 = 120 De iteratieve versie van een functie voor de faculteit is: long int faculteit(int n) { int k; long produkt = 1L; if (n == 0) return (1L); else { for (k = n; k > 0; k--) produkt *= k; return (produkt); } } De definitie van de faculteitsfunctie wordt meestal recursief gegeven. We zien dat we n! = n* (n - 1) * (n - 2) *... *3*2*1 kunnen groeperen als n! = n* [ (n - 1) * (n - 2) *... *3*2*1] De groep tussen vierkante haken is natuurlijk gelijk aan de definitie van (n - 1)!. De recursieve definitie is daarom: n! = n*(n - 1)! met het speciale geval 0! = 1 We kunnen nu een functie ontwikkelen die de faculteit van een getal volgens deze recursieve definitie berekent. Programma 10-1 Geeft een tabel van faculteiten voor 0, 1, 2,... 10. De faculteiten worden met een recursieve functie berekend. #include <stdio.h> main() { int j; long int faculteit(const int); for (j = 0; j <= 10; j++) printf("%2d! is %ld\n”, j, faculteit(j)); } long int faculteit(const int n) { if (n == 0) return (1L); else return (n * faculteit(n-1)); } De uitvoer van het programma is: 0! 1! 2! 3! 4! 5! 6! 7! 8! 9! 10! is is is is is is is is is is is 1 1 2 6 24 120 720 5040 40320 362880 3628800 De functie faculteit is recursief omdat er een aanroep van de functie zelf in voorkomt. Laten we eens bekijken wat er gebeurt als de functie wordt aangeroepen om de faculteit van 5 te berekenen. Als de functie wordt binnengegaan, wordt de formele parameter n gelijk gemaakt aan 5. De if-statement stelt vast dat n niet nul is en levert de waarde af die wordt verkregen door het evalueren van n * faculteit (n - l) met n = 5,en dat is: 5 * faculteit (4) Deze expressie geeft aan dat de functie faculteit nog eens moet worden aangeroepen, dit keer om de waarde van faculteit(4) te berekenen. De vermenigvuldiging met 5 wordt uitgesteld tot faculteit (4) is berekend. We roepen de functie faculteit dus nog eens aan. Het actuele argument is nu 4. Elke keer dat een functie in C wordt aangeroepen, wordt er een eigen stel automatische variabelen en formele parameters toegewezen om mee te werken. Dat geldt ook voor recursieve functies. Het formele argument n dat bestaat als de functie wordt aangeroepen om de faculteit van 4 te berekenen, verschilt van het formele argument uit de eerste aanroep van de functie met argument 5. Nu n de waarde 4 heeft, levert de functie de waarde van de expressie 4 * faculteit (3) af. De vermenigvuldiging met 4 wordt weer uitgesteld tot de faculteit van 3 is berekend. Dit gaat zo door tot het formele argument de waarde 0 heeft. Dan hebben we de situatie die u in tabel 10- 1 ziet. faculteiten return (n * faculteit (n-1)) 5 4 3 2 1 5 * faculteit (4) = 5 * 4 * faculteit (3) = 4 * 3 * faculteit (2) = 3 * 2 * faculteit (1) = 2 * 1 * faculteit (0) = 1 * ? ? ? ? 0 Tabel 10-1 Als het formele argument n tot 0 is gereduceerd, laat de if -statement de functie onmiddellijk de long waarde 1 afleveren. De recursieve afdaling kan nu weer omhoog gaan en alle uitgestelde vermenigvuldigingen kunnen in omgekeerde volgorde worden uitgevoerd. Als we tabel 10-1 omkeren, krijgen we tabel 10-2. faculteit (n) 1 2 3 4 5 return (n * faculteit (n-1)) 1 * faculteit (0) = 1 * 2 * faculteit (1) = 2 * 3 * faculteit (2) = 3 * 4 * faculteit (3) = 4 * 5 * faculteit (4) = 5 * 1 1 2 6 24 = = = = = 1 2 6 24 120 Tabel 10-2 Voor wie dit misschien een wat kunstmatig voorbeeld vindt, bekijken we nu de algoritme van Euclides voor het bepalen van de grootste gemene deler van twee positieve gehele getallen m en n. De algoritme kan zo worden beschreven: GGD (n, m) = •als m > n: •als m = 0: •anders: GGD (m, n) n GGD(M, rest na deling van n door m) De recursieve functie kan rechtstreeks uit deze beschrijving worden afgeleid: int ggd(int n, int m) { if (m > n) return (ggd(m, n)); else if (m == 0) return (n); else return (ggd(m, n % m)); } Voor een niet-recursieve oplossing is enige vaardigheid bij het programmeren nodig. De implementatie is ook minder duidelijk: int ggd(int n, int m) { if (m > n) { int hulp = m; m=n; n=hulp; } while (n % m != 0) { int hulp = m; m=n % m; n=hulp; } return (m); } Het is in C ook mogelijk een functie te schrijven die een tweede functie aanroept die weer de oorspronkelijke functie aanroept. Dan hebben we een cyclus van functieaanroepen en spreken we van indirecte recursie. We kunnen bijvoorbeeld drie functies A, B en C hebben, waarbij A de functie B, B de functie C, en C de functie A aanroept. Om het mogelijk te maken dat een aanroep van een functie voorafgaat aan de declaratie van die functie, moet er een prototypedeclaratie worden gebruikt. Bij indirecte recursie is dat nodig, omdat het programma nooit zo kan worden ingedaald dat elke functiedeclaratie aan het gebruik van de functie voorafgaat. Neem bijvoorbeeld twee elkaar aanroepende functies A en B. Omdat A de functie B aanroept, kunnen we B eerder dan A plaatsen, maar dan wordt A eerder aangeroepen dan gedeclareerd. Recursie is niet altijd de efficiëntste oplossing voor een probleem. Veel problemen die recursief kunnen worden opgelost, kunnen ook met iteratie worden opgelost, zoals we hebben gezien. De oplossing is dan misschien minder elegant, maar wel efficiënter in termen van de uitvoeringstijd van het programma en het benodigde geheugen. Voor elke recursieve functieaanroep wordt een apart stuk geheugen in gebruik genomen om de waarde van de argumenten en de lokale variabelen in op te slaan. Daardoor zijn recursieve functies duurder in geheugenruimte. Elke recursieve functieaanroep vergt ook tijd voor het doorgeven van de argumenten, het in gebruik nemen van nieuw geheugen en het afleveren van het resultaat. Het overtuigendste argument ten gunste van recursieve functies is dat ze een goede afspiegeling zijn van recursief gedefinieerde datastructuren en algoritmen die recursief gedefinieerd zijn. Verder zijn sommige recursieve algoritmen vrijwel niet iteratief te schrijven. Recursieve functies die recursieve datastructuren weerspiegelen, zijn in overeenstemming met onze filosofie van programma's die met data overeenkomen. Een goede oefening in het schrijven van recursieve functies is het probleem van de toren van Hanoi. Gegeven zijn drie pennen en een aantal (64) schijven. Deze schijven hebben allemaal een andere diameter. De beginsituatie is die waarbij alle schijven op 1 pen (bijvoorbeeld pen 1) liggen. Hierbij liggen er alleen kleinere schijven boven een grotere. De eindsituatie is die waarbij alle schijven op pen 3 liggen, waarbij ook weer eveneens nooit een grotere pen boven een kleinere ligt. Om het probleem op te lossen moeten monniken schijf per schijf verplaatsen. Er moet voldaan zijn aan de voorwaarde dat alleen de verplaatste schijf (eventjes) niet op een pen mag liggen: alle andere schijven moeten steeds op een pen liggen. De monniken mogen ook steeds maar 1 schijf verplaatsen: de schijven wegen te zwaar om er meerdere te verplaatsen. Tijdens de bewerking moet er steeds voldaan zijn aan de voorwaarde dat er nooit een grotere schijf boven een kleinere mag liggen. Om nog een klein beetje een overzicht op de bewerking te hebben, is het beter het aantal schijven variabel te houden en dit in het begin zeer klein te kiezen, bijvoorbeeld 3 of 4. Wanneer men de PC het probleem met 64 schijven laat oplossen zal het zeer lang duren!!!