Recursivite
Recursivite
Recursivite
Récursivité
I. Introduction
I.1. Définition
Déf 1:
Une procédure récursive est une procédure qui s’appelle elle-même, une ou plusieurs fois.
Une procédure non récursive ne comporte donc que des appels à d’autres procédures.
La récursivité est un domaine très intéressant de l’informatique, un peu abstrait, mais très élégant ; elle permet
de résoudre certains problèmes d’une manière très rapide, alors que si on devait les résoudre de manière itérative,
il nous faudrait beaucoup plus de temps et de structures de données intermédiaires.
L’exemple le plus simple (même s’il n’est pas très intéressant) pour décrire le principe de la récursivité est celui
de la fonction factorielle.
Celle-ci est définie de façon directe par :
1
si n = 0
n
n! = Y
i si n ∈ N .
∗
i=1
Une procédure récursive doit toujours comporter un ou plusieurs cas ≪ de base ≫ qui sont définis de manière
non récursive (le cas n = 0 dans l’exemple ci-dessus).
3 def factoVisu(n):
4 print(locals())
5 if n==0:
6 pile = stack()
7 for i in range(len(pile)):
8 print(getframeinfo(pile[i][0]))
9 return 1
10 else:
11 res = n*factoVisu(n-1)
12 print(locals())
13 return res
14
15 print(factoVisu(5))
{'n': 5}
{'n': 4}
{'n': 3}
{'n': 2}
{'n': 1}
{'n': 0}
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=62, function='fa
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=65, function='fa
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=65, function='fa
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=65, function='fa
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=65, function='fa
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=65, function='fa
Traceback(filename='pythontex-files-recursivite\\sympy_default_default.py', lineno=69, function='<m
{'n': 1, 'res': 1}
{'n': 2, 'res': 2}
{'n': 3, 'res': 6}
{'n': 4, 'res': 24}
{'n': 5, 'res': 120}
120
I.3. Efficacité
• Un programme récursif bien conçu est un peu plus long à l’exécution que sa version itérative corres-
pondante, d’autant plus que celle-ci est
√ simple, comme le montre l’exemple suivant où l’on a calculé les
termes de la suite définie par un+1 = 1 + un :
1 import timeit
2 from math import sqrt
3
4 import sys
5 sys.setrecursionlimit(10000)
6
7 def iteratif(n):
8 u = 0
9 for i in range(n):
10 u = sqrt(1 + u)
11 return u
12
13 def recursif(n):
14 if n == 0:
15 return 1
16 else:
17 return sqrt(1 + recursif(n-1))
18
• Cela peut être encore pire si l’on abuse de la récursivité. Considérons par exemple la suite de Fibonacci
définie par :
u0 = 0 , u1 = 1 et ∀ n ∈ N, un+2 = un+1 + un
En voici une version itérative :
et la version récursive :
u5
u4 u3
u3 u2 u2 u1
u2 u1 u1 u0 u1 u0
u1 u0
Pour calculer u5 , il faut d’abord calculeru4 et u3 . Or pour calculer u4 , il faut calculer u3 etc. . . Puisque
les appels récursifs sont indépendants, la valeur de u2 par exemple va être calculée 3 fois. Pour des valeurs
de n supérieures, on s’aperçoit qu’un grand nombre de valeurs des ui sont calculées plusieurs fois.
Plus précisément, notons A(n) le nombre d’additions effectuées par la version récursive. A(n) vérifie :
√ n+1 √
1 1+ 5 1+ 5
On voit donc que A(n) ∼ √
n→+∞ 5 2
(avec
2
≈ 1, 618 ), alors que le nombre d’additions
dans la version itérative est tout simplement égal à n − 1 ...
• Il existe cependant une implémentation du calcul du n-ième élément de la suite de Fibonacci qui contourne
cette difficulté.
Pour obtenir le n e terme de la suite de Fibonacci classique, il suffit donc d’appeler fibo(n, 0, 1). Dans
cette version, les valeurs utiles ne sont calculées qu’une seule fois. On a utilisé pour cela la relation :
On remarque que dans ce dernier programme l’appel récursif est la dernière instruction exécutée : il n’y
a pas de traitement du résultat de l’appel récursif. Un tel programme est dit récursif terminal. Il y a
une différence fondamentale entre la version récursive et la version récursive terminale : dans la version
récursive simple (algorithme 4 ou algorithme 2) , l’appel récursif n’est pas la dernière opération réalisée
(il est utilisé dans un calcul), il faut donc que la machine garde en mémoire l’état de son calcul. Cela
conduit aux problèmes de mémoire et de temps de calcul cités précédemment. Dans la version récursive
terminale, l’appel récursif est la dernière action réalisée : l’interpréteur ou le compilateur sont capables
de détecter ces situations et traitent alors le programme comme un programme itératif, ce qui n’engendre
pas les problèmes cités précédemment.
• Une autre idée pour éviter les appels redondants est la mémoı̈sation. Cela consiste à mémoriser au fur et
à mesure, dans une variable globale, les valeurs prises par la fonction. Cela consomme malheureusement
beaucoup de place mémoire !
Dans la version ci-dessous, on a utilisé une liste pour stocker les valeurs calculées au fur et à mesure ; au
moment de calculer fibo(n), on commence par vérifier s’il existe dans la liste une valeur correspondant à
n ; si oui, on renvoie directement la valeur stockée, sinon on la calcule récursivement et on stocke cette
nouvelle valeur dans la liste.
Algorithme 6 : Suite de Fibonacci récursive avec
mémoı̈sation
Données : n : entier naturel, a et b : valeurs des deux termes
initiaux de la suite
Résultat : n e terme de la suite de Fibonacci
def fibo memo(n) :
liste = [N one] ∗ (n + 1);
liste[0] = 0 ;
liste[1] = 1 ;
def fibo(n) :
si liste[n] 6= None alors
retourner liste[n]
sinon
x = f ibo memo(n − 1) + f ibo memo(n − 2) ;
liste[n] = x ;
retourner x
finsi
fin
retourner fibo(n)
fin
ce qui prouve que le nombre d’appels est égal à ⌊log2 (n)⌋ + 1 . Puisqu’il y a un test, une division entière et au
maximum 2 multiplications par appel, le nombre d’opération est un O (log2 (n)).
Pour écrire une version itérative de cet algorithme, nous allons relier son fonctionnement à la l’écriture en base
2 de l’exposant n.
Par exemple, si n = 89 , n s’écrit en base 2 : n = 1 + 8 + 16 + 64 = 1011001. Et l’on a :
La suite x, x2 , x4 , x8 , x16 , x32 , x64 s’obtient par des élévations au carré successives ; le résultat final est le
produit de ceux des termes de cette suite qui correspondent à un chiffre 1 dans l’écriture en base 2 de n.
Cela donne l’algorithme itératif suivant :
Une version itérative de cet algorithme est plus difficile. On utilise ici encore l’écriture binaire de n, mais cette
fois-ci on la lit de gauche à droite. Par exemple, dans le cas de n = 89 = 1 + 8 + 16 + 64 = 1011001, on écrira
x = x64 ⋆ x16 ⋆ x8 ⋆ x .
Dans cette écriture, x64 correspond à x élevé au carré 6 fois, 6 correspondant à la position du 1 de gauche,
puis x16 correspond à x élevé au carré 4 fois, 4 correspondant à la position du second 1 à partir de la gauche
etc. . . Cela donne le programme itératif suivant :
3 import sys
4 sys.setrecursionlimit(10000)
5
11 if n == 0:
12 return 1
13 else:
14 return x * puiss_rec1(x, n-1)
15
42 debut = time()
43 for i in range(1,100):
44 for j in range(0,nmax):
45 puiss_rec2(i,j)
46 print("Récursif 1ère version", "i<=100 et j<=",nmax, "--->",time() - debut)
47
48 debut = time()
49 for i in range(1,100):
50 for j in range(0,nmax):
51 puiss_rec3(i,j)
52 print("Récursif 2ème version", "i<=100 et j<=",nmax, "--->",time() - debut)
53
90 debut = time()
91 for i in range(1,20):
92 for j in range(0,nmax):
93 puiss_iter1(i,j)
94 print("Itératif primaire", "i<=20 et j<=",nmax, "--->",time() - debut)
95
96 debut = time()
97 for i in range(1,100):
98 for j in range(0,nmax):
99 puiss_iter2(i,j)
100 print("Itératif 1ère version", "i<=100 et j<=",nmax, "--->",time() - debut)
101
1 def permutations(E):
2 n = len(E)
3 if n == 1:
4 return [E]
5 else:
6 liste_permus = []
7 for i in range(0, n):
8 C = E[:i] + E[i+1:]
9 L = permutations(C)
10 for perm in L:
11 perm.append(E[i])
12 liste_permus.append(perm)
13 return liste_permus
14
Par exemple, en appelant ce programme avec les arguments (3, “gauche”, “milieu”, “droite”) l’affichage produit
est le suivant : gauche → droite ; gauche → milieu ; droite → milieu ; gauche → droite ; milieu → gauche ;
milieu → droite ; gauche → droite.
Pour résoudre le problème à n tours, on a besoin de résoudre deux fois le problème à n − 1 tours, et d’effectuer
une opération supplémentaire. La relation de récurrence sur le nombre d’opérations Tn est donc :
Tn = 2Tn−1 + 1 soit Tn + 1 = 2(Tn−1 + 1)
Indications : On démontrera d’abord que si A a pour coordonnées (x, y) et B pour coordonnées (z, t) les
coordonnées de C sont (u, v) avec :
x−y+z+t x+y−z+t
u= et v= ·
2 2
import math
import matplotlib.pyplot as plt
Écrire à partir de là deux fonctions récursives permettant d’obtenir les figures ci-dessous.
30
10
20
5
0
10
−5
0
−10
−10
−15
−20
−20
−25
−30
−10 −5 0 5 10 15 20 25 −20 −10 0 10 20
import math
import matplotlib.pyplot as plt
if rempli:
plt.fill(X, Y, color='blue')
else:
plt.plot(X, Y, color='blue')
Écrire une fonction permettant de réaliser les graphiques ci-dessous (les triangles sont équilatéraux).
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
Déf 2:
Un algorithme est de type Diviser Pour Régner s’il comprend les étapes suivantes :
– condition d’arrêt ;
– découpage du problème en sous-problèmes ;
– traitement des sous-problèmes ;
– rassemblement des solutions aux sous-problèmes en une solution au problème principal.
Le tri fusion par exemple respecte cette philosophie : le problème principal est coupé exactement en deux, chaque
sous-liste est triée, puis on effectue une fusion des deux sous-listes triées pour obtenir la liste principale triée.
Dans le cas du tri fusion, le découpage ne coûte rien, mais la recombinaison est coûteuse. à l’inverse dans le tri
rapide, le découpage est coûteux (on sépare la liste autour d’un pivot), mais la recombinaison est immédiate.
2n−2
X X
alors PQ = ck X k avec ck = ai b j
k=0 i+j=k
Nous exposons ci-dessous l’algorithme de Karatsuba (1960), qui a une complexité en O (nlog 2 3
) ≈ O (n1,58 ).
On suppose dans un 1er temps que n = 2m est pair. Pour P, Q ∈ Kn−1 [X], on écrit :
(
P = P1 + X m P2
Q = Q1 + X m Q2
R1 = P1 Q1 , R2 = P2 Q2 et R3 = (P1 + P2 )(Q1 + Q2 ) .
On a alors :
P Q = R1 + (R3 − R2 − R1 )X m + X 2m R2
On ne réalise ainsi que 3 multiplications de polynômes de tailles moitié (on ne compte pas les multiplications
par X m et X 2m qui reviennent simplement à décaler les indices) ; on a évidemment fait des additions
supplémentaires, mais l’on considère le coût d’une addition de polynômes comme négligeable devant celui d’une
multiplication (en effet, c’est en O (n) contre O (n2 )).
Puisque les additions et les multiplications par X m ou X 2m prennent un temps proportionnel à n, on peut
dire que le temps d’exécution T (n) vérifie une relation de la forme :
n
T (n) = 3T + an ·
2
On utilise alors le théorème général suivant.
Théorème 1:
n
Si T vérifie la relation de récurrence T (n) = aT + αnc , avec a, b et c des entiers positifs et α
b
un réel positif, alors :
– si c < logb (a) , on a T (n) = O (nlogb (a) ) ;
– si c = logb (a) , on a T (n) = O (nc logb (n)) ;
– c > logb (a) , on a T (n) = O (nc ) .
Démonstration:
– Supposons dans un 1er temps que n est une puissance de b : n = bm .
On a alors T (bm ) = aT (bm−1 ) + αbmc .
T (bm ) T (bm−1 ) αbmc T (bm ) T (bm−1 ) αbmc
En divisant le tout par am (on suppose a 6= 0 ), on obtient m
= m−1
+ m , soit encore m
− m−1 = m .
a a a a a a
En sommant ces relations pour k 6 m , on a une somme télescopique dont le résultat est :
m
T (bm ) X bkc
m
− T (1) = α .
a ak
k=2
soit :
m
bc k
X
T (bm ) = am T (1) + αam × .
a
k=2
Trois cas se distinguent :
– Si c = logb (a) : dans ce cas bc = a et la somme vaut m − 1 et on a T (bm ) = am T (1) + α(m − 1)am , ce qui nous donne
puisque m = logb (n) :
T (n) = O (logb (n)alogb (n) ) = O (nc logb (n)).
– Si c < logb (a) : dans ce cas, la somme converge et on a T (bm ) = O (am ) soit encore T (n) = O (alogb (n) ) = Θ(nlogb (a) ) .
bc m bc
– Si c > logb (a) : alors la somme diverge donc est équivalente quand m → +∞ à et on a T (bm ) = O (am ×( )m )
a a
soit encore T (n) = O (nc ) .
– Dans le cas général, on encadre n entre deux puissances successives de b et on obtient les mêmes résultats (car la fonction T
est croissante, comme on peut le vérifier par récurrence).
La figure ci-dessous illustre l’écart des performances ; on a également fait un tracé des temps en utilisant une
échelle logarithmique : si T (n) ≈ anα alors ln(T (n)) est une fonction affine de ln n, de pente α.
On remarque que les pentes calculées de ces droites sont très proches des valeurs théoriques (pour calculer ces
pentes, j’ai utilisé le module stats de scipy et la fonction linregress).
C1,1 = A1,1 B1,1 + A1,2 B2,1 C1,2 = A1,1 B1,2 + A1,2 B2,2
C2,1 = A2,1 B1,1 + A2,2 B2,1 C2,2 = A2,1 B1,2 + A2,2 B2,2
Mais Strassen a eu l’idée de définir des matrices intermédiaires, au nombre de 7. L’idée principale est que la
somme de matrices demande un nombre d’opérations plus faible que la multiplication et qu’il faut donc réduire
le nombre de multiplications, quitte à augmenter le nombre d’additions. Les matrices intermédiaires sont les
suivantes :
M1 = (A1,1 + A2,2 )(B1,1 + B2,2 ) M2 = (A2,1 + A2,2 )B1,1 M3 = A1,1 (B1,2 − B2,2 )
M4 = A2,2 (B2,1 − B1,1 ) M5 = (A1,1 + A1,2 )B2,2 M6 = (A2,1 − A1,1 )(B1,1 + B1,2 )
M7 = (A1,2 − A2,2 )(B2,1 + B2,2 )
On récupère alors la matrice C grâce aux relations :
C1,1 = M1 + M4 − M5 + M7
C2,1 = M2 + M4
C1,2 = M3 + M5
C2,2 = M1 − M2 + M3 + M6
n
On n’a donc besoin que de 7 multiplications matricielles de matrices carrées de taille et 18 sommes, ce qui
2
apporte un vrai plus en terme de complexité comme nous allons le voir.
Dans l’algorithme naı̈f de multiplication par blocs, si T (n) représente le nombre d’opérations, on a la relation
de récurrence suivante : n n 2
T (n) = 8 ∗ T +4 .
2 2
Avec les notations du théorème précédent, on a donc α = 1, a = 8, b = 2 et c = 2 . D’où c < logb (a) et donc
T (n) = O (n3 ) .
Dans le cas de l’algorithme de Strassen, on trouve
n 2
T (n) = 7T (n) + 18
2
9
soit α = , a = 7, b = 2 et c = 2 . Encore une fois, c < logb a et donc T (n) = O (nlog2 (7) ) = Θ(n2,807 ) ce qui est
2
mieux. . .
Implementation
On se contentera d’écrire l’algorithme de Strassen pour des matrices carrées dont la taille est une puissance de
2 (ce n’est pas vraiment une limitation car n’importe quelle matrice peut devenir de cette forme en complétant
les lignes et les colonnes par des 0.)
Pour les matrices de ≪ petite ≫ taille, on écrira une procédure de calcul du produit matriciel qui utilise les
formules habituelles, car l’algorithme de Strassen fait perdre du temps si n est petit.
Les matrices seront définies par des tableaux de NumPy. On rappelle ci-dessous quelques commandes sur ces
tableaux.
import numpy as np
A = np.random.random( (x,y) )
# renvoie une matrice de dimensions x*y avec des coefficients
# aléatoires entre 0 et 1
A.shape
# renvoie le tuple (x,y) correspondant à la taille de la matrice
A[i,j]
# élément d'indices i et j
# les indices commencent à 0
A[i:]
# renvoie la ième ligne de A
A[:,j]
# renvoie la jeme colonne de A
np.concatenate( (A,B), axis=0)
# concatène des matrices de tailles compatibles
# dans la direction verticale
np.concatenate( (A,B), axis=1)
# concatène des matrices de tailles compatibles
# dans la direction horizontale
np.dot(A,B)
La figure ci-dessous illustre les performances de la méthode comparée à la multiplication matricielle classique,
mais on reste bien loin de la fonction implémentée dans NumPy !
Déf 3:
Si A est un polynôme de degré n dont les coefficients sont a0 , a1 , . . . , an et ω une racine primitive ℓ -ième
de l’unité, la transformée de Fourier de A est la donnée des valeurs (A(ω 0 ), A(ω 1 ), . . . , A(ω ℓ−1 ).
Pour représenter un polynôme informatiquement, on peut utiliser plusieurs méthodes : soit le polynôme est
représenté par ses coefficients sous la forme d’une liste, soit par sa valeur en certains points prédéfinis. Ces deux
représentations ont leur avantage. Par exemple la représentation par points/valeurs est très efficace pour le
calcul du produit de deux polynômes : si A et B sont deux polynômes de degré n dont les valeurs sont connues
en 2n + 1 points, alors on connait les valeurs du polynôme AB en ces 2n + 1 points par simple multiplication
dans les réels : il en faut 2n + 1 . Les résultats classiques d’interpolation nous permettent de dire que cela est
suffisant pour connaı̂tre exactement AB.
La représentation points/valeurs n’est cependant pas efficace pour l’évaluation d’un polynôme en d’autres points
que les points d’interpolation. Dans ce cas, c’est la représentation par coefficients qui est la plus utilisée, couplée
avec la méthode de Hörner pour l’évaluation.
Xn
Si l’on veut évaluer le polynôme A = ai X i , au point x, on écrit :
i=0
Cette méthode nécessite de l’ordre de n additions et n multiplications, ce qui est mieux que la méthode naı̈ve
en n2 .
Mais concernant la multiplication, la représentation par coefficients est très peu efficace. Si on pose C = AB ,
i
X
les coefficients de A étant les ai , ceux de B les bi et ceux de C les ci , on a ci = ak bi−k . On comprend qu’il
k=0
faut alors de l’ordre de n2 multiplications et additions : c’est un ordre de grandeur supérieur à la représentation
points/valeurs. La représentation par coefficients étant plus répandue, il est légitime de se demander si on ne
pourrait pas utiliser la représentation points/valeurs pour la multiplication, en allant chercher par exemple des
propriétés d’interpolations.
La bonne idée est d’interpoler sur les racines de l’unité. Dans la suite, on considère que A et B sont deux
polynômes de degré 6 n − 1 .
Commençons par la phase d’évaluation. Soit ω une racine primitive n e de l’unité, on cherche à calculer A(ω k )
pour 0 6 k 6 n − 1 :
n−1
X
A(ω k ) = ai ω ki
i=0
X X
= a2i ω 2ki + a2i+1 ω k(2i+1)
062i6n−1 062i+16n−1
X X
2ki
= a2i ω + ωk a2i+1 ω 2ki
062i6n−1 062i+16n−1
2k k 2k
= P (ω ) + ω I(ω ) .
Ainsi pour calculer l’évaluation sur une racine de l’unité, il suffit de calculer l’évaluation en ω 2 des polynômes
n
de degré composés des coefficients d’indice pair (polynôme P ) pour l’un et des coefficients d’indice impair
2
(polynôme I ) pour l’autre (si ω est une racine n e de l’unité, ω 2 en est une racine (n/2) e). Pour obtenir la
transformée de Fourier d’un polynôme A, il faudra faire cette évaluation sur toutes les racines n es de l’unité
pour obtenir la liste [A(ω 0 ), . . . , A(ω n−1 )], mais les polynômes P et I restent les mêmes.
n
On a ainsi divisé le problème de taille n en deux sous problèmes de taille qu’il faut traiter tous les deux.
2
Il faudra prévoir n opérations pour séparer les coefficients pairs et impairs et deux opérations à chaque fois
(une multiplication et une addition) pour construire la solution. La relation de récurrence vérifiée par le nombre
d’opérations est donc de la forme :
n n
T (n) = 2T + n + 2n = 2T + 3n
2 2
On se trouve dans les conditions du théorème avec α = 3, a = 2, b = 2 et c = 1 . On est donc dans le cas
d’égalité et T (n) = O (n log2 (n)) .
On effectue cette opération sur les deux polynômes A et B , puis on fait le produit terme à terme des deux
listes pour obtenir la transformée de Fourier du polynôme AB . Cette opération s’effectue en temps linéaire.
Reste à savoir comment retrouver, à partir de sa transformée de Fourier, les coefficients d’un polynôme.Soit P
π
un polynôme de degré 6 n−1 . On note γk le k e coefficient de sa transformée : γk = P (ω k ) avec ω = exp 2i .
n
On a pour un entier i 6 n − 1 donné :
n−1
X n−1
X n−1
X n−1
X n−1
X n−1
X 2n−1
X k
γk ω −ik = aj ω kj ω −ki = aj ω k(j−i) = aj ω (j−i)
k=0 k=0 j=0 j=0 k=0 j=0 k=0
La deuxième somme est nulle sauf si j = i : en effet le fait d’avoir choisi les racines n-ièmes de l’unité assure
que ω j−i 6= 1 sauf si i = j :
n−1
X
γk ω −ik = nai .
k=0
Ainsi pour retrouver ai il suffit de refaire une transformée de Fourier en prenant cette fois ω −1 comme élément
de base pour l’évaluation. Retrouver les coefficients se fera donc encore avec une complexité de l’ordre de
n log(n).
La stratégie pour multiplier deux polynômes est illustrée par le schéma ci-dessous.
À faire :
1. Écrire une procédure récursive permettant de calculer la transformée de Fourier d’un polynôme de degré
6 n − 1 donné par la liste de ses coefficients. On suppose que n est une puissance de 2 .
Pour cela on pourra utiliser le module cmath qui permet de manipuler des nombres complexes.
import cmath as cm
abs(z)
# calcule le module de z
phase(z)
# calcule un argument de z entre -pi et pi
polar(z)
# retourne le couple (r, theta)
rect(r,theta)
# retourne le complexe de module r et d'argument theta
L’algorithme est expliqué ci-dessous (cette figure ainsi que la précédente sont issues de la ≪ bible ≫ :
Algorithmique, de Cormen aux Éditions Dunod (1200 pages !))
2. Écrire une procédure pour retrouver les coefficients d’un polynôme à partir de sa transformée de Fourier.
3. En déduire une procédure permettant de calculer le produit de deux polynômes A et B donnés par
leurs coefficients. On commencera pour cela par chercher la plus grande puissance de 2 supérieure à
deg A + deg B et on complètera la liste des coefficients par des 0 .
– On sépare l’ensemble initial en 2 sous-ensembles : celui des j éléments inférieurs à x et celui des m − j
éléments supérieurs à x, les éléments égaux à x étant mis alternativement d’un côté ou de l’autre.
Si j est supérieur à n, on est ramené à chercher le n e élément du 1er sous-ensemble ; s’il est inférieur à
n, on cherche le (n − j) e élément du second.
Lorsque la taille d’un ensemble est inférieure à k , on calculera directement la médiane ou le n e élément par une
procédure auxiliaire qui utilise la méthode sort de Python.
Complexité :
Notons T (m) le temps nécessaire pour trouver le n e élément d’une liste de cardinal m. On considèrera que si
m 6 k , T (m) = a = cste).
On :
T (m) = αm (temps de sépartion ne m/k sous-ensembles)
m
+ T (k) (médianes de chacun de ces sous-ensembles)
k
m
+T (temps pour la médiane des médianes)
k
+ βm (temps de séparation en deux sous-ensembles)
+ T min(j, m − j)
m m m
Puisque T (k) = a, que T min(j, m − j) 6 T et que T 6T car k > 3 on obtient :
2 k 3
a m m
T (m) 6 α + β + m+T +T .
k 2 3
Montrons alors par récurrence qu’il existe une constante C telle que T (m) 6 Cm pour tout m.
– Initialisation : on peut toujours trouver c telle que T (m) 6 cm pour les premières valeurs de m, et toute
valeur C > c convient aussi.
– Ensuite :
a m m
T (m) 6 α + β + m+C +C
k k 2
a C C
6 α+β+ + + m
k k 2
a 5C
6 α+β+ + m
k 6
a
et il suffit donc de prendre C > 6 α + β + pour obtenir T (m) 6 Cm.
k
La complexité de l’algorithme est donc bien un O (m).
Comme nous n’aurons certainement pas le temps de faire tous les programmes de ce chapitre, je vous donne la
solution en Python ; j’y ai inclus un graphique pour comparer le temps d’exécution avec celui obtenu à l’aide d’un
simple programme de tri (qui est en O (m log m) au minimum, comme on le verra dans un prochain chapitre).
import random
from time import clock
import matplotlib.pyplot as plt
def nieme_element1(liste,n):
# n-ième élément d'une liste en utilisant le tri
liste.sort()
return liste[n-1]
nb_elements = 10000000
nmax = 10000000
liste_complete = [random.randint(0, nmax) for i in range(nb_elements)]
N = []
T1 = []
T2 = []
for n in range(100000, nb_elements, 200000):
# print(n, sep=' ') juste pour suivre la progression
liste = liste_complete[0:n]
debut = clock()
x = nieme_element(liste, n//2 + 1)
T1.append(clock() - debut)
debut = clock()
y = nieme_element1(liste, n//2 + 1)
T2.append(clock() - debut)
if x != y:
print('oups! programme faux!')
N.append(n)
plt.show()
25
Algorithme récursif
Avec tri
20
15
Temps
10
0
0.0 0.5 1.0 1.5 2.0 2.5
Taille de la liste 1e7
⋆ ⋆ ⋆ ⋆
⋆ ⋆ ⋆
⋆ ⋆
⋆