Stages de Pré-Rentrée — Inscriptions ouvertes, places très limitées ! S'inscrire

Aller au contenu principal
Complexité algorithmique : la notation grand O
Méthode
11 min

Complexité algorithmique : la notation grand O

Mohamed K.

Mohamed K.

Centralien · MPSI puis MP · Recherche ML santé

Deux programmes donnent le même résultat : l'un répond en une seconde, l'autre tourne encore une heure plus tard. La différence n'est presque jamais la machine, c'est l'algorithme. La complexité algorithmique est l'outil qui permet de prédire ce genre d'écart avant même d'exécuter le code, en comptant comment le travail à faire grandit avec la taille des données.

Cet article te donne tout ce qu'il faut pour être solide en informatique de prépa (tronc commun et option info) : pourquoi et comment on mesure le coût d'un programme, le comptage des opérations élémentaires, la notation de Landau grand OO avec son intuition, les classes usuelles de O(1)O(1) à O(2n)O(2^n) dans un tableau comparatif, la distinction meilleur / pire / moyen cas, l'analyse des boucles imbriquées, une mention de la complexité amortie, et une méthode pas à pas pour analyser un code, avec 3 exercices corrigés. Nos profs Hadamard, anciens de MP2I et de MPSI option info passés par l'X, l'ENS et CentraleSupélec, voient revenir les mêmes maladresses sur les copies, on les liste pour que tu les évites.

Pourquoi mesurer la complexité

On pourrait croire qu'il suffit de chronométrer un programme. Mauvaise idée : le temps en secondes dépend de la machine, du langage, de la charge du processeur, de la taille exacte du jeu de test. Un même code paraîtra rapide sur ton ordinateur et lent sur une vieille calculatrice, sans que l'algorithme ait changé. Ce qu'on veut, c'est une mesure intrinsèque à l'algorithme.

L'idée est de compter le nombre d'opérations élémentaires exécutées, en fonction de la taille des données, notée nn (le nombre d'éléments d'un tableau, la longueur d'une chaîne, la valeur d'un entier à traiter). On ne cherche pas le nombre exact, mais la façon dont il grandit quand nn augmente. C'est ce comportement, dit asymptotique, qui décide si un programme passe à l'échelle ou s'effondre sur de grosses entrées.

Deux complexités, pas une. La complexité en temps compte les opérations exécutées. La complexité en espace mesure la mémoire supplémentaire utilisée (tableaux auxiliaires, pile d'appels récursifs). Les deux se notent avec le même grand OO, et un bon algorithme cherche souvent un compromis entre les deux.

Compter les opérations élémentaires

Une opération élémentaire est une action au coût supposé constant, indépendant de nn : une affectation, une comparaison, une addition, un accès à une case de tableau tab[i]. On lui attribue un coût O(1)O(1). Analyser un algorithme, c'est additionner ces coûts en suivant le déroulement du code.

Prenons la somme des éléments d'un tableau de taille nn :

def somme(tab):
    total = 0            # 1 operation
    for x in tab:        # la boucle tourne n fois
        total += x       # 1 operation par tour
    return total         # 1 operation

L'initialisation et le return coûtent O(1)O(1) chacun. La boucle exécute nn fois une opération à O(1)O(1), soit un coût nn. Le total est 1+n+1=n+21 + n + 1 = n + 2 opérations. Quand nn devient grand, le +2+2 ne pèse plus rien face au nn : on retient que cet algorithme est linéaire, en O(n)O(n).

C'est le premier réflexe : on ne garde ni les constantes additives, ni les facteurs multiplicatifs. Un algorithme en 3n+53n + 5 et un algorithme en nn sont tous les deux en O(n)O(n), car ils grandissent à la même vitesse. Ce qui compte, c'est l'ordre de grandeur.

La notation de Landau, grand O

La notation grand OO (notation de Landau) formalise cette idée d'ordre de grandeur. Soit T(n)T(n) le nombre d'opérations d'un algorithme et ff une fonction de référence (par exemple nn, n2n^2, logn\log n). On dit que TT est un O(f(n))O(f(n)) lorsqu'il existe une constante C>0C > 0 et un rang n0n_0 tels que :

T(n)    Cf(n)pour tout nn0.T(n) \;\leq\; C \cdot f(n) \qquad \text{pour tout } n \geq n_0.

Autrement dit, à partir d'une certaine taille, T(n)T(n) reste en dessous de f(n)f(n) à une constante multiplicative près. Le grand OO est donc une borne supérieure sur la vitesse de croissance : il dit « ça ne grandit pas plus vite que ff ».

L'intuition à garder. O(f(n))O(f(n)) répond à une question simple : quand nn double, par quel facteur le temps est-il multiplié ? En O(n)O(n), il double. En O(n2)O(n^2), il est multiplié par quatre. En O(logn)O(\log n), il n'augmente que d'une constante. En O(2n)O(2^n), il est multiplié par 2n2^n : catastrophique.

Deux règles pratiques découlent de la définition. On supprime les constantes : O(5n)=O(n)O(5n) = O(n). Et dans une somme, on ne garde que le terme dominant : O(n2+n)=O(n2)O(n^2 + n) = O(n^2), car pour nn grand, n2n^2 écrase nn. Ces deux réflexes suffisent à simplifier la quasi-totalité des calculs de complexité en prépa.

Cours particuliers

Maîtrisez l'informatique avec un prof expert

Python, algorithmique, structures de données... Un accompagnement adapté.

Exercices pratiquesProfs expérimentés
Voir les cours d'info

Les classes de complexité usuelles

Quelques fonctions de référence reviennent en permanence. Les voici de la plus rapide à la plus lente, avec un exemple typique et l'ordre de grandeur du nombre d'opérations.

NotationNomExemplen=10n = 10n=1000n = 1000
O(1)O(1)ConstantAccès à tab[i]1111
O(logn)O(\log n)LogarithmiqueRecherche dichotomique3\approx 310\approx 10
O(n)O(n)LinéaireParcours d'un tableau101010001000
O(nlogn)O(n \log n)Quasi-linéaireTri fusion33\approx 33104\approx 10^4
O(n2)O(n^2)QuadratiqueTri par insertion10010010610^6
O(2n)O(2^n)ExponentielÉnumérer les sous-ensembles1024102410301\approx 10^{301}

La colonne n=1000n = 1000 dit tout. Un algorithme en O(n2)O(n^2) demande un million d'opérations là où un O(nlogn)O(n \log n) en demande dix mille : cent fois moins. Et l'O(2n)O(2^n) pour n=1000n = 1000 dépasse le nombre d'atomes de l'univers observable, aucune machine ne le terminera jamais. C'est pourquoi passer d'un algorithme quadratique à un algorithme quasi-linéaire change concrètement ce qui est calculable, comme on le voit sur les algorithmes de tri et de recherche au programme de prépa.

Repère mental. O(logn)O(\log n) et O(1)O(1) sont excellents, O(n)O(n) et O(nlogn)O(n \log n) sont bons et courants, O(n2)O(n^2) passe sur de petites données mais devient lourd, O(2n)O(2^n) (et O(n!)O(n!)) sont à éviter dès que possible, réservés aux problèmes où l'on ne connaît rien de mieux.

Meilleur cas, pire cas, cas moyen

Pour une même taille nn, un algorithme n'exécute pas toujours le même nombre d'opérations : cela dépend des données précises qu'on lui donne. On distingue donc trois analyses.

  • Meilleur cas : la situation la plus favorable, le plus petit nombre d'opérations possible.
  • Pire cas : la situation la plus défavorable, le plus grand nombre d'opérations. C'est une garantie : l'algorithme ne sera jamais plus lent.
  • Cas moyen : la moyenne sur toutes les entrées possibles, souvent plus délicate à calculer car elle suppose un modèle de probabilité sur les données.

Exemple classique, la recherche d'une valeur par parcours simple : si l'élément cherché est en première position, on s'arrête tout de suite, c'est O(1)O(1) au meilleur cas. S'il est absent ou en dernière position, on parcourt tout, c'est O(n)O(n) au pire cas. La recherche dichotomique, elle, est O(1)O(1) au meilleur cas (on tombe pile au milieu) mais O(logn)O(\log n) au pire cas.

La convention en prépa. Sauf précision contraire, « la complexité » désigne le pire cas. C'est le plus utile : il donne une borne sûre sur laquelle on peut s'engager. Quand un énoncé demande la complexité sans préciser, réponds au pire cas.

RDV gratuit de 15 min

Trouvez le prof qu'il vous faut

Échangez avec notre équipe pour trouver le professeur idéal selon vos besoins.

Matching avec le bon prof
Programme sur-mesure
Premier cours d'essai

Sans engagement • Réponse sous 24h

Complexité des boucles imbriquées

Les boucles sont le principal moteur de la complexité. La règle est simple : une boucle qui tourne kk fois multiplie par kk le coût de son corps. Deux boucles imbriquées multiplient donc leurs nombres d'itérations.

Le cas le plus courant, deux boucles indépendantes de nn tours chacune :

def compte_doublons(tab):
    n = len(tab)
    c = 0
    for i in range(n):          # n tours
        for j in range(n):      # n tours a chaque fois
            if tab[i] == tab[j]:
                c += 1
    return c

Le corps interne est en O(1)O(1), exécuté n×n=n2n \times n = n^2 fois : l'algorithme est en O(n2)O(n^2). Attention toutefois, il ne faut pas conclure « deux boucles imbriquées donc O(n2)O(n^2) » de façon automatique. Tout dépend du nombre réel de tours de la boucle interne. Regardons une variante où la boucle interne démarre à i + 1 :

def compte_paires(tab):
    n = len(tab)
    c = 0
    for i in range(n):              # i de 0 a n-1
        for j in range(i + 1, n):   # n-1-i tours
            if tab[i] == tab[j]:
                c += 1
    return c

Ici la boucle interne fait n1in - 1 - i tours. Le nombre total d'itérations est :

i=0n1(n1i)  =  (n1)+(n2)++1+0  =  n(n1)2.\sum_{i=0}^{n-1} (n - 1 - i) \;=\; (n-1) + (n-2) + \cdots + 1 + 0 \;=\; \frac{n(n-1)}{2}.

On trouve n2n2\dfrac{n^2 - n}{2}, dont le terme dominant est n22\dfrac{n^2}{2}. En supprimant la constante 12\dfrac12, cet algorithme est aussi en O(n2)O(n^2). Faire deux fois moins de travail ne change pas la classe de complexité : c'est un facteur constant. En revanche, une boucle interne qui ne dépend pas de nn (nombre fixe de tours) laisse la complexité en O(n)O(n).

Le facteur logn\log n apparaît quand une boucle divise la taille par deux à chaque tour, sans la parcourir. Une boucle while qui fait n = n // 2 jusqu'à atteindre 11 s'exécute environ log2n\log_2 n fois : c'est exactement le mécanisme de la recherche dichotomique. Combinée à un parcours linéaire imbriqué, elle donne du O(nlogn)O(n \log n), la complexité des bons tris.

Un mot sur la complexité amortie

Certaines opérations sont coûteuses de temps en temps, mais bon marché la plupart du temps. La complexité amortie répond à cette situation : au lieu de regarder le pire cas d'une seule opération, on regarde le coût moyen d'une opération sur une longue suite d'opérations.

L'exemple canonique est l'ajout en fin de liste dynamique, le append de Python. La plupart du temps il coûte O(1)O(1). Mais quand le tableau interne est plein, il faut le recopier dans un tableau plus grand, ce qui coûte O(n)O(n). Comme ce doublement de capacité est rare (il se produit de moins en moins souvent à mesure que la liste grossit), le coût amorti d'un append reste O(1)O(1). Sans cette analyse, on surestimerait le coût d'une suite d'ajouts.

À retenir. Complexité amortie n'est pas complexité moyenne. Le cas moyen suppose une distribution de probabilité sur les données ; la complexité amortie est une garantie déterministe sur une séquence d'opérations, sans hypothèse aléatoire. C'est une notion d'option info, souvent seulement citée dans le tronc commun.

Méthode pour analyser un code

Face à un code dont on cherche la complexité, une procédure fiable évite les erreurs. On la déroule de l'intérieur vers l'extérieur.

  1. Identifier la taille nn. C'est la longueur du tableau, de la chaîne, ou la valeur de l'entier traité. Toute la complexité s'exprime en fonction d'elle.
  2. Coter chaque instruction simple à O(1)O(1). Affectations, comparaisons, opérations arithmétiques, accès à une case : coût constant.
  3. Additionner les blocs successifs. Deux morceaux de code exécutés l'un après l'autre : on somme leurs coûts, puis on garde le plus grand (O(n)+O(n2)=O(n2)O(n) + O(n^2) = O(n^2)).
  4. Multiplier pour les boucles. Une boucle de kk tours multiplie par kk le coût de son corps. On imbrique en multipliant, sans oublier de vérifier le vrai nombre de tours de chaque boucle.
  5. Repérer les divisions par deux. Une taille divisée par deux à chaque tour donne un facteur logn\log n.
  6. Simplifier. On supprime les constantes et on ne garde que le terme dominant.

Cette lecture « de l'intérieur vers l'extérieur » se généralise aux fonctions récursives, où l'on écrit une relation de récurrence sur le coût. Elle est aussi le point de départ de la programmation dynamique, dont tout l'intérêt est de transformer une récursivité exponentielle en un calcul polynomial. Le choix de la bonne structure de données joue le même rôle : il fait souvent chuter la complexité d'une classe entière.

3 exercices corrigés · déterminer la complexité

Exercice 1, blocs successifs

Énoncé. Déterminer la complexité en temps, au pire cas, de la fonction suivante en fonction de n=n = len(tab).

def resume(tab):
    n = len(tab)
    total = 0
    for x in tab:            # bloc 1
        total += x
    maxi = tab[0]            # bloc 2
    for x in tab:
        if x > maxi:
            maxi = x
    return total, maxi

Solution. Il y a deux boucles successives, pas imbriquées. Chacune parcourt le tableau une fois avec un corps en O(1)O(1), donc chacune est en O(n)O(n). Les instructions hors boucle sont en O(1)O(1). On additionne les blocs :

T(n)  =  O(n)+O(n)+O(1)  =  O(2n+const)  =  O(n).T(n) \;=\; O(n) + O(n) + O(1) \;=\; O(2n + \text{const}) \;=\; O(n).

La fonction est linéaire, en O(n)O(n). Le piège serait de croire que deux boucles donnent O(n2)O(n^2) : c'est faux, seules des boucles imbriquées se multiplient. Des boucles successives s'additionnent.

Exercice 2, boucles imbriquées

Énoncé. Déterminer la complexité au pire cas de cette recherche de triplets, avec n=n = len(tab).

def contient_zero_somme(tab):
    n = len(tab)
    for i in range(n):
        for j in range(i + 1, n):
            for k in range(j + 1, n):
                if tab[i] + tab[j] + tab[k] == 0:
                    return True
    return False

Solution. Trois boucles imbriquées, chacune parcourant une part du tableau. Le nombre de triplets (i,j,k)(i, j, k) avec i<j<ki < j < k examinés dans le pire cas (aucun triplet trouvé, on va jusqu'au bout) est le nombre de combinaisons de 33 éléments parmi nn :

(n3)  =  n(n1)(n2)6.\binom{n}{3} \;=\; \frac{n(n-1)(n-2)}{6}.

Le terme dominant de ce polynôme est n36\dfrac{n^3}{6}. Le corps de la boucle interne (une addition et une comparaison) est en O(1)O(1). En supprimant la constante 16\dfrac16, la complexité est cubique, en O(n3)O(n^3). La règle générale se lit ici : pp boucles imbriquées de ce type donnent une complexité en O(np)O(n^p).

Exercice 3, division par deux et terme dominant

Énoncé. Déterminer la complexité au pire cas de la fonction suivante, où tab n'est pas trié au départ, avec n=n = len(tab).

def presence_triee(tab, cible):
    t = sorted(tab)                 # etape 1 : tri
    g, d = 0, len(t) - 1
    while g <= d:                    # etape 2 : dichotomie
        m = (g + d) // 2
        if t[m] == cible:
            return True
        elif t[m] < cible:
            g = m + 1
        else:
            d = m - 1
    return False

Solution. On analyse les deux étapes séparément, puis on additionne. L'étape 1, le tri par sorted, est en O(nlogn)O(n \log n) (c'est la complexité des tris efficaces de Python). L'étape 2 est une recherche dichotomique : à chaque tour, la boucle while divise par deux la largeur de l'intervalle [g,d][g, d], elle fait donc environ log2n\log_2 n tours, chacun en O(1)O(1), soit O(logn)O(\log n). On additionne les deux étapes successives :

T(n)  =  O(nlogn)+O(logn)  =  O(nlogn).T(n) \;=\; O(n \log n) + O(\log n) \;=\; O(n \log n).

On ne garde que le terme dominant : nlognn \log n écrase logn\log n. La complexité totale est O(nlogn)O(n \log n), dictée par le tri. C'est un enseignement récurrent : la dichotomie est très rapide, mais le prix à payer est le tri préalable, et c'est lui qui fixe la complexité globale. Si le tableau était déjà trié en entrée, on tomberait à O(logn)O(\log n).

Ce qu'il faut retenir

  • La complexité mesure comment le nombre d'opérations grandit avec la taille nn, indépendamment de la machine, en temps et en espace.
  • La notation grand OO : T(n)=O(f(n))T(n) = O(f(n)) si T(n)Cf(n)T(n) \leq C f(n) à partir d'un certain rang. On supprime les constantes et on garde le terme dominant.
  • Les classes usuelles : O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n).
  • Meilleur / pire / moyen cas : par défaut, on raisonne au pire cas, qui donne une garantie.
  • Les boucles : successives, on additionne ; imbriquées, on multiplie, en vérifiant le vrai nombre de tours. Une division par deux donne un logn\log n.
  • La complexité amortie lisse le coût d'opérations parfois chères (comme append) sur une longue séquence.

L'analyse de complexité est un fil rouge de toute l'informatique de prépa : elle décide quel algorithme de tri ou de recherche utiliser, justifie les structures de données, et fonde la programmation dynamique. C'est aussi un attendu direct à l'écrit comme à l'oral d'informatique aux concours. Si tu hésites entre suivre l'option informatique en MPSI ou viser une filière où l'informatique est centrale comme la MP2I, la place qu'y tient ce raisonnement sur le coût des algorithmes est un bon indicateur de ce qui t'attend.

Partager

FAQ

Questions fréquentes

Accompagnement personnalisé

Besoin d'aide pour réussir votre prépa ?

Nos professeurs, issus de Polytechnique et Centrale, vous accompagnent dans votre réussite avec un suivi sur-mesure.