Terminaison et complexité
Numérique et sciences informatiques
Il est nécessaire de pouvoir s'assurer qu'un algorithme effectue bien ce qui est attendu (la correction),
qu'il finit bien par s'arréter (la terminaison) et qu'il effectue son travail dans un délai raisonnable (la compléxité).
Nous allons découvrir comment démontrer qu’une boucle while se termine avec le variant de boucle, puis nous allons prouver qu’un algorithme est correct avec
l’invariant de boucle.
Problème de terminaison : le variant de boucle
Le problème de la terminaison est un problème important en informatique lorsqu'on parle de boucles. Ce problème se pose lorsque l'on écrit une boucle qui peut potentiellement ne jamais se terminer, c'est-à-dire qu'elle continuera à tourner indéfiniment sans jamais s'arrêter.
Pour éviter cela, on utilise une technique appelée "variant de boucle". Un variant de boucle est une expression numérique qui doit être modifiée à chaque itération de la boucle et qui doit garantir que la boucle finira par se terminer.
-
Exemple 1 : boucle for en Python avec un variant :
for i in range(n): # instructions de la boucle
Dans cet exemple, le variant de boucle est la variable i, qqui est incrémentée à chaque itération de la boucle. Comme i commence à 0 et que la boucle continue tant que i < n, le variant de boucle atteindra finalement la valeur "n" et la boucle se terminera.
-
Exemple 2 : boucle while en Python avec un variant :
i = 0 while i < n: # instructions de la boucle i += 1
Dans cet exemple, le variant de boucle est la variable i, qui augmente de 1 à chaque itération de la boucle. Comme i commence à 1 et que la boucle continue tant que i < n, le variant de boucle atteindra finalement la valeur "n" et la boucle se terminera.
Il est important de choisir un variant de boucle qui garantit que la boucle se terminera. Si le variant de boucle ne décroît pas à chaque itération, ou s'il ne décroît pas suffisamment rapidement, la boucle pourrait ne jamais se terminer.
En résumé, le variant de boucle est une technique importante pour garantir que les boucles se terminent correctement. Il est important de bien choisir un variant de boucle qui garantit la terminaison de la boucle.
Entraînement 1 :
Algorithme division(a, b)
Entrée : a et b sont deux entiers positifs non nuls avec a > b.
Sortie : un tuple (q,r) constitué de deux entiers q et r tels que a = bq + r et r < b.
q ← 0
r ← a
tant que r > b faire
q ← q + 1
r ← r - b
retourner (q,r)
Compléter le tableau suivant pour faire un compte rendu de l'algorithme précédant pour a = 19 et b = 3.
Itération | a = | b = | q = | r = |
---|---|---|---|---|
1 | ||||
2 | ||||
... |
Pour cette séquence d'instruction le variant de boucle est r - b. Au bout
d'un certain nombre d'itération, il devient négatif,et cela terminera la boucle while
.
Problème de correction : l'invariant de boucle
En programmation, l'une des tâches les plus importantes est de s'assurer que le code fonctionne correctement. Pour cela, on utilise souvent des boucles pour parcourir une liste ou effectuer une opération un certain nombre de fois. Cependant, il peut arriver que la boucle ne s'exécute pas comme prévu, entraînant des erreurs dans le programme.
C'est là que l'invariant de boucle entre en jeu. Un invariant de boucle est une condition qui doit être vraie à chaque itération de la boucle, garantissant ainsi que le programme fonctionne correctement. En d'autres termes, si l'invariant est vrai avant l'itération de la boucle, il sera également vrai après l'itération.
Voici un exemple d'utilisation de l'invariant de boucle : Supposons que nous voulions calculer la somme des n premiers entiers. Nous pouvons utiliser une boucle pour itérer à travers chaque nombre et ajouter le résultat à la somme totale. Cependant, si la boucle n'est pas correctement écrite, elle peut entraîner des erreurs dans le calcul de la somme.
Voici un exemple de code en Python pour calculer la somme des n premiers entiers :
def somme_des_entiers(n):
"""Renvoie la somme des entiers jusqu'à n."""
somme = 0 # invariant
for i in range(1, n+1):
somme += i
# invariant: contient la somme partielle des i premiers entiers
assert somme == i*(i+1)//2, "Invariant de boucle rompu !"
return somme
Dans ce code, l'invariant de boucle est que la variable somme contient la somme des i premiers entiers positifs avant chaque itération de la boucle.
- Avant la boucle, somme est initialisé à 0, ce qui est bien la somme des 0 premiers entiers positifs.
- À chaque itération, nous mettons à jour la somme partielle pour inclure le nombre suivant et vérifions que l'invariant est toujours vrai. Si l'invariant est faux à une itération, cela signifie qu'il y a une erreur dans la boucle et nous devons corriger le code..
Après la boucle, somme contient la somme des n premiers entiers positifs, comme requis.
Problème de temps : la complexité d'un algorithme
La complexité d'un algorithme est une mesure de la quantité de ressources (en temps et en espace) nécessaires pour exécuter l'algorithme. En d'autres termes, c'est une mesure de la difficulté de l'algorithme.Le calcul de la complexité d’un algorithme permet de mesurer sa performance. Il existe deux types de complexité :
- complexité spatiale : permet de quantifier l’utilisation de la mémoire
- complexité temporelle : permet de quantifier la vitesse d’exécution
Il est important de connaître la complexité d'un algorithme car elle peut aider à déterminer le temps nécessaire pour exécuter l'algorithme sur des entrées de différentes tailles. Cela peut être particulièrement utile lorsque l'on travaille avec des ensembles de données volumineux, car certains algorithmes peuvent prendre beaucoup plus de temps que d'autres pour traiter de grandes quantités de données.
Réaliser un calcul de complexité en temps revient à compter le nombre d’opérations élémentaires (affectation, calcul arithmétique ou logique, comparaison…) effectuées par l’algorithme.
Par soucis de simplicité, on fera l’hypothèse que toutes les opérations élémentaires sont à égalité de coût, soit 1 « unité » de temps.
Exemple :
toto = toto + 1
---> 1 addition + 1 affectation = 2 unité
La complexité en temps d’un algorithme sera exprimé par une fonction, notée T (pour Time).
On calculera le plus souvent la complexité dans le pire des cas, car elle est la plus pertinente.
Pour comparer des algorithmes, il n’est pas nécessaire d’utiliser la fonction T mais seulement l’ordre de grandeur, noté O(« grand O »).
Exemples d'ordre de grandeur :
- T(n) = 7 => O(1)
- T(n) = 12n + 5 => O(n)
- T(n) = 4n^2 + 2n+6 => O(n^2)
- T(n) = 2 + (n-1) x 5 => O(n)
Les différentes classes de compléxité
O | Type de complexité | Exemple |
---|---|---|
O(1) | constante | accès à une valeur d’une liste |
O(log(n)) | logarithmique | recherche dichotomique dans une liste triée (nous verrons cela en détail plus tard.) |
O(n) | linéaire | parcours d’une liste |
O(n×log(n)) | quasi-linéaire | tri fusion |
O(n^2) | quadratique | parcours d’une liste de liste (tableau n × n) |
O(n^3) | cubique | cubique |
O(n^p) | polynomiale | multiplication de matrices |
O(2^n) | exponentielle | problème du sac à dos (algo naïf), calcul d’un terme de la suite de Fibonacci (algo naïf) |
O(n!) | factorielle | problème du voyageur de commerce (algo naïf) |
Pour se fixer les idées, voici un tableau qui donne le temps approximatif d’exécution en fonction de la taille des données. Nous partirons du principe qu’une instruction élémentaire prend 1 µs (10−6 secondes).
Taille (n) | log_2(n) | n | n log_2(n)) | n² | 2^n | n! |
---|---|---|---|---|---|---|
10 | 3 µs | 10 µs | 30 µs | 100 µs | 1 000 µs | 3 s |
100 | 7 µs | 100 µs | 700 µs | 1/100 s | 10^14 siècles | astronomique |
1 000 | 10 µs | 1 000 µs | 1/100 s | 1 s | astronomique | astronomique |
10 000 | 13 µs | 1/100 s | 1/7 s | 1,7 min | astronomique | astronomique |
100 000 | 17 µs | 1/10 s | 2 s | 2,8 h | astronomique | astronomique |
Exemples pour le calcul de la compléxité :
Exemple 1 : Calcul de la somme des n premiers carrés (Boucle for)
def somme_carres(n):
somme = 0
for i in range(1, n+1):
somme += i**2
return somme
La boucle for s'exécute n fois, donc la complexité de cet algorithme est O(n), car le temps nécessaire pour exécuter l'algorithme est proportionnel à la taille de l'entrée n.
Exemple 2 : Recherche du maximum d'une liste :
def max_liste(liste):
maximum = liste[0]
for element in liste:
if element > maximum:
maximum = element
return maximum
La boucle for s'exécute n fois (où n est la taille de la liste), donc la complexité de cet algorithme est O(n), car le temps nécessaire pour exécuter l'algorithme est proportionnel à la taille de l'entrée n (la taille de la liste).
Exemple 3 : Calcul du produit de deux matrices (Boucles for imbriquées):
def produit_matrices(A, B):
n = len(A)
m = len(B[0])
C = [[0]*m for i in range(n)]
for i in range(n):
for j in range(m):
for k in range(len(B)):
C[i][j] += A[i][k] * B[k][j]
return C
La première boucle for s'exécute n fois, la deuxième boucle for s'exécute m fois, et la troisième boucle for s'exécute len(B) fois. Par conséquent, la complexité de cet algorithme est O(n*m*len(B)), car le temps nécessaire pour exécuter l'algorithme dépend de la taille des deux matrices A et B.
Entraînement 2 :
def fonction_mystere(tab):
mystere = tab[0] #tab est un tableau de réels de taille n
i = 0
while i < len(tab) : # ligne 3
if tab[i] > mystere : # ligne 4
mystere = tab[i]
i = i + 1
return mystere
Que fait le code ci-dessus ?
Entraînement 3 :
Dans l'algorithme précédant, nous avons les opérations suivantes :
- ligne 1 : Accès à un élément de tableau et Affectation de réel
- ligne 2 : Affectation d’entier
- ligne 3 : Comparaison de deux entiers et accès à un élément de tableau
- ligne 4 : Accès à un élément de tableau et comparaison de deux réels
- ligne 5 : Accès à un élément de tableau et Affectation de réel
- ligne 6 : Addition de deux entiers et affectation d’entier
Compléter le tableau suivant en comptant le nombre de fois où chaque opération est effectuée, pour chaque ligne du programme et vérifier que cet algorithme de recherche du maximun exécute T(n) = 8𝑛 + 3 opérations primitives dans le pire des cas. Soit O(n) en compléxité
Ligne | Accès tableau | Affectation entier | Affectation réel | Comparaison de deux entiers | Comparaison de deux réels | Addition de deux entiers |
---|---|---|---|---|---|---|
1 | ||||||
2 | ||||||
3 | ||||||
4 | ||||||
5 | ||||||
6 | ||||||
Total |
Entraînement 4 :
def somme_de_n(n):
somme = 0
i = 1
while i <= n:
somme += i
i += 1
return somme
Quelle est la complexité de cet algorithme ?
Voir une solutionEntraînement 5 : Recherche d'un élément dans une liste
def recherche_element(liste, element):
for i in range(len(liste)):
if liste[i] == element:
return True
return False
Quelle est la complexité de cet algorithme ?
Voir une solutionEntraînement 6 :
Donner la classe de compléxité de cet algorithme.
def conversion(n):
h = n // 3600
m = (n - 3600*h) // 60
s = n % 60
return h,m,s
Entraînement 7 :
Voici quelques complexités d’algorithme, chercher les complexités de référence qui sont du même ordre de
grandeur.
- 3n² + 5n - 2 = O(n²) -> Compléxité quadratique
- 1/2 * n² + 2n³ + 4 = O( ... ?) -> Compléxité ... ?
- 5n⁴ + 2n³ + 4 = O( ... ?) -> Compléxité ... ?
- 5n + 2nlog_2(n) + 1 = O( .... ?) -> Compléxité ... ?
- n! + 2n + 4n² = O( ... ?) -> Compléxité ... ?
Entraînement 8 :
- Que fait cet algoritme ?
- Donner la compléxité de cet algorithme.
def on_est_jamais_trop_prudent(tab):
"""
Paramametre :
tab : un tableau d'entier de type list
Renvoie :
nombre : entier de type int
"""
nombre = L[0]
n = len(L)
for i in range(n):
if nombre < L[i]:
nombre = L[i]
for j in range(n):
if nombre < L[j]:
nombre = L[j]
return nombre
Entraînement 9 :
- Que fait la fonction ci_dessous ?
- Donner la compléxité de cette fonction
def mystere_1(n):
x = 0
for i in range(n):
for j in range(n):
x += 1
return x
Entraînement 10 :
- Que fait la fonction ci_dessous ?
- Donner la compléxité de cette fonction
def mystere_2(n):
x, i = 0, n
while i > 1:
for j in range(i):
x += 1
i //= 2
return x
Savoir faire
- Déterminer la complexité d'un algorithme en temps dans des cas linéaires et quadratiques..
- Variant et invariant de boucle.
- Terminaison d'un algorithme.
- Correction d'un algorithme.
Fiche de cours