Vous êtes sur la page 1sur 10

Récursivité

Lycée Berthollet, MP/MP* 2021-22

I Structures récursives
Définition 1 Une structure est dite récursive lorsque la description de cette structure fait réfé-
rence à la structure elle-même.

Exemple 2 En calcul des propositions (logique), on dit qu’une expression est une proposition
si :
— soit c’est une variable logique (A, B...) ;
— soit elle est du type non(P), où P est une proposition ;
— soit elle est du type (P et Q), (P ou Q) ou (P =⇒ Q), où P est Q sont deux propositions.

Exemple 3 Les fractals sont des objets mathématiques “autosimilaires” qui peuvent être décrits
de manière récursive. Voir un exemple dans la suite.

Exemple 4 La notion de “mise en abyme” en littérature ou au cinéma est une sorte de récursi-
vité. Voir par exemple la publicité de la “vache-qui-rit”.

Enfin, ce qui nous préoccupe ici est la notion d’algorithme récursif :

Définition 5 Dans un langage informatique, une fonction est dite récursive, si elle s’appelle
elle-même soit directement, soit par le biais d’une autre fonction.

Beaucoup de langages modernes permettent de gérer les fonctions récursives. C’est le cas
du langage Python. Cette gestion nécessite de gérer les paramètres, variables locales et points
de retour de chaque appel de la fonction dans une ou plusieurs piles, comme nous le verrons
dans un exemple simple. Voici un exemple de fonction récursive en pseudo-code,
√ qui calcule le
n-ième terme de la suite (un )n∈N définie par u0 = 0 et la relation un+1 = 1 + un :

fonction u(n):
si n==0, retourner 0
sinon, retourner sqrt(1+u(n-1))

1
II Fonctions récursives en Python
Voir le fichier C02-recursivite.py en annexe

1 Factorielle
On programme dans un premier temps la factorielle de manière récursive et on observe la
pile des paramètres ainsi que la pile des appels. On remarque que le nombre d’appels imbriqués
est limité en Python (par défaut à 1000). On programme aussi cette factorielle de manière non
récursive et on remarque que l’ordre de complexité est inchangé (linéaire dans les deux cas),
mais que la fonction non récursive est environ 2 fois plus rapide.

2 Puissances
On programme le calcul des puissances entières positives d’un nombre de manière récursive
(linéaire en l’exposant n), puis de manière récursive et rapide (logarithmique en l’exposant n).

3 PGCD
L’algorithme d’Euclide se prête très bien à la récursivité avec une complexité dont on pour-
rait montrer qu’elle est au pire logarithmique.

4 Fibonacci
Le calcul naïf de la suite de Fibonacci de manière récursive est d’une complexité catastro-
phique, alors qu’un algorithme itératif est très facile à écrire et efficace (linéaire). On montre
comment faire un calcul récursif linéaire, mais cela reste sensiblement plus lent que le calcul
itératif précédent.
On représentera au tableau l’arbre des appels pour avoir une idée de la complexité de l’al-
gorithme naïf.

5 Permutations de séquences
On montre comment créer la liste de toutes les permutations d’une séquence donnée (liste
ou chaîne de caractères) de manière récursive (ce qui est beaucoup plus délicat de manière non
récursive). On mettra la complexité en lien avec l’arbre des appels.

6 Flocon de neige
Il est possible de dessiner assez facilement certaines courbes fractales à l’aide de la récur-
sivité et du module graphique turtle. On donne l’exemple archi-classique du flocon de neige
de Van Koch.

2
III Correction, terminaison, complexité
Sur certains des exemples précédents, on voit que la récurrence (normale ou forte) est l’ins-
trument adapté aux preuves de correction dans le cas récursif, tandis que la terminaison repose
sur la stricte décroissance de la taille du ou des paramètres des appels successifs. On donne
aussi des exemples de calcul de complexité.

1 Factorielle
Terminaison
On considère la fonction Python fact du fichier C02-recursivite.py en annexe. Lors des
appels successifs imbriqués provoqués par l’appel fact(n), les valeurs prises par les arguments
n successifs forment une suite d’entiers positifs ou nuls strictement décroissante. Cette suite est
donc finie et le nombre d’appels imbriqués est fini. Donc l’appel de la fonction fact sur un
entier positif ou nul termine toujours.

Complexités
Il est évident que la suite précédente est n, n − 1, ..., 1, 0. Il y a donc n + 1 appels imbri-
qués. Excepté l’appel récursif, le code de chaque appel nécessite un test et une multiplication,
sauf le dernier appel (argument 0) qui nécessite seulement un test. Si on fait l’approximation
que le temps d’exécution d’une multiplication est constant (i.e. si on travaille avec des entiers
standard, codés sur 32 ou 64 bits), on en déduit que l’exécution de l’appel fact(n) s’effec-
tue en un temps de l’ordre de n, i.e. la complexité temporelle est linéaire. Cependant, cette
approximation est inappropriée, puisque la croissance de la fonction factorielle est très forte.
Par exemple, 100 ! s’écrit avec 525 chiffres en base 2, donc n’est pas un entier standard. Il
est donc exclu, même pour des entiers n petits, de considérer que les résultats intermédiaires
sont des entiers standard. Les multiplications ne se font alors pas en temps constant. En parti-
culier, il faut au moins calculer tous les chiffres (dans la base 2 de codage) du résultat. Si on
reste dans le cas où les nombres k ≤ n sont des entiers standard, une multiplication de k par
(k − 1)! s’exécute alors en un temps proportionnel à log(k!), d’où une complexité de l’ordre
de ∑nk=1 log(k!) = log (∏nk=1 k!). En particulier, la complexité est d’ordre supérieur ou égal à
Θ(log(n!)). En utilisant la formule de Stirling, une composition d’équivalents dans un loga-
rithme (autorisé) et le fait que log(n) = o(n), Θ(log(n!)) = Θ(n log(n)), ce qui est strictement
plus que linéaire. La complexité temporelle est donc strictement sur-linéaire si on considère des
entiers n tels que n! de puisse pas se coder de manière standard.
En ce qui concerne la complexité spatiale, elle repose sur la pile des environnements locaux
des appels imbriqués d’une part et le stockage du résultat intermédiaire d’autre part. Pour la pile
des appels, si l’entier n initial est standard, le stockage de chaque valeur locale de n prend une
place constante et la pile des appels necessite une place mémoire linéaire en n. En revanche,
pour la même raison que ci-dessus, la taille de stockage du résultat intermédiaire courant est
maximale pour le dernier retour et de l’ordre de log(n!), soit en Θ(n log(n)). La complexité
spatiale de la fonction fact est donc non linéaire, même si on considère que les entiers inférieurs
ou égaux à l’argument initial ont tous la même taille de stockage.

3
Correction
Pour n ∈ N on définit l’assertion de récurrence suivante :
An : “En notant n le nom d’un entier Python représentant l’entier naturel n, l’appel
fact(n) retourne un entier Python représentant l’entier naturel n!.”
On fait alors une démonstration par récurrence sur n :
Initialisation : Si n = 0, le test de l’appel fact(0) est positif donc la valeur retournée est 1 = 0!.
Héridité : Supposons que An−1 soit vraie pour un certain n ≥ 1. Lors de l’appel fact(n), le
test est négatif, donc la valeur retournée est n fois la valeur retournée par fact(n-1), qui est
(n − 1)! par hypothèse de récurrence. Comme n · (n − 1)! = n!, An est vraie.
Par le principe de récurrence, on a donc montré que, pour tout n ∈ N, l’appel fact(n)
retourne un entier représentant n!.

2 Puissance rapide
Terminaison
On considère la fonction Python puissRapide du fichier C02-recursivite.py en an-
nexe. Lors des appels successifs imbriqués provoqués par l’appel puissRapide(x,n), les va-
leurs prises par les arguments n successifs forment une suite d’entiers positifs ou nuls stric-
tement décroissante, puisque, pour tout n ∈ N? , le quotient q de la division euclidienne de n
par 2 est strictement plus petit que n, car, en notant r le reste de cette division, q ≤ 2q+r n
2 = 2.
Cette suite est donc finie et le nombre d’appels imbriqués est fini. Donc l’exécution de l’appel
puissRapide(x,n) termine toujours.

Complexités
Pour simplifier les choses, on suppose ici que l’argument x est un flottant et que n est un
entier standard (au sens qu’il peut être codé en 32 ou 64 bits, suivant la machine), ce qui entraîne
que les opérations arithmétiques se font en temps constant.
On note n = n0 , n1 , ..., nk = 0 la suite finie ci-dessus des valeurs des arguments successifs n
des appels imbriqués. Pour chaque i ∈ [[0, k − 1]], ni+1 est le quotient de la division euclidienne
de ni par 2. On note ri le reste de cette division. Remarquons que le dernier reste est rk−1 =
nk−1 − 2nk = nk−1 , donc est non nul et donc vaut 1. Une récurrence immédiate donne
k−1
n = r0 + 2n1 = r0 + 2r1 + 4n2 = ... = ∑ ri2i,
i=0

ce qui est l’écriture de n en base 2. Comme rk−1 = 1, 2k−1 ≤ n < 2k et donc k = blog2 (n)c + 1.
Comme l’exécution des instructions de chaque appel de la fonction, hors l’appel récursif, se fait
en temps compris entre deux constantes (un test et entre zéro et quatre opérations arithmétiques),
la complexité temporelle est logarithmique.
La pile des environnements locaux des appels est de taille logarithmique en n d’après ce
qu’on vient de voir, et la place mémoire de ces environnements est constante (un flottant et
un entier standard), donc la taille mémoire de la pile est en Θ(log(n)). Par ailleurs, le résultat
intermédiaire courant est un flottant donc prend une place fixe en mémoire. Ainsi la complexité
spatiale est logarithmique.

4
Correction
On considère un réel x représenté par un flottant x. Pour tout n ∈ N, on considère l’assertion
de récurrence
An : “En notant n le nom d’un entier Python représentant l’entier naturel n, l’appel
puissRapide(x,n) retourne un flottant représentant le réel xn .”
On fait alors une démonstration par récurrence forte sur n :
Initialisation : Si n = 0, le test de l’appel puissRapide(x,0) est positif donc la valeur retournée
est 1 = x0 .
Héridité : Soit n ∈ N? . Supposons que Ak soit vraie pour tout k < n. Lors de l’appel de fonction
puissRapide(x,n), le premier test est négatif. On note q le quotient et r le reste de la division
de n par 2. D’après l’hypothèse de récurrence, l’appel puissRapide(x,n//2) retourne xq .
Dans les deux cas du second test, la valeur retournée est alors (xq )2 xr = x2q+r = xn , donc An est
vraie.
Par le principe de récurrence forte, on a donc montré que, pour tout n ∈ N, l’appel de fonc-
tion puissRapide(x,n) retourne un flottant représentant le réel xn .

IV Conclusion
Au vu des exemples ci-dessus, on tire quelques enseignements.

Quand utiliser la récursivité ?


— Lorsqu’on ne sait pas faire autrement ;
— Lorsqu’on travaille sur des structures elle-même récursives (listes, arbres, fractales,...) ;
— Lorsque l’algorithme récursif est très élégant, et ne change pas l’ordre de grandeur de
la complexité par rapport à un algorithme itératif. Attention cependant à la limite du
nombre d’appels imbriqués !

Quand ne pas utiliser la récursivité ?


— Lorsqu’il existe un algorithme non-récursif raisonnablement simple et élégant faisant la
même chose ;
— Lorsque l’arbre des appels est potentiellement très profond ;
— S’il y a des paramètres ou des variables locales “volumineux” dans la fonction.

5
V Annexe : code
# ------------------------------------------------------------------------
# Mesure du temps d’execution d’une fonction
# ------------------------------------------------------------------------

import time

def teste(appel):
print(appel+’ ---> ’,end=’’)
tInit = time.clock()
res = eval(appel)
tFin = time.clock()
print(res,tFin-tInit)

# ------------------------------------------------------------------------
# Factorielle
# ------------------------------------------------------------------------

def facto(n):
if n==0:
return 1
else:
return n*facto(n-1)

print(facto(10))
# produit une erreur:
# print(facto(1000))

# Visualisation de la pile des variables locales et des appels

from inspect import *

def factoVisu(n):
print(locals())
if n==0:
pile = stack()
for i in range(len(pile)):
print(getframeinfo(pile[i][0]))
return 1
else:
res = n*factoVisu(n-1)
print(locals())
return res

print(factoVisu(10))

6
# Comparaison avec la version non recursive

def factoIter(n):
res = 1
for i in range(2,n+1):
res *= i
return res

teste(’factoIter(900)’)
teste(’facto(900)’)

print(factoIter(10000))

# ------------------------------------------------------------------------
# Puissance
# ------------------------------------------------------------------------

def puiss(x,n):
""" Calcule x**n de facon recursive """
if n==0:
return 1
else:
return x*puiss(x,n-1)

print(puiss(2,32))

def puissRapide(x,n):
""" Calcule rapidement x**n de facon recursive """
if n==0:
return 1
else:
y =puissRapide(x,n//2)
if (n%2==0):
return y*y
else:
return x*y*y

teste(’puissRapide(3,900)’)
teste(’puiss(3,900)’)

7
# ------------------------------------------------------------------------
# PGCD
# ------------------------------------------------------------------------

def pgcd(a,b):
if b==0:
return a
else:
return pgcd(b,a%b)

print(pgcd(10,6))
print(pgcd(1235674567543467567,908765345432342453243234254254233212356453456))

# ------------------------------------------------------------------------
# Fibonacci
# ------------------------------------------------------------------------

def fibo(n):
if n<2:
return n
else:
return fibo(n-1)+fibo(n-2)

for i in range(10):
print(fibo(i),end=’ ’)
print(fibo(30))
print(fibo(35))

def fiboIter(n):
if n<2:
return n
else:
Fnm1,Fn = 0,1
for i in range(n-1):
Fnm1,Fn = Fn,Fnm1+Fn
return Fn

for i in range(10):
print(fiboIter(i),end=’ ’)

print(fiboIter(35))
print(fiboIter(100000)

def fiboRecBis(n):
""" Retourne F(n-1) et Fn """
if n==0:
return 1,0

8
elif n==1:
return 0,1
else:
Fnm2,Fnm1 = fiboRecBis(n-1)
return Fnm1,Fnm2+Fnm1

print(fiboRecBis(35)[1])

# Comparaison recursif/iteratif

teste(’fiboIter(900)’)
teste(’fiboRecBis(900)[1]’)

# ------------------------------------------------------------------------
# Permutations
# ------------------------------------------------------------------------

def permChaine(ch):
if len(ch)==1:
return [ch]
else:
res = []
for i,car in enumerate(ch):
res += [car+s for s in permChaine(ch[:i]+ch[i+1:])]
return res

print(permChaine(’abc’))
print(permChaine(’abricot’))

def permListe(liste):
if len(liste)==1:
return [liste]
else:
res = []
for i,item in enumerate(liste):
res += [[item]+s for s in permListe(liste[:i]+liste[i+1:])]
return res

print(permListe(list(’abc’)))

9
# ------------------------------------------------------------------------
# Flocon
# ------------------------------------------------------------------------

from math import *


from turtle import *

def fractTurtle(n):
if n==0:
forward(1)
else:
fractTurtle(n-1)
right(60)
fractTurtle(n-1)
left(120)
fractTurtle(n-1)
right(60)
fractTurtle(n-1)

def floconTurtle(n):
clearscreen()
speed(’fastest’)
# hideturtle()
getpen()
setheading(240)
fractTurtle(n)
left(120)
fractTurtle(n)
left(120)
fractTurtle(n)

floconTurtle(5)
exitonclick()

# ------------------------------------------------------------------------
# Arbres et tris seront vus plus tard
# ------------------------------------------------------------------------

10

Vous aimerez peut-être aussi