cours1 min de lecture

Bases de la complexité

Combien coûte un algorithme ? Premier contact avec la notation grand-O, sans dramatiser les maths.

Introduction

Vous savez maintenant écrire un parcours séquentiel. Mais vous écrirez bientôt des algorithmes plus subtils, et la première question d'un informaticien devant un algorithme n'est pas « est-ce qu'il marche ? » — c'est « combien coûte-t-il ? ». Combien d'opérations effectue-t-il selon la taille des données ? Si je double l'entrée, le temps double-t-il, quadruple-t-il, ou augmente-t-il à peine ? Ce cours pose le socle de cette analyse. Pas de théorème lourd : juste une intuition claire et une notation simple, qui vous serviront dans tout le reste du chapitre.

Pourquoi mesurer ?

Imaginez deux programmes qui font la même chose : chercher un nom dans un annuaire de 10 millions d'entrées. Le premier met 3 secondes, le second met 3 minutes. Vous préférerez le premier, évidemment. La complexité, c'est l'outil qui permet de prédire cette différence avant même d'exécuter le code, en analysant la structure de l'algorithme.

On ne mesure pas en secondes (cela dépend de la machine, du langage, de la charge système). On compte le nombre d'opérations élémentaires effectuées en fonction de la taille de l'entrée, notée nn.

Taille de l'entrée

Avant de compter, il faut décider de ce qu'est la taille nn. C'est en général évident :

  • pour un tableau ou une liste : nn = le nombre d'éléments,
  • pour une chaîne de caractères : nn = sa longueur,
  • pour un entier kk : nn = sa valeur (parfois son nombre de chiffres).

Trois ordres de grandeur typiques

Coût constant — O(1)\mathcal{O}(1)

Le coût ne dépend pas de la taille. Que tableau ait 10 ou 10 millions d'éléments, l'opération prend le même temps.

def premier(tableau):
    return tableau[0]   # un seul accès, peu importe la taille

Coût linéaire — O(n)\mathcal{O}(n)

Le coût est proportionnel à nn. Si on double la taille, le temps double. Tout parcours séquentiel du cours précédent est dans ce cas.

def somme(tableau):
    total = 0
    for v in tableau:   # exactement n tours
        total += v
    return total

Coût quadratique — O(n2)\mathcal{O}(n^2)

Le coût est proportionnel à n2n^2. Si on double la taille, le temps est multiplié par 4. On l'obtient typiquement avec deux boucles imbriquées qui parcourent chacune le tableau.

def doublons(tableau):
    """Renvoie True si tableau contient au moins un doublon."""
    n = len(tableau)
    for i in range(n):
        for j in range(i + 1, n):
            if tableau[i] == tableau[j]:
                return True
    return False

Le nombre de comparaisons est de l'ordre de n×n/2n \times n / 2 — ce qui reste proportionnel à n2n^2.

Si un algo en O(n²) met 1 seconde pour n=1000, combien de temps environ pour n=10000 ?

La notation grand-O, en douceur

On a écrit O(n)\mathcal{O}(n), O(n2)\mathcal{O}(n^2). Que signifie ce symbole ?

L'idée : on néglige les constantes et les détails de petite taille pour ne garder que l'ordre de grandeur quand nn devient grand. Par exemple, un algorithme qui fait 3n+73n + 7 opérations et un autre qui en fait n1n - 1 ont le même comportement asymptotique (c'est-à-dire la même allure quand nn devient grand) — tous deux sont en O(n)\mathcal{O}(n). Quand nn atteint 1 million, le « +7 » et le « ×3 » sont sans importance face au grand nn.

Définition intuitive : un algorithme est en O(f(n))\mathcal{O}(f(n)) si, à partir d'une certaine taille, son nombre d'opérations reste borné par un multiple de f(n)f(n). Autrement dit : « il croît au pire comme f(n)f(n) ».

Vous rencontrerez aussi O(logn)\mathcal{O}(\log n) au cours 4 (dichotomie) — c'est un coût encore plus petit que O(n)\mathcal{O}(n), qui croît extrêmement lentement.

Comparer les ordres de grandeur

nnlog2n\log_2 nnnn2n^2
10≈ 310100
100≈ 710010 000
1 000≈ 101 0001 000 000
1 000 000≈ 201 000 000101210^{12}

Pour un million d'éléments, un algorithme logarithmique finit en 20 étapes ; un algorithme linéaire en un million ; un algorithme quadratique en mille milliards — irréalisable en pratique.

Hiérarchie à retenir :O(1)  <  O(logn)  <  O(n)  <  O(n2)\mathcal{O}(1) \;<\; \mathcal{O}(\log n) \;<\; \mathcal{O}(n) \;<\; \mathcal{O}(n^2)Plus on est à gauche, mieux on passe à l'échelle.
⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.

Cas pire, cas moyen

Tous les algorithmes n'effectuent pas le même nombre d'opérations sur toutes les entrées de même taille. La recherche linéaire peut :

  • trouver dès la 1ère case (cas favorable, 1 comparaison) ;
  • trouver à la dernière case ou ne rien trouver (cas pire, nn comparaisons).

On parle souvent du cas pire car c'est la garantie maximale offerte par l'algorithme. Quand on dit « la recherche linéaire est en O(n)\mathcal{O}(n) », c'est au pire — au mieux, elle est en O(1)\mathcal{O}(1).

Au mieux, combien d'opérations effectue une recherche linéaire qui trouve sa cible ?

Pièges courants

  • Compter en secondes au lieu de compter des opérations : trompeur, car dépendant de la machine.
  • Garder les constantes dans la notation O\mathcal{O} : on écrit O(n)\mathcal{O}(n), jamais O(3n+5)\mathcal{O}(3n + 5).
  • Confondre cas pire et cas moyen : à l'oral, préciser quand c'est ambigu.

Pour aller plus loin

Vous allez vérifier ces ordres de grandeur dans les cours suivants : les tris du cours 3 sont en O(n2)\mathcal{O}(n^2), la dichotomie du cours 4 est en O(logn)\mathcal{O}(\log n). La différence de performance entre les deux dernières est colossale — un algorithme dichotomique sur un milliard d'éléments tient en 30 étapes seulement.