Récursivité - Lycée Victor Hugo

publicité
Lycée Victor Hugo
MPSI-PCSI 2016-2017
Récursivité
La récursivité est un concept très général qui s’applique à divers objets et diverses démarches
et selon lequel dans la définition de cet objet ou la réalisation de cette démarche, il est fait appel à
l’objet lui-même ou à la démarche elle-même (récurrence, auto-référence, ...).
Par exemple, les cas suivants constituent des cas concrets de récursivité :
— Processus dépendant de paramètres et faisant appel à ce même processus sur d’autres paramètres plus simples (par exemple : suites récurrentes)
— Image contenant en elle-même des images similaires (voir figures ?? et ??)
— Concept défini en invoquant le même concept
— Algorithme qui s’appelle lui-même.
(a) Vache
(b) Arbre récursif
Figure 1 – Exemples de dessins récursifs
I
Définitions récursives
1
Définition
Une définition récursive d’un objet a pour caractéristique d’y voir figurer l’objet lui-même :
objet == (une définition qui utilise le mot objet)
2
Exemples
1. Fonctions mathématiques définies récursivement
(a) Multiplication de deux entiers naturels à l’école primaire (reformulation) :∀a ∈ N
∀n > 1, n × a = (n − 1) × a + a
(b) Puissance d’un nombre décimal a (ou dans un groupe G) : ∀n > 1, an = an−1 × a
(c) suite définie par récurrence : ∀n ∈ N, un+1 = f (un )
1
Lycée Victor Hugo
MPSI-PCSI 2016-2017
2. Arbre généalogique : concaténation de l’individu et des arbres généalogiques de son père et de
sa mère
3. Palindrome : palindrome entouré de deux lettres identiques
Question 1 Ces définitions sont-elles pertinentes ? Que manque-t-il ?
II
1
Algorithme récursif
Définition
Un algorithme récursif se caractérise par :
1. une base de récursivité correspondant à un ou plusieurs cas particuliers à partir desquels la
chaîne de récurrence (on dira plutôt d’induction) s’enclenche
2. le traitement des autres éléments faisant appel à un ou plusieurs appels récursifs
Le processus est semblable à la récurrence utilisée en mathématiques qui comporte de même :
1. une phase d’initialisation
2. la propriété dite d’hérédité
2
Structure générale en langage Python
A
Cas particulier
Ici le paramètre est un entier n, prend pour valeur initiale 0 et l’appel porte sur l’argument
précédent n − 1 du paramètre
def maFonction ( n ) :
if n == 0 :
return le r é sultat pour n = 0
else :
return le r é sultat calcul é à l ’ aide d ’ un appel à maFonction (n -1)
Ainsi pour l’exemple 1.(b), on définira :
def puissance (a , n ) :
if n == 0 :
return 1
else :
return a * puissance (a ,n -1)
B
Cas plus général
Le codage de manière générale d’une fonction récursive s’écrira selon le canevas :
maFonction ( x ) :
if x == ( base ) :
return ( r é sultat pour maFonction appliqu é e à la base )
else :
...
return ( r é sultat calcul é à partir d ’ un ou plusieurs appels r é cursifs à
maFonction )
Question 2 Quelle condition est souhaitable sur les arguments avec lesquels on appelle récursivement
la fonction ?
2
Lycée Victor Hugo
3
MPSI-PCSI 2016-2017
Déroulement : création et exécution d’une pile
Pour comprendre le processus de calcul, reprenons la fonction puissance et voyons ce qui se passe
lors de l’appel puissance(5, 2) permettant de calculer 52 ; tout se passe à la manière d’une pile.
— Au départ la pile est V ide.
— Juste après l’appel puissance(5, 2), une place est réservée en mémoire à une adresse, notons-la
A0, pour stocker puissance(5, 2) : l’état de la pile est donc après une action push
Adresse
A0
Valeur
?
— Dans l’exécution de puissance(5, 2), on appelle puissance(5, 1) que l’on multiplie par 5 : une
place est cette fois réservée en mémoire en haut de pile (nouveau push) à une adresse que nous
noterons A1 pour stocker puissance(5, 1) ; et on laisse en attente en A0 le calcul 5 × V aleur1
où la V aleur1 est la valeur « pointée » en adresse A1 ; l’état de la mémoire est à présent :
Adresse
Valeur
A1
?
A0
5 × V aleur1
— Dans l’exécution de puissance(5, 1) : même démarche qu’à l’étape précédente avec appel à
puissance(5, 0) et un nouveau push :
Adresse
A2
A1
A0
Valeur
?
5 × V aleur2
5 × V aleur1
— Dans l’exécution de puissance(5, 0), on rend directement comme résultat 1 :
Adresse
A2
A1
A0
Valeur
1
5 × V aleur2
5 × V aleur1
— Dans la pile ainsi constituée, on a une valeur en haut de pile : ceci a pour effet de supprimer
(action pop) la tête de pile et d’envoyer sa valeur comme argument à l’adresse A1 :
Adresse
A1
A0
Valeur
5
5 × V aleur1
— puis par le même procédé (nouvelle action pop)
Adresse
A0
Valeur
25
— La pile est de hauteur 1 et contient une valeur, nouvelle action pop qui rend la pile V ide : la
valeur finale est renvoyée comme résultat : 25
4
Exercice : jeu de briques
Coder une fonction Python qui rend le nombre de façons d’obtenir une ligne de briques de longueur
n en n’utilisant que des briques de longueur 2 ou 3 :
7→3
8→4
10 → 7
3
Lycée Victor Hugo
III
MPSI-PCSI 2016-2017
Intérêt de la récursivité
De nombreux problèmes sont résolus efficacement par l’ordinateur en ayant recours à des algorithmes récursifs. Citons par exemple :
— la résolution du problème des tours de Hanoï
— l’algorithme hautement efficace de tri par fusion d’une liste
Les algorithmes récursifs sont souvent élégants et plus faciles à mettre en place, cependant ils sont
souvent plus délicats à prouver !
IV
1
Problèmes soulevés par la récursivité
Terminaison des algorithmes récursifs
• Considérons la fonction Python définie par :
def u ( n ) :
if n == 0 :
return 1
else :
return n - u ( u (n -1) )
Question 3 Que va-t-il se passer ? D’où vient exactement le problème ?
• Considérons à présent la célèbre suite de Syracuse (dite aussi de Collatz) définie par la récurrence (où c ∈ N∗ est une constante d’initialisation donnée) :
a0 = c
et
∀ n ∈ N, an+1 =
a
 n
2
3a + 1
n
si an est pair
si an est impair
On conjecture que pour toute valeur de la constante c, il existe un entier n tel que an = 1.
Le plus petit entier n ainsi obtenu s’appelle la longueur du vol (on imagine le vol d’un oiseau,
an représentant son altitude à l’instant n).
A l’instar de cette suite, on définit alors la fonction Python suivante :
def longueurDuVol ( c ) :
assert c >0 , " le param è tre doit ê tre un naturel non nul "
if c == 1:
return 0
elif c % 2 == 0:
return 1+ longueurDuVol ( c // 2)
else :
return 1+ longueurDuVol (3* c +1)
Question 5 Quel problème pose cette fonction ?
• Ces deux exemples nous fournissent une illustration du théorème suivant :
Théorème
Soit une fonction récursive dont le paramètre est un entier naturel.
Pour qu’elle termine, il suffit que les appels récursifs portent sur des arguments strictement inférieurs à l’argument initial (et que le cas n = 0 soit un cas de base)
4
Lycée Victor Hugo
MPSI-PCSI 2016-2017
• Lorsque la fonction ne porte pas sur un paramètre entier naturel, il est suffisant, pour que cette
fonction termine, que :
1. l’ensemble des paramètres soit muni d’un ordre qui permet de dire qu’un argument est
"avant" un autre,
2. les appels récursifs portent sur des arguments qui sont strictement "avant" l’argument initial.
3. et les premiers arguments soient traités dans les cas des bases
On parle alors d’induction (au lieu de récurrence dans le cas de N)
2
Preuve des algorithmes récursifs
Il s’agit de démontrer que la fonction programmée rend bien pour le résultat escompté. En général
on réglera ce problème en même temps que celui de la terminaison.
• Cas particulier : le paramètre est un entier naturel
On utilisera un raisonnement par récurrence (simple, double, forte selon le type de récursivité
utilisée dans la fonction) en posant :
P(n) : l’algorithme termine et rend le résultat escompté pour la valeur n du paramètre
• Cas général : le paramètre fait partie d’un ensemble ordonné
On effectue ici un raisonnement par induction en posant :
P(x) : l’algorithme termine et rend le résultat escompté pour la valeur x du paramètre
Le raisonnement suit d’assez près le principe du raisonnement par récurrence, plus précisément
celui de récurrence forte.
La phase dite de base (similaire à l’initialisation d’un raisonnement par récurrence) sera :
P(base) est vraie
La phase dite d’induction (similaire à l’hérédité pour un raisonnement par récurrence) sera :
Supposons que P(y) est vraie pour tout y situé avant x, alors [...] P(x) est vraie
3
Complexité des algorithmes récursifs
Évaluation théorique de la complexité
La complexité d’un algorithme récursif peut (sous quelques réserves) s’évaluer à l’aide du nombre
d’opérations effectuées : il va dépendre de deux éléments :
— le nombre d’appels récursifs effectués
— le nombre d’opérations effectuées lors de l’appel de la fonction (ligne d’appel, souvent celle qui
suit else:, et qui traite le cas général)
Pour l’évaluer, on peut poser que C(n) est le nombre d’opérations effectuées, déterminer la formule de
récurrence vérifiée par C(n) par relecture de l’algorithme puis utiliser ses connaissances en mathématiques pour
obtenir sinon la formule explicite de C(n), du moins une comparaison O(...) avec une suite connue.
Évaluation de la complexité
Pour donner quelques idées, lorsque le paramètre dépend d’un entier naturel n :
• S’il s’agit d’une récursivité simple usuelle et que la ligne d’appel ne nécessite qu’un nombre
borné d’opérations, la complexité de l’algorithme sera O(n)
• S’il s’agit d’une récursivité simple et que la ligne d’appel avec la valeur n nécessite un nombre
d’opérations proportionnel à n, la complexité de l’algorithme sera O(n2 )
• S’il s’agit d’une récursivité double et que la ligne d’appel ne nécessite qu’un nombre borné
d’opérations, la complexité de l’algorithme sera O(2n )
• S’il s’agit d’une récursivité simple pour laquelle l’appel est fait sur un paramètre voisin de n/2
(méthode appelée “Diviser pour régner”) et que la ligne d’appel ne nécessite qu’un nombre
borné d’opérations, la complexité de l’algorithme sera O(log2 (n))
5
Lycée Victor Hugo
MPSI-PCSI 2016-2017
• S’il s’agit d’une récursivité simple pour laquelle l’appel est fait sur deux paramètres voisins de
n/2 (méthode “Diviser pour régner”) et que la ligne d’appel nécessite un nombre d’opérations
proportionnel à n, la complexité de l’algorithme sera O(n.log2 (n))
Attention, lorsqu’on dit que la ligne d’appel ne nécessite qu’un nombre borné d’opérations, cela signifie
que la borne est indépendante de la valeur des paramètres d’entrée.
Ces différents cas recouvrent de traiter la plupart des situations rencontrées cette année, notamment
l’algorithme dit d’exponentiation rapide et les algorithmes de tris.
Bilan
Certains algorithmes récursifs sont donc très coûteux en complexité temporelle (il peut y avoir le
même souci pour la complexité spatiale) et s’avèrent vite déraisonnables. On rappelle qu’un algorithme
de complexité O(n3 ) est lent, et devient irrecevable lorsque sa complexité est exponentielle (O(k n ) (avec
k > 1)).
Le choix de programmation s’avère donc crucial : un double appel récursif pour calculer le
terme général d’une suite définie par récurrence double est rédhibitoire (en O(2n )), mais en changeant
la nature de la suite (i.e. en considérant directement deux termes successifs), on augmente un peu
la complexité spatiale mais diminue drastiquement la complexité temporelle en faisant alors un seul
appel récursif (en O(n)).
Par ailleurs Python limite le nombre d’appels récursifs imbriqués (à des éléments distincts) à
1000. Pour de grandes valeurs de n, le seul recours possible est donc que ce nombre d’appels soit en
O(log2 (n)) ce qui encourage les méthodes “Diviser pour régner”.
Un ordinateur ultra-rapide ne pourra jamais récupérer les faiblesses d’un algorithme mal conçu.
Illustration par un exemple
La célèbre suite de Fibonacci définie par
F0 = 0,
F1 = 1
∀ n ∈ N, Fn+2 = Fn+1 + Fn
et
aura ainsi pour complexité :
• O(2n ) si on la programme sans trop réfléchir :
def Fibo ( n ) :
if n == 0:
return 0
elif n == 1:
return 1
else :
return Fibo (n -1) + Fibo (n -2)
• O(n) si on la programme sous forme itérative ou sous la forme récursive :
def Fibo_double ( n ) :
""" calcule le vecteur [ F ( n ) ,F ( n +1) ] """
if n == 0 :
return [0 ,1]
else :
resul = Fibo_double (n -1)
return [ resul [1] , resul [0]+ resul [1]]
def Fibo2 ( n ) :
return Fibo_double ( n ) [0]
• O(log2 (n)) si on utilise une exponentiation rapide pour calculer la puissance de matrice.
6
Lycée Victor Hugo
4
MPSI-PCSI 2016-2017
Occupation en mémoire : complexité spatiale
On a vu que le processus récursif a pour effet de créer en mémoire une pile avec un numéro d’adresse
(en fait un pour chaque valeur distincte de l’argument) et une valeur stockée (ou en attente) à cette
adresse. Ainsi chaque appel récursif à un argument nouveau alloue de la mémoire ; cet inconvénient
est rarement rédhibitoire (il l’était autrefois lorsque la place en mémoire coûtait cher), mais rappelons
que Python limite par défaut le nombre d’appels récursifs imbriqués à 1000, on peut changer cette
limite :
from sys import *
set recurs ionlim it (1500)
5
Difficulté intrinsèque de la notion de récursivité et prises de tête
La récursivité pose intrinsèquement des problèmes logiques sur lesquels se sont penchés de grands
logiciens du XXe siècle (Bertrand Russell, Kurt Gödel), mais aussi les linguistes et les philosophes.
Songez juste pour illustrer ce point à la difficulté logique posée par la phrase (récursive !) syntaxiquement correcte : « Cette phrase est fausse » ...
On peut démontrer qu’aucun algorithme ne pourra répondre de manière universelle (c’est-à-dire
pour toute fonction récursive) à la question “la fonction termine”.
7
Téléchargement