Aller au contenu

Accueil › PTSI › Informatique

PTSI · Informatique · semaine 3

Boucles imbriquées et complexité quadratique

Recherche d'un facteur dans un texte, des deux valeurs les plus proches dans un tableau, tri à bulles. Notion de complexité quadratique et premiers outils pour valider la correction d'un algorithme.

Sommaire

Le cours

Résumé en cinq points

  1. Une double boucle sur les paires i<ji < j fait n(n−1)/2n(n-1)/2 tours, une double boucle complète en fait n2n^2, dans les deux cas un coût quadratique.
  2. La recherche naïve d'un motif de longueur pp dans un texte de longueur nn coûte au plus (n−p+1) p(n-p+1)\,p comparaisons, quadratique si pp est de l'ordre de nn.
  3. L'écart minimal entre deux valeurs se calcule par examen des paires en coût quadratique, et plus vite après un tri.
  4. Le tri à bulles, en place et stable, fait au pire n(n−1)/2n(n-1)/2 comparaisons. Son invariant est que la fin de la liste contient déjà les plus grands éléments, triés.
  5. Un coût quadratique est multiplié par 4 quand la taille double. Tests, assertions et invariants servent à valider la correction.
Pourquoi ce chapitre

En PCSI, l'informatique sert d'abord d'outil pour la physique et la chimie : traitement de mesures, simulation, résolution numérique. Ces traitements comparent souvent des données entre elles. Chercher un motif dans une séquence, repérer deux raies spectrales trop proches pour être séparées par un spectromètre, classer des espèces par valeur de pKa\mathrm{p}K_a : tout cela fait apparaître une boucle à l'intérieur d'une autre.

Le chapitre donne trois compétences :

  • écrire des boucles imbriquées sans erreur d'indice ;
  • compter les tours pour prévoir le temps de calcul, et reconnaître le coût quadratique, celui qui devient prohibitif quand les données grossissent ;
  • justifier qu'un programme est correct, par des tests, des assertions et des invariants.

Aux concours de la banque PC, ces trois demandes, écrire, évaluer le coût, justifier, structurent la plupart des questions d'informatique.

1Compter les tours de boucles imbriquées

1.1Boucle intérieure de longueur fixe

Propriété 1.1Nombre de tours

Hypothèses

  • la boucle extérieure fait nn tours ;
  • la boucle intérieure fait toujours mm tours ;
  • le corps de la boucle est en coût constant.

Alors le corps est exécuté n×mn \times m fois, et le coût est proportionnel à nmnm.

Pour un tableau de mesures à nn lignes et mm colonnes parcouru case par case, le coût est proportionnel au nombre de cases.

1.2Boucle intérieure dépendant de l'indice extérieur

C'est le cas des algorithmes qui considèrent chaque paire d'éléments une seule fois.

for i in range(n):
    for j in range(i + 1, n):
        ...

Propriété 1.2Nombre de paires

Pour ii fixé, la boucle intérieure fait n−1−in - 1 - i tours. En sommant de i=0i = 0 à i=n−1i = n-1 : (n−1)+(n−2)+⋯+0=n(n−1)2.\begin{aligned} (n-1) + (n-2) + \dots + 0 \\ = \frac{n(n-1)}{2}. \end{aligned} C'est le nombre de paires {i,j}\{i, j\} d'indices distincts.

À retenir

Rappel : 1+2+⋯+k=k(k+1)21 + 2 + \dots + k = \dfrac{k(k+1)}{2}. Ce résultat, vu en mathématiques, sert à chaque calcul de coût.

1.3Boucles cachées

Une seule ligne peut contenir un parcours. Le test x in liste, la copie d'une liste, la concaténation, l'extraction d'une tranche t[a:b] coûtent un temps proportionnel à la longueur concernée.

Attention

Une telle instruction, écrite dans une boucle de nn tours et portant sur une structure de taille de l'ordre de nn, donne un coût de l'ordre de n2n^2, même si le code ne montre qu'une seule boucle. Si la liste ou la tranche est de longueur de l'ordre de nn, le coût est quadratique. Si elle est de longueur bornée, il ne l'est pas.

2Recherche d'un facteur dans un texte

2.1Énoncé

Définition 2.1Facteur

Un mot mm de longueur pp est un facteur d'un texte tt de longueur nn s'il apparaît dans tt sous forme de pp caractères consécutifs.

Exemple 2.2Contextes

  • La séquence de bases « GAATTC », site de reconnaissance d'une enzyme de restriction, est-elle présente dans une séquence d'ADN écrite avec les lettres A, C, G, T ?
  • Le motif « OH » apparaît-il dans une formule chimique écrite en toutes lettres ?

2.2Algorithme naïf

On teste chaque position de départ ii possible, de 00 à n−pn - p. Pour chacune, on compare les caractères un à un tant qu'ils coïncident.

def contient(t, m):
    # Renvoie True si m est un facteur de t.
    n, p = len(t), len(m)
    i = 0
    while i <= n - p:
        k = 0
        while k < p and t[i + k] == m[k]:
            k = k + 1
        if k == p:
            return True
        i = i + 1
    return False

Justifications :

  • La dernière position testée est n−pn - p, car à partir de i=n−p+1i = n - p + 1 il reste moins de pp caractères. Si le motif est plus long que le texte, la boucle extérieure ne s'exécute pas et la fonction renvoie False.
  • Dans la condition du while intérieur, k < p est évalué en premier. Quand k vaut pp, le caractère paresseux de and évite l'accès hors limites m[p].
  • En sortie du while intérieur, k == p signifie que les pp caractères ont coïncidé.
  • Terminaison : la quantité n−p−in - p - i décroît d'une unité à chaque tour de la boucle extérieure, et p−kp - k décroît à chaque tour de la boucle intérieure. Ce sont des variants.

Remarque

Pour le motif vide (p=0p = 0), la boucle intérieure ne s'exécute pas, k == p est vrai dès le premier tour, et la fonction renvoie True : le mot vide est facteur de tout texte.

2.3Coût dans le cas le pire

Chaque position demande au plus pp comparaisons, il y a n−p+1n - p + 1 positions, d'où au plus (n−p+1) p(n-p+1)\,p comparaisons.

Exemple 2.3Pire cas

Ce maximum est atteint pour un texte « AAAA…A » de nn lettres et un motif formé de p−1p - 1 lettres A suivies de C. Chaque position échoue à la dernière comparaison.

Exemple 2.4Cas favorable

Si le motif est en tête du texte, la fonction s'arrête après pp comparaisons. Si la première lettre du motif est absente du texte, chaque position coûte une seule comparaison, soit n−p+1n - p + 1 au total.

Le coût dépend de deux tailles :

  • pour un motif court de longueur fixée, il est linéaire en nn ;
  • pour un motif de longueur proportionnelle à nn, par exemple p=n/2p = n/2, il vaut environ n2/4n^2/4, quadratique.

Des algorithmes plus rapides existent, mais ils ne relèvent pas de ce thème.

3Les deux valeurs les plus proches

3.1Énoncé et contexte

On dispose d'une liste de nn valeurs, par exemple les longueurs d'onde, en nanomètres, des raies d'émission d'un mélange de gaz. On veut l'écart minimal entre deux raies distinctes, pour savoir si un spectromètre de résolution donnée pourra toutes les séparer.

Exemple 3.1Doublet du sodium

Le doublet du sodium, à 589,0 nm589{,}0\ \text{nm} et 589,6 nm589{,}6\ \text{nm}, est l'exemple classique de deux raies très proches.

3.2Algorithme

def ecart_minimal(lam):
    # lam : liste d'au moins deux flottants.
    n = len(lam)
    assert n >= 2
    d_min = None
    for i in range(n - 1):
        for j in range(i + 1, n):
            d = lam[i] - lam[j]
            if d < 0:
                d = -d
            if d_min == None or d < d_min:
                d_min = d
    return d_min
  • On ne considère que les paires i<ji < j, car l'écart est symétrique et qu'une case comparée à elle-même donnerait un écart nul, sans intérêt.
  • La valeur None sert de marqueur « aucun écart encore calculé ».
  • Le test d_min == None est évalué d'abord, et grâce au caractère paresseux de or, la comparaison d < d_min n'est jamais faite avec None.
  • On aurait pu aussi initialiser d_min avec l'écart entre les deux premières valeurs.

Propriété 3.2Invariant

Au début du tour d'indice ii de la boucle extérieure, d_min est le plus petit écart parmi les paires (a,b)(a, b) avec a<ia < i et a<ba < b, ou None si aucune paire n'a encore été vue. En fin de boucle, toutes les paires ont été vues.

Propriété 3.3Coût

Exactement n(n−1)/2n(n-1)/2 comparaisons d'écarts, quelle que soit la liste. Le coût est quadratique dans tous les cas.

3.3Faire mieux en triant

Dans une liste triée, deux valeurs les plus proches sont forcément voisines. Après un tri, il suffit donc de comparer lam[k] et lam[k + 1] pour chaque kk, en un seul parcours. Avec un tri de coût nlog⁡nn \log n, vu au second semestre, le coût total devient de l'ordre de nlog⁡nn \log n.

À retenir

L'amélioration vient d'une propriété mathématique du problème, pas d'une astuce de programmation.

4Tri à bulles

4.1Principe

On veut ranger par ordre croissant une liste, par exemple des valeurs de pKa\mathrm{p}K_a pour ordonner des acides du plus fort au plus faible. Le tri à bulles compare les voisins deux à deux de gauche à droite, et les échange s'ils sont mal ordonnés.

  • À la fin d'un passage, la plus grande valeur de la zone parcourue se retrouve à la dernière place.
  • On recommence sur la zone restante, raccourcie d'une case.
  • On peut s'arrêter dès qu'un passage ne fait aucun échange.
def tri_bulles(t):
    # t : liste de nombres, triée en place.
    fin = len(t) - 1
    echange = True
    while echange and fin > 0:
        echange = False
        for j in range(fin):
            if t[j] > t[j + 1]:
                t[j], t[j + 1] = t[j + 1], t[j]
                echange = True
        fin = fin - 1

La liste passée en argument est modifiée par la fonction, qui ne renvoie rien. C'est possible parce qu'une liste est mutable : la fonction et l'appelant désignent la même liste. Pour conserver la liste d'origine, on trie une copie : u = t.copy(), ou u = list(t), ou u = t[:].

4.2Correction

Propriété 4.1Invariant du passage

Pendant un passage, avant la comparaison d'indice jj, t[j] est le maximum de t[0], …, t[j]. En fin de passage, t[fin] est donc le maximum de la zone t[0], …, t[fin].

Propriété 4.2Invariant de la boucle while

Les cases d'indice strictement supérieur à fin contiennent les plus grands éléments de la liste, rangés par ordre croissant, et la liste est une permutation de la liste de départ.

La boucle s'arrête dans deux cas :

  • fin vaut 0 : toutes les cases sauf la première sont en place, donc la liste est triée ;
  • un passage n'a fait aucun échange : sans échange, t[j]⩽t[j+1]t[j] \leqslant t[j+1] pour tout j<finj < \text{fin}, donc la zone est triée et, avec l'invariant, toute la liste l'est.

==La terminaison est assurée par le variant fin, entier qui décroît d'une unité à chaque passage.==

4.3Coût

Propriété 4.3Coût du tri à bulles

Dans le cas le pire, une liste rangée par ordre décroissant, chaque passage fait des échanges, et le nombre total de comparaisons vaut (n−1)+(n−2)+⋯+1=n(n−1)2.\begin{aligned} (n-1) + (n-2) + \dots + 1 \\ = \frac{n(n-1)}{2}. \end{aligned} Le coût est quadratique.

Pour une liste déjà triée, un seul passage de n−1n - 1 comparaisons suffit. Mais le programme s'intéresse au cas le pire, qui reste quadratique.

4.4Propriétés

  • Le tri à bulles trie en place, sans liste auxiliaire.
  • Il est stable : deux valeurs égales ne sont jamais échangées, la comparaison étant stricte. Cette stabilité compte quand on trie des enregistrements, par exemple des couples (espèce, pKa\mathrm{p}K_a), selon un seul champ.
  • Il est comparatif : il ne fait que comparer des éléments deux à deux.

5Complexité quadratique

5.1Définition

Définition 5.1Complexité quadratique

La complexité en temps d'un algorithme est le nombre d'opérations élémentaires qu'il exécute, en fonction de la taille nn des données, dans le pire des cas. On n'en retient que l'ordre de grandeur quand nn devient grand. Elle est quadratique si elle est de l'ordre de n2n^2, c'est-à-dire encadrée, pour nn assez grand, par deux multiples strictement positifs de n2n^2.

Remarque

Formellement, un coût C(n)C(n) est de l'ordre de n2n^2 s'il existe deux réels a,b>0a, b > 0 et un rang n0n_0 tels que an2⩽C(n)⩽bn2a n^2 \leqslant C(n) \leqslant b n^2 pour tout n⩾n0n \geqslant n_0. Si seule la majoration est connue, on dit que le coût est au plus quadratique.

5.2Lecture physique

L'idée rejoint celle des ordres de grandeur en physique. Un coût quadratique signifie que le temps de calcul est multiplié par 4 quand la taille double, par 100 quand elle est multipliée par 10.

Python exécute de l'ordre de quelques dizaines de millions d'opérations simples par seconde. Un algorithme quadratique traite quelques milliers de mesures presque instantanément, mais un enregistrement d'un million d'échantillons, courant pour une acquisition à haute fréquence, demanderait de l'ordre de 101210^{12} opérations, soit plusieurs heures.

MéthodeVérifier expérimentalement un coût quadratique

  1. Mesurer les durées d'exécution pour des tailles nn, 2n2n, 4n4n.
  2. Tracer le temps en fonction de n2n^2, ou le logarithme du temps en fonction du logarithme de nn.
  3. Dans le second tracé, une droite de pente 2 signale un coût quadratique, exactement comme on identifie une loi de puissance en physique.

5.3Pièges

Attention

  • Deux boucles l'une après l'autre ne font pas un coût quadratique, mais un coût linéaire.
  • Une boucle intérieure dont le nombre de tours est borné par une constante ne change pas l'ordre de grandeur.
  • Une instruction apparemment élémentaire comme x in t ou une tranche peut cacher une boucle.

6Valider la correction d'un algorithme

6.1Tests

Un test compare la sortie d'une fonction à un résultat connu. Un jeu de tests pertinent couvre les cas généraux et les cas limites :

  • pour contient : motif en tête, en fin, au milieu, absent, plus long que le texte, égal au texte, vide ;
  • pour ecart_minimal : deux valeurs seulement, valeurs égales, valeurs négatives ;
  • pour le tri : liste vide, à un élément, déjà triée, triée à l'envers, avec des doublons.

On peut compléter par des tests aléatoires, en comparant le résultat de sa fonction à celui d'une fonction de référence, par exemple sorted pour un tri, sur des listes tirées au hasard avec le module random.

Remarque

Seuls les éléments de l'annexe « Langage Python » sont exigibles. random et sorted ne le sont pas, mais s'utilisent avec leur documentation.

6.2Assertions

L'instruction assert condition arrête le programme avec une erreur si la condition est fausse. On s'en sert pour les préconditions, assert n >= 2, et pour écrire les tests sous une forme compacte, assert contient("ACGAATTCG", "GAATTC"). L'annexe prévoit assert sans message.

6.3Invariants et variants

Définition 6.1Invariant et variant

L'invariant d'une boucle est une propriété vraie à l'entrée et conservée par chaque tour. Combiné à la condition de sortie, il donne le résultat.

Le variant d'une boucle while est un entier naturel (donc minoré) qui décroît strictement à chaque tour, ce qui garantit la terminaison.

Ces notions seront formalisées au second semestre. Dès ce chapitre, on les énonce en une phrase pour chaque algorithme.

À retenir

Un test réussi ne démontre pas qu'un programme est juste, il augmente seulement la confiance. Un invariant prouve la correction de l'algorithme. Les deux se complètent.

Méthodes

MéthodeÉcrire et justifier une double boucle

  1. Décrire en français le rôle de chaque indice avant d'écrire la double boucle.
  2. Contrôler les bornes sur le plus petit cas utile : motif de longueur égale au texte, liste de deux valeurs.
  3. Écrire les conditions d'arrêt en tenant compte de l'évaluation paresseuse (ordre des tests).
  4. Rédiger l'invariant de la boucle extérieure en y faisant apparaître le résultat partiel.
  5. Prévoir les cas limites dans un jeu de tests écrit avec des assertions.

MéthodeÉvaluer un coût

  1. Exprimer le nombre de tours intérieurs en fonction de l'indice extérieur.
  2. Sommer sur l'indice extérieur.
  3. Vérifier que le corps de boucle est en coût constant, sans boucle cachée.

Ce qu'il faut savoir refaire en khôlle

  1. Écrire la recherche naïve d'un motif dans une séquence, en justifiant les bornes et l'ordre des conditions.
  2. Donner le coût de cette recherche dans le pire des cas, avec un exemple d'entrée qui l'atteint, et discuter selon la longueur du motif.
  3. Écrire le calcul de l'écart minimal entre deux valeurs d'une liste, et montrer qu'il fait n(n−1)/2n(n-1)/2 comparaisons.
  4. Expliquer pourquoi un tri préalable permet un algorithme plus rapide pour ce problème.
  5. Écrire le tri à bulles avec arrêt anticipé, énoncer ses invariants, et donner son coût dans le pire et dans le meilleur cas.
  6. Expliquer ce que signifient tri en place et tri stable, et vérifier ces propriétés pour le tri à bulles.
  7. Définir la complexité quadratique et proposer une vérification expérimentale par mesure de durées.
  8. Proposer un jeu de tests et des assertions pour une fonction donnée.

Méthodes et exercices

Les résultats à connaître

Dans ce chapitre, les « résultats » sont des dénombrements de tours, des preuves de correction et des coûts dans le pire cas.

  • Deux boucles imbriquées de n et m tours exécutent le corps n × m fois : exigible.
  • La double boucle sur les paires i < j fait n(n − 1)/2 tours, celle sur i ≤ j en fait n(n + 1)/2 : exigible avec démonstration.
  • 1 + 2 + … + k = k(k + 1)/2 : exigible, démontré en mathématiques.
  • Deux boucles successives de n tours ont un coût linéaire : exigible.
  • Une boucle intérieure dont le nombre de tours est borné par une constante ne change pas l'ordre de grandeur : exigible.
  • La recherche naïve d'un facteur est correcte et fait au plus (n − p + 1)p comparaisons. Cette borne est atteinte : exigible avec justification.
  • L'écart minimal par examen des paires demande exactement n(n − 1)/2 comparaisons : exigible.
  • Dans une liste triée, deux valeurs les plus proches sont voisines : exigible avec démonstration.
  • Le tri à bulles avec arrêt anticipé est correct et termine, par invariant et variant : exigible avec justification.
  • Le tri à bulles fait n(n − 1)/2 comparaisons dans le pire cas, atteint pour une liste strictement décroissante. Il en fait n − 1 pour une liste déjà triée : exigible.
  • Le tri à bulles à comparaison stricte est en place et stable : exigible.
  • Le nombre d'échanges du tri à bulles est égal au nombre d'inversions de la liste : classique, non exigible en tant que tel, à savoir redémontrer.
  • Un coût quadratique est multiplié par environ 4 quand n double : exigible.
  • Hors programme signalé : les algorithmes rapides de recherche de motif (Knuth-Morris-Pratt, Boyer-Moore) et l'étude du coût en moyenne. La méthode sort, la fonction sorted et le module time ne s'utilisent qu'avec leur documentation.

Clés de démonstration

Compter les paires. Pour i fixé, la boucle for j in range(i + 1, n) fait n − 1 − i tours. On somme de i = 0 à n − 1 et l'on reconnaît 0 + 1 + … + (n − 1). L'astuce est le dénombrement : chaque tour correspond à une paire {i, j} et à une seule, et il y a n(n − 1)/2 paires.

Recherche de facteur. On écrit un invariant par boucle. Boucle intérieure : les k premiers caractères de m coïncident avec t[i], …, t[i + k − 1]. Boucle extérieure : m n'apparaît à aucune position strictement inférieure à i. En sortie de la boucle intérieure, soit k = p et m est trouvé en i, soit un caractère diffère et l'invariant extérieur reste vrai pour i + 1. L'astuce est de nommer séparément ce que prouve chaque boucle.

Voisins dans une liste triée. Soit u triée et i < j. L'écart u[j] − u[i] est la somme télescopique des écarts u[k + 1] − u[k] pour k allant de i à j − 1. Tous ces termes sont positifs, donc u[j] − u[i] est supérieur ou égal à u[i + 1] − u[i]. L'astuce est le télescopage : le minimum sur toutes les paires est atteint sur une paire de voisins.

Passage du tri à bulles. On montre par récurrence sur j qu'avant la comparaison d'indice j, t[j] est le maximum de t[0], …, t[j]. Si t[j] > t[j + 1], l'échange amène ce maximum en j + 1. Sinon, t[j + 1] est déjà le plus grand. L'astuce est de suivre la plus grande valeur rencontrée, qui avance comme une bulle.

Pire cas du tri à bulles. Sur une liste strictement décroissante, le premier passage décale toutes les valeurs d'une case vers la gauche et place le maximum à la fin. La zone restante est donc encore strictement décroissante. Par récurrence, chaque passage fait un échange tant que la zone a deux cases. On compte alors n − 1 passages et (n − 1) + … + 1 comparaisons. L'astuce est de montrer que la forme de l'entrée se conserve d'un passage au suivant.

Terminaison. Chaque boucle while se justifie par un entier positif qui décroît strictement : n − p − i pour la recherche, p − k pour la boucle intérieure, fin pour les bulles. L'astuce est d'écrire ce variant en une phrase, même quand il paraît évident.

Exemples et contre-exemples à connaître

  1. La liste [5, 1, 4]. Si l'on compare seulement les voisins, on obtient 3, alors que 5 et 4 sont à distance 1. L'argument des voisins exige une liste triée.
  2. Le texte « AAAA…A » et le motif « AA…AC ». Chaque position échoue à la dernière comparaison. C'est l'entrée qui réalise le pire cas de la recherche naïve.
  3. Le motif vide. La fonction contient de la fiche renvoie True, même pour un texte vide. C'est la convention usuelle, mais il faut la signaler.
  4. Le motif « AA » dans « AAAA ». Il a trois occurrences, car elles se chevauchent. Un compteur qui avance de p cases après un succès n'en trouve que deux.
  5. Une liste strictement décroissante. Elle réalise le pire cas du tri à bulles, avec n(n − 1)/2 comparaisons et autant d'échanges.
  6. Le tri à bulles écrit avec >=. Il trie encore et il termine grâce au variant fin. Mais il échange les valeurs égales, donc il perd la stabilité. Sur une liste constante, il fait tous les passages au lieu d'un seul.
  7. for i in range(n): if t[i] in t[i + 1:]. On ne voit qu'une boucle, mais la tranche et le test in en cachent une seconde. Le coût est quadratique.
  8. Un seul passage de bulles. Il trie [2, 1] et [1, 3, 2], mais il transforme [3, 2, 1] en [2, 1, 3]. Deux tests réussis ne prouvent rien.
  9. Le test de symétrie d'une matrice n × n. Il demande n(n − 1)/2 comparaisons : c'est quadratique en n, mais linéaire en n², le nombre de cases. Le mot « quadratique » n'a de sens qu'une fois la taille précisée.

Techniques du chapitre

Technique 1, le tableau des tours. Mini-exercice : combien de fois s'exécute le corps de for i in range(n): for j in range(i): ... ? Pour i = 0, 1, …, n − 1, la boucle intérieure fait i tours. Le total vaut 0 + 1 + … + (n − 1) = n(n − 1)/2.

Technique 2, réutiliser le tour précédent. Mini-exercice : calculer les sommes cumulées c[k] = t[0] + … + t[k]. Refaire chaque somme par une boucle intérieure coûte de l'ordre de n²/2. Avec c[k] = c[k − 1] + t[k], chaque terme coûte une addition, et le total est linéaire.

Technique 3, sortir des deux boucles par return. Mini-exercice : renvoyer le premier couple (i, j), avec i < j, de mesures égales. On écrit la double boucle dans une fonction et l'on renvoie (i, j) dès que t[i] == t[j]. Le return interrompt les deux boucles d'un coup, sans variable drapeau.

Technique 4, fabriquer l'entrée du pire cas. Mini-exercice : quel est le pire cas d'une recherche de doublon par double boucle ? La sortie anticipée n'a lieu que si deux valeurs sont égales. Le pire cas est donc une liste sans doublon, qui impose les n(n − 1)/2 comparaisons.

Technique 5, trier puis ne regarder que les voisins. Mini-exercice : tester l'absence de doublon dans une liste triée u. Deux valeurs égales y sont forcément voisines. Un parcours de k = 0 à n − 2 qui compare u[k] et u[k + 1] suffit. Son coût est linéaire.

Technique 6, tracer un passage à la main. Mini-exercice : premier passage du tri à bulles sur [4, 1, 3, 2]. On échange 4 et 1, puis 4 et 3, puis 4 et 2. On obtient [1, 3, 2, 4] : le maximum est en place, le reste n'est pas encore trié.

Technique 7, lire un rapport de durées. Mini-exercice : un programme quadratique met 2 s pour n = 10 000. Combien pour n = 30 000 ? La taille est multipliée par 3, donc la durée par 9, soit environ 18 s.

Exercices classiques corrigés

Choisis ton niveau. Difficulté indicative ; les exercices qui se suivent restent dans l'ordre.

Exercice 1. Donner, en fonction de n, la valeur finale de c

Classiques de khôlle et concours · Khôlle

c = 0
for i in range(n):
    for j in range(i, n):
        for k in range(3):
            c = c + 1

Technique tableau des tours.

Voir le corrigé
  1. Pour i et j fixés, la boucle en k fait 3 tours.
  2. Pour i fixé, j parcourt i, …, n − 1, soit n − i valeurs.
  3. On somme n + (n − 1) + … + 1 = n(n + 1)/2, puis on multiplie par 3 : c = 3n(n + 1)/2.
  4. Le coût est quadratique.

piège : Conclure à un coût cubique parce qu'il y a trois boucles. La troisième boucle a une longueur constante.

Exercice 2

Classiques de khôlle et concours · Khôlle

Compter les occurrences d'un motif non vide m dans une séquence d'ADN t, chevauchements compris.

Technique recherche naïve sans sortie anticipée.

Voir le corrigé
  1. Idée : on teste toutes les positions de 0 à n − p et l'on incrémente le compteur à chaque succès.
  2. Code :
def occurrences_motif(t, m):
    n, p = len(t), len(m)
    c = 0
    for i in range(n - p + 1):
        k = 0
        while k < p and t[i + k] == m[k]:
            k = k + 1
        if k == p:
            c = c + 1
    return c
  1. Si p > n, range(n - p + 1) est vide et la fonction renvoie 0. On vérifie que occurrences_motif("AAAA", "AA") vaut 3.
  2. Coût : au plus (n − p + 1)p comparaisons, comme la recherche de la fiche. Ici, un succès ne fait pas sortir, donc le texte « AAAA » et le motif « AA » sont aussi un pire cas.

piège : Écrire range(n - p), qui manque une occurrence placée en fin de texte.

Exercice 3

Classiques de khôlle et concours · Khôlle

Le caractère « ? » du motif s'accorde avec n'importe quelle base. Renvoyer la première position du motif dans t, ou −1 s'il est absent.

Technique modifier la condition de la boucle intérieure.

Voir le corrigé
  1. Idée : la case k du motif convient si m[k] vaut « ? » ou si elle est égale à t[i + k].
  2. Code :
def premiere_position(t, m):
    n, p = len(t), len(m)
    for i in range(n - p + 1):
        k = 0
        while k < p and (m[k] == "?" or t[i + k] == m[k]):
            k = k + 1
        if k == p:
            return i
    return -1
  1. Le return i est correct grâce à l'invariant extérieur : aucune position antérieure ne convenait.
  2. L'ordre de grandeur du coût est inchangé. Il y a au plus (n − p + 1)p tours de la boucle intérieure, chacun avec au plus deux comparaisons de caractères, soit au plus 2(n − p + 1)p comparaisons.

piège : Oublier les parenthèses. Comme and est prioritaire sur or, la condition devient (k < p and m[k] == "?") or t[i + k] == m[k]. Quand k vaut p, le programme lit alors m[p], ce qui provoque une erreur d'indice.

Exercice 4

Classiques de khôlle et concours · Khôlle

Tester si toutes les valeurs d'une liste sont distinctes. Donner le coût, puis la méthode par tri.

Technique paires i < j, sortie anticipée, pire cas.

Voir le corrigé
  1. Code :
def tous_distincts(t):
    n = len(t)
    for i in range(n):
        for j in range(i + 1, n):
            if t[i] == t[j]:
                return False
    return True
  1. Invariant extérieur : au début du tour i, aucune paire (a, b) avec a < i et a < b n'est formée de valeurs égales.
  2. Pire cas : une liste sans doublon, qui impose n(n − 1)/2 comparaisons.
  3. Après un tri, deux valeurs égales sont voisines. Un parcours linéaire suffit, et le coût total devient celui du tri.

piège : Écrire range(i, n). Chaque case est alors comparée à elle-même, et la fonction renvoie toujours False.

Exercice 5. Écrire le tri par sélection, donner son coût et montrer qu'il n'est pas stable

Classiques de khôlle et concours · Khôlle vers écrit

Technique double boucle avec recherche d'un minimum.

Voir le corrigé
  1. Idée : au tour i, on cherche l'indice k du minimum de t[i], …, t[n − 1], puis on échange t[i] et t[k].
  2. Code :
def tri_selection(t):
    n = len(t)
    for i in range(n - 1):
        k = i
        for j in range(i + 1, n):
            if t[j] < t[k]:
                k = j
        t[i], t[k] = t[k], t[i]
  1. Invariant : au début du tour i, t[0], …, t[i − 1] sont les i plus petites valeurs, rangées par ordre croissant.
  2. Coût : n(n − 1)/2 comparaisons pour toutes les entrées, et au plus n − 1 échanges.
  3. Contre-exemple de stabilité : on note 2a et 2b deux valeurs égales à 2, dans cet ordre. Sur [2a, 2b, 1], le premier tour échange 2a et 1, ce qui donne [1, 2b, 2a]. L'ordre des deux 2 est inversé.

piège : Croire que la comparaison stricte suffit à rendre le tri stable. Ici, c'est l'échange à longue distance qui déplace 2a derrière 2b.

Exercice 6

Classiques de khôlle et concours · D'après un écrit CCINP PC

Une matrice carrée est donnée sous forme de liste de n listes de longueur n. Tester si elle est symétrique.

Technique ne parcourir que le triangle supérieur.

Voir le corrigé
  1. Idée : on compare M[i][j] et M[j][i] pour i < j seulement. La diagonale n'impose aucune condition.
  2. Code :
def est_symetrique(M):
    n = len(M)
    for i in range(n):
        for j in range(i + 1, n):
            if M[i][j] != M[j][i]:
                return False
    return True
  1. Pire cas : une matrice symétrique, qui demande n(n − 1)/2 comparaisons.
  2. Ce coût est quadratique en n, mais linéaire en la taille des données, qui comporte n² cases.

piège : Faire parcourir à j tout range(n). Le résultat reste juste, mais on fait deux fois plus de comparaisons, et l'on présente alors mal le coût.

Exercice 7

Classiques de khôlle et concours · Type écrit e3a

Une liste de couples (nom, pKa) est déjà rangée par ordre alphabétique. On la trie par pKa croissant avec le tri à bulles de la fiche. Quelle modification faut-il faire ? Dans quel ordre sortent deux espèces de même pKa ?

Technique tri selon un champ, stabilité.

Voir le corrigé
  1. On remplace le test par t[j][1] > t[j + 1][1]. Seul le second champ est comparé.
  2. La comparaison est stricte, donc deux couples de même pKa ne sont jamais échangés. Leur ordre relatif est conservé.
  3. Deux espèces de même pKa restent donc dans l'ordre alphabétique : c'est l'intérêt de la stabilité.
  4. Avec >=, deux voisins de même pKa sont échangés à chaque rencontre. L'ordre final entre eux n'est plus garanti.

piège : Comparer les couples entiers avec t[j] > t[j + 1], qui compare d'abord les noms.

Exercice 8

Classiques de khôlle et concours · D'après un écrit CCINP PC

Une inversion de t est une paire i < j telle que t[i] > t[j]. Écrire une fonction qui compte les inversions. Montrer que le tri à bulles fait exactement autant d'échanges que la liste a d'inversions.

Technique double boucle sur les paires, puis quantité qui diminue à chaque échange.

Voir le corrigé
  1. Code :
def inversions(t):
    c = 0
    n = len(t)
    for i in range(n):
        for j in range(i + 1, n):
            if t[i] > t[j]:
                c = c + 1
    return c
  1. Échanger deux voisins t[j] > t[j + 1] supprime l'inversion formée par ces deux valeurs.
  2. Pour toute autre valeur, sa position par rapport à chacune des deux est inchangée. Les autres paires ne bougent donc pas, et le nombre d'inversions baisse d'exactement 1.
  3. En fin de tri, la liste est triée et n'a plus d'inversion. Le nombre d'échanges est donc le nombre initial d'inversions, au plus n(n − 1)/2. Ce maximum est atteint pour une liste strictement décroissante.

piège : Oublier l'étape 3. Il faut justifier qu'un échange ne crée aucune inversion nouvelle.

Exercice 9

Approfondissement · Type écrit Mines

On enregistre des variations d'énergie successives t[0], …, t[n − 1], liste non vide. Trouver la plus grande somme t[i] + … + t[j] avec i ≤ j, d'abord naïvement, puis en coût quadratique.

Technique réutiliser le tour précédent.

Voir le corrigé
  1. Méthode naïve : pour chaque couple (i, j), recalculer la somme par une boucle. On obtient trois boucles imbriquées et un coût de l'ordre de n³.
  2. Amélioration : pour i fixé, la somme jusqu'à j s'obtient en ajoutant t[j] à la somme jusqu'à j − 1.
def somme_max(t):
    meilleur = t[0]
    for i in range(len(t)):
        s = 0
        for j in range(i, len(t)):
            s = s + t[j]
            if s > meilleur:
                meilleur = s
    return meilleur
  1. Invariant intérieur : après le traitement de j, s = t[i] + … + t[j] et meilleur est le maximum des sommes déjà formées.
  2. Coût : n(n + 1)/2 additions, donc quadratique. Un algorithme linéaire existe, mais il sort du thème.

piège : Initialiser meilleur à 0, ce qui donne 0 pour une liste de valeurs toutes négatives.

Exercice 10

Approfondissement · Type oral Centrale

On chronomètre une fonction : 0,12 s pour n = 1 000, 0,47 s pour n = 2 000, 1,9 s pour n = 4 000. Quelle complexité peut-on conjecturer ? Quelle durée faut-il prévoir pour n = 100 000 ?

Technique rapports de durées et tracé logarithmique.

Voir le corrigé
  1. Les rapports successifs valent environ 3,9 et 4,0 quand n double. Cela suggère un coût quadratique.
  2. En échelle logarithmique, la pente vaut ln(1,9/0,12) / ln(4), soit environ 2,76 / 1,39, c'est-à-dire environ 2.
  3. De n = 1 000 à n = 100 000, la taille est multipliée par 100, donc la durée par 10⁴. On prévoit environ 1 200 s, soit 20 minutes.
  4. On mesure sur plusieurs tailles assez grandes. Pour de petites tailles, les coûts fixes et le bruit de mesure faussent les rapports.

piège : Extrapoler linéairement et annoncer 12 s, ou tirer une conclusion d'une seule mesure.

Erreurs fréquentes

  1. Écrire range(i, n) au lieu de range(i + 1, n) dans une double boucle sur les paires. Une case comparée à elle-même donne un écart nul ou un faux doublon.
  2. Se tromper sur la dernière position d'une recherche de motif. La borne correcte est range(n - p + 1). On teste toujours un motif placé en fin de texte et un motif égal au texte.
  3. Chiffrer un coût en comptant les boucles au lieu des tours. Une boucle de longueur constante ne compte pas, deux boucles successives s'additionnent, et in, une tranche ou une concaténation cachent une boucle.
  4. Mal écrire les conditions composées : placer t[i + k] == m[k] avant k < p, ou oublier les parenthèses autour d'un or combiné à un and.
  5. Confondre pire cas et meilleur cas, par exemple annoncer un coût linéaire pour le tri à bulles parce qu'il s'arrête vite sur une liste triée. Autre confusion : croire qu'un jeu de tests réussi remplace l'invariant.

Fiche de cours rédigée à partir du programme officiel d'informatique commune des filières MPSI, PCSI, PTSI, MP, PC, PSI et PT (2021), premier semestre, thème « Algorithmes opérant sur une structure séquentielle par boucles imbriquées ». Le texte est le même pour toutes les filières. Les activités qu'il propose sont la recherche d'un facteur dans un texte, la recherche des deux valeurs les plus proches dans un tableau et le tri à bulles, avec pour objectifs la notion de complexité quadratique et des outils pour valider la correction des algorithmes. Ces activités sont au choix du professeur. Seuls les éléments de l'annexe « Langage Python » sont exigibles, et le coût s'estime asymptotiquement dans le cas le pire. Les exemples sont pris dans le contexte de la physique et de la chimie de PCSI.

Au concours

Les questions des sujets corrigés qui travaillent ce chapitre. La première question de chaque sujet est en accès libre.

CCINP 2025, PC, Modélisation de systèmes physiques ou chimiques

  • Q34 à Q36 Profondeur du front de carbonatation : seuil, lecture et tracé
  • Q42 à Q45 Contour d'une image et diamètre maximal par boucles imbriquées