Recursieve functies

advertisement
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!!!
Download