Exercice 1 : Récursivité et pile d'appels
Une fonction récursive s'appelle elle-même. Le langage ne fait rien de magique : il empile un cadre par appel, et c'est cette pile, visible sur la figure, qui explique tout le comportement.
def fact(n):
if n <= 1:
return 1
return n * fact(n - 1)- a) Suivez la figure et décrivez l'exécution de fact(4) : ordre d'empilement, moment où le premier résultat apparaît, ordre de dépilement.
- b) Quel est le cas de base ? Que se passerait-il si on l'oubliait ? Quel message obtiendrait-on exactement ?
- c) Combien de cadres la pile contient-elle au plus pour fact(n) ? Pour quelle valeur de n un programme Python échoue-t-il par défaut, et pourquoi cette limite existe-t-elle ?
- d) Réécrivez fact de façon itérative. Comparez les deux versions sur trois critères : lisibilité, mémoire, robustesse.
- e) Écrivez une fonction récursive qui inverse une chaîne de caractères, puis donnez sa trace sur la chaîne « abcd ».
Voir la correction
Réponses
- a) fact(4) : quatre cadres, résultat 24 à la remontée
- b) Sans cas de base : RecursionError
- c) cadres, limite vers 1 000
- d) Itératif : mémoire constante
- e) inverse(s[1:]) + s[0] donne dcba
a) L'appel fact(4) empile un cadre, constate que 4 est supérieur à 1 et appelle fact(3) : un deuxième cadre est empilé, puis un troisième pour fact(2), puis un quatrième pour fact(1). C'est à ce moment, et seulement à ce moment, que le premier résultat apparaît : fact(1) atteint le cas de base et renvoie 1 sans rien appeler. Le dépilement suit alors l'ordre INVERSE de l'empilement : fact(2) reçoit 1 et renvoie ; fact(3) reçoit 2 et renvoie ; fact(4) reçoit 6 et renvoie . Rien n'est calculé à la descente, tout l'est à la remontée : c'est exactement ce que montre la colonne de droite de la figure.
b) Le cas de base est n inférieur ou égal à 1, qui renvoie 1 sans appel récursif. Sans lui, chaque appel en déclencherait un autre indéfiniment, avec des valeurs de n de plus en plus petites qui ne rencontreraient jamais de condition d'arrêt. La pile grossirait jusqu'à saturation, et Python lèverait une RecursionError, avec le message « maximum recursion depth exceeded ». Le programme ne bouclerait donc pas éternellement : il s'arrêterait par épuisement de la pile, ce qui est une chance, car cela donne un message au lieu d'un gel.
c) Pour fact(n), la pile contient au plus n cadres, un par valeur de n jusqu'au cas de base. Python fixe par défaut une profondeur maximale de récursion de l'ordre de 1 000 : un appel à fact(1500) échoue donc, alors que le calcul lui-même n'aurait rien d'impossible. Cette limite existe parce que la pile d'exécution occupe une zone mémoire de taille fixe, réservée au démarrage du fil : sans garde-fou, une récursion incontrôlée l'écraserait et corromprait les données voisines. Mieux vaut une erreur nette qu'un comportement imprévisible.
d) Version itérative : on initialise un résultat à 1, puis pour k allant de 2 à n on multiplie le résultat par k, et on renvoie le résultat. Comparaison. LISIBILITÉ : la version récursive colle à la définition mathématique, la version itérative demande d'inventer un accumulateur ; avantage au récursif pour la factorielle. MÉMOIRE : la version récursive consomme n cadres de pile, la version itérative une seule variable ; avantage net à l'itératif. ROBUSTESSE : la version itérative fonctionne pour n valant un million, la récursive échoue au-delà de mille ; avantage net à l'itératif. On retient que la récursivité est un outil d'EXPRESSION, excellent quand le problème est naturellement récursif, comme un arbre, et coûteux quand une simple boucle suffit.
e) La fonction : si la chaîne est vide, renvoyer la chaîne vide ; sinon renvoyer l'inverse de tout sauf le premier caractère, suivi du premier caractère. En Python : return inverse(s[1:]) + s[0]. Trace sur « abcd » : inverse('abcd') appelle inverse('bcd') et gardera 'a' ; inverse('bcd') appelle inverse('cd') et gardera 'b' ; inverse('cd') appelle inverse('d') et gardera 'c' ; inverse('d') appelle inverse('') et gardera 'd' ; inverse('') renvoie ''. À la remontée : 'd', puis 'dc', puis 'dcb', puis 'dcba'. Le résultat est 'dcba'. On note au passage que chaque appel construit une nouvelle chaîne, donc cette version coûte un temps quadratique en la longueur, ce que la version itérative évite.