Le cours
Résumé en cinq points
- Deux boucles imbriquées sur éléments, indépendantes ou sur les paires , font de l'ordre de tours, précisément ou .
- La recherche naïve d'un facteur de longueur dans un texte de longueur coûte au pire comparaisons, quadratique en quand est proportionnel à .
- Chercher les deux valeurs les plus proches par examen des paires est quadratique. Trier d'abord permet de faire mieux.
- Le tri à bulles fait comparaisons. Il est en place, stable et comparatif, et sa correction repose sur l'invariant « les dernières cases sont les plus grands éléments, triés ».
- Un coût quadratique quadruple quand la taille double. On valide un algorithme par des tests couvrant les cas limites, des assertions et des invariants.
Pourquoi ce chapitre
Au chapitre précédent, chaque algorithme parcourait un tableau une seule fois, avec un coût linéaire. Beaucoup de problèmes demandent de comparer les éléments deux à deux, ou de tester une position de départ puis de lire plusieurs cases à partir d'elle. On écrit alors une boucle à l'intérieur d'une autre.
Ce chapitre apprend trois choses :
- écrire correctement des boucles imbriquées, avec des bornes justes, première source d'erreurs d'indice ;
- compter le nombre de tours pour estimer le coût, et reconnaître une complexité quadratique ;
- justifier qu'un algorithme fait ce qu'on attend, par des tests bien choisis, des assertions et des invariants de boucle.
Aux concours, les questions de ce type sont systématiques : « écrire une fonction qui… », puis « déterminer sa complexité dans le pire cas », puis « justifier sa correction ».
1Boucles imbriquées et comptage des tours
1.1Deux formes à distinguer
Définition 1.1Boucles indépendantes
Deux boucles imbriquées sont indépendantes quand la boucle intérieure a toujours le même nombre de tours.
for i in range(n):
for j in range(n):
... # corps exécuté n × n fois
Le corps est exécuté fois.
Définition 1.2Boucles dépendantes
Deux boucles imbriquées sont dépendantes quand la borne de la boucle intérieure dépend de l'indice de la boucle extérieure. C'est le cas de tous les algorithmes qui examinent les paires avec .
for i in range(n):
for j in range(i + 1, n):
... # corps exécuté (n-1) + ... + 0 fois
Le nombre total de tours vaut la somme des pour allant de à : C'est exactement le nombre de paires d'éléments distincts d'un ensemble à éléments.
1.2Formules à connaître
Propriété 1.3Comptages usuels
- ;
- le nombre de paires avec parmi indices est ;
- une boucle de tours contenant une opération de coût linéaire en coûte de l'ordre de .
Attention
Boucles cachées : une instruction comme x in t, t.copy(), t + u ou t[a:b] parcourt une liste. Placée dans une boucle de tours, elle crée une double boucle invisible à la lecture.
2Recherche d'un facteur dans un texte
2.1Le problème
Définition 2.1Facteur
Un mot de longueur est un facteur d'un texte de longueur s'il apparaît dans en positions consécutives, c'est-à-dire s'il existe un indice tel que t[i + k] == m[k] pour tout entre et .
Exemple 2.2
« ana » est facteur de « banane », à la position 1. Le mot vide est facteur de tout texte.
2.2Algorithme naïf
On essaie toutes les positions de départ possibles, de à inclus. Pour chacune, on compare le mot au texte caractère par caractère, et on s'arrête à la première différence.
def position_facteur(m, t):
# Renvoie le plus petit i tel que m apparaît dans t
# à partir de l'indice i, ou -1 sinon.
n, p = len(t), len(m)
for i in range(n - p + 1):
k = 0
while k < p and t[i + k] == m[k]:
k = k + 1
if k == p:
return i
return -1
Trois points de vigilance :
- la borne du
range: avecrange(n), l'expressiont[i + k]sortirait du texte. Si ,range(n - p + 1)est vide et la fonction renvoie , ce qui est correct ; - l'ordre des conditions du
while: le testk < pest placé avantt[i + k] == m[k], et le caractère paresseux deandévite de lirem[p]; - le test
k == paprès lewhiledistingue les deux sorties possibles de la boucle : le mot entièrement reconnu, ou une différence trouvée.
2.3Coût
Pour chaque position , la boucle intérieure fait au plus comparaisons. Il y a positions. Dans le cas le pire, le coût est de l'ordre de comparaisons.
Exemple 2.3Cas le pire effectif
Soit formé de lettres a et formé de lettres a suivies d'un b. Chaque position demande exactement comparaisons avant d'échouer. Pour de l'ordre de , le coût est de l'ordre de , donc quadratique en la longueur du texte. Pour un mot de longueur fixée, petite devant , le coût est au contraire linéaire en .
Il existe des algorithmes plus efficaces, mais ils sont hors du programme de ce thème. L'intérêt ici est de reconnaître la structure « position de départ, puis lecture à partir de cette position ».
3Recherche des deux valeurs les plus proches dans un tableau
3.1Le problème
Étant donné une liste t d'au moins deux nombres, on cherche deux éléments d'indices distincts dont l'écart, en valeur absolue, est minimal. On renvoie par exemple le couple d'indices.
3.2Algorithme par examen de toutes les paires
def plus_proches(t):
# t : liste d'au moins deux nombres.
# Renvoie (i, j), i < j, d'écart minimal.
n = len(t)
assert n >= 2
meilleur = (0, 1)
d_min = t[1] - t[0]
if d_min < 0:
d_min = -d_min
for i in range(n):
for j in range(i + 1, n):
d = t[j] - t[i]
if d < 0:
d = -d
if d < d_min:
d_min = d
meilleur = (i, j)
return meilleur
On calcule la valeur absolue à la main pour rester dans le noyau de l'annexe. La fonction abs de Python n'est pas listée dans l'annexe et devrait donc être accompagnée de sa documentation dans un sujet.
Propriété 3.1Invariant de la boucle extérieure
Au début du tour d'indice , d_min est l'écart minimal parmi toutes les paires avec et , ainsi que la paire initiale , et meilleur est une paire qui réalise cet écart.
À la fin, toutes les paires ont été examinées, donc le résultat est correct.
La boucle intérieure est exécutée fois en tout, chaque tour faisant un nombre constant d'opérations. Le coût est quadratique dans tous les cas, pas seulement le pire, puisqu'il n'y a aucune sortie anticipée.
3.3Remarque sur une meilleure méthode
Si le tableau est trié, les deux valeurs les plus proches sont forcément voisines, et un seul parcours suffit. Le coût total devient celui du tri suivi d'un parcours linéaire. Avec un tri efficace, vu plus tard dans l'année, on passe d'un coût quadratique à un coût de l'ordre de .
À retenir
Un coût quadratique n'est pas une fatalité : une idée algorithmique vaut mieux qu'une optimisation de détail.
4Tri à bulles
4.1Principe
Trier une liste, c'est la réorganiser par ordre croissant. Le tri à bulles parcourt la liste de gauche à droite en comparant chaque élément à son voisin de droite, et les échange s'ils sont dans le mauvais ordre. Au cours d'un parcours, le plus grand élément non encore placé est poussé jusqu'au bout de la zone non triée, comme une bulle qui remonte. On recommence sur une zone raccourcie d'une case.
def tri_bulles(t):
# Trie t en place par ordre croissant.
n = len(t)
for k in range(n - 1):
for j in range(n - 1 - k):
if t[j] > t[j + 1]:
t[j], t[j + 1] = t[j + 1], t[j]
L'échange utilise le dépaquetage de tuples, qui fait partie de l'annexe. La fonction ne renvoie rien. Elle modifie la liste reçue, ce qui est possible parce qu'une liste est mutable et que la fonction reçoit une référence vers la même liste. L'appelant constate donc la modification.
4.2Correction
Propriété 4.1Invariant de la boucle intérieure
Au moment de comparer t[j] et t[j + 1], t[j] est le plus grand des éléments t[0], …, t[j]. Après le tour, t[j + 1] est donc le plus grand de t[0], …, t[j + 1].
En fin de boucle intérieure, la dernière case de la zone parcourue contient le maximum de cette zone.
Propriété 4.2Invariant de la boucle extérieure
Après parcours, les dernières cases contiennent les plus grands éléments de la liste, rangés par ordre croissant, et la liste est une permutation de la liste initiale, puisqu'on ne fait que des échanges.
Après parcours, les dernières cases sont en place, donc la première aussi, et la liste est triée.
Terminaison : les deux boucles sont des boucles for sur des range finis. Elles terminent toujours.
4.3Coût
Le nombre de comparaisons vaut quelle que soit la liste. Le nombre d'échanges est au plus égal à ce nombre, atteint pour une liste triée dans l'ordre décroissant. Le coût est quadratique.
Amélioration classique : si un parcours complet ne fait aucun échange, la liste est déjà triée et on peut s'arrêter. On ajoute un booléen mis à False au début de chaque parcours et à True à chaque échange, et l'on remplace la boucle extérieure par un while. Sur une liste déjà triée, le coût devient linéaire. Le coût dans le cas le pire reste quadratique.
4.4Caractéristiques
Le tri à bulles a trois caractéristiques, reprises au chapitre sur les tris :
- il est en place : il n'utilise pas d'autre tableau que la liste elle-même ;
- il est stable : deux éléments égaux ne sont jamais échangés grâce à la comparaison stricte, donc leur ordre relatif est conservé ;
- il est comparatif : il n'utilise que des comparaisons entre éléments.
5Complexité quadratique
5.1Définition
Définition 5.1Coût et complexité quadratique
Le coût, ou complexité en temps, d'un algorithme est le nombre d'opérations élémentaires qu'il effectue, exprimé en fonction de la taille de l'entrée. On s'intéresse au cas le pire, c'est-à-dire au maximum de ce nombre sur toutes les entrées de taille , et à son ordre de grandeur quand devient grand.
Le coût est quadratique s'il est de l'ordre de , c'est-à-dire compris, pour assez grand, entre deux constantes strictement positives fois .
On ne cherche pas le nombre exact d'opérations. Les constantes dépendent de la machine et de la façon de compter. Seule la forme de la croissance compte.
5.2Conséquences pratiques
Un coût quadratique signifie que doubler la taille de l'entrée multiplie le temps d'exécution par quatre environ, et que multiplier la taille par dix le multiplie par cent.
Un interpréteur Python exécute de l'ordre de quelques dizaines de millions d'opérations simples par seconde. Un algorithme quadratique traite donc confortablement quelques milliers d'éléments, mais un million d'éléments demanderait de l'ordre de opérations, soit des heures. Un algorithme linéaire traite le même million en une fraction de seconde.
Ce raisonnement permet de vérifier expérimentalement une complexité. On mesure les temps d'exécution pour , , , et l'on regarde s'ils sont multipliés par 2 ou par 4 d'une mesure à la suivante. La mesure du temps demande un module, time par exemple, dont la connaissance n'est pas exigible.
5.3Erreurs fréquentes
Attention
- Conclure qu'un algorithme est quadratique dès qu'il contient deux boucles. Deux boucles successives, non imbriquées, donnent un coût linéaire. Deux boucles imbriquées dont l'intérieure a un nombre de tours borné par une constante donnent aussi un coût linéaire.
- Oublier les boucles cachées dans
in, les copies et les tranches. - Confondre cas le pire et cas moyen. Le programme ne demande que le cas le pire.
6Outils pour valider la correction
Le programme demande de proposer des outils pour valider la correction des algorithmes. Trois outils complémentaires sont utilisés dès le premier semestre.
6.1Jeux de tests
Un jeu de tests est une liste d'entrées dont on connaît la sortie attendue. Un bon jeu couvre les cas généraux et les cas limites.
- Recherche de facteur : un mot au début, au milieu, à la fin du texte, un mot absent, un mot plus long que le texte, le mot vide, un mot égal au texte.
- Tri : une liste vide, une liste à un élément, une liste déjà triée, une liste triée à l'envers, une liste avec des doublons.
On peut aussi comparer le résultat d'une fonction à celui d'une fonction de référence sur de nombreuses entrées tirées au hasard, par exemple comparer tri_bulles à la fonction sorted de Python. Le module random et la fonction sorted ne sont pas exigibles, mais leur usage documenté est naturel en travaux pratiques.
6.2Assertions
L'instruction assert condition interrompt l'exécution avec une erreur si la condition est fausse. On l'utilise pour vérifier une précondition à l'entrée d'une fonction, par exemple assert len(t) >= 2 dans plus_proches, ou pour vérifier le résultat d'un test, par exemple assert position_facteur("ana", "banane") == 1. L'annexe mentionne assert sans message d'erreur.
6.3Invariants et variants
Définition 6.1Invariant et variant
Un invariant de boucle est une propriété vraie avant la boucle et conservée par chaque tour. En sortie de boucle, il donne la correction.
Un variant est une quantité entière positive qui décroît strictement à chaque tour d'une boucle while, ce qui prouve sa terminaison.
Exemple 6.2
Dans position_facteur, la quantité est un variant de la boucle while intérieure.
Ces notions seront formalisées au second semestre. Dès maintenant, il faut savoir énoncer l'invariant d'un algorithme en une phrase.
Un test ne prouve jamais qu'un programme est correct, il peut seulement montrer qu'il est faux. Un invariant correctement démontré prouve la correction de l'algorithme, mais pas l'absence d'erreur de frappe dans le code. Les deux démarches sont complémentaires.
Méthodes
MéthodeÉcrire et justifier des boucles imbriquées
- Avant d'écrire, écrire en français ce que représente chaque indice : position de départ, indice de lecture, première et seconde case d'une paire.
- Vérifier les bornes des
rangesur le plus petit cas utile, par exemple une liste de deux éléments ou un mot de longueur égale au texte. - Pour le coût, exprimer le nombre de tours de la boucle intérieure en fonction de l'indice extérieur, puis sommer.
- Pour la correction, énoncer l'invariant de la boucle extérieure en une phrase qui contient la réponse partielle.
- Tester systématiquement les cas limites : vide, un élément, absence, doublons.
Ce qu'il faut savoir refaire en khôlle
- Écrire la recherche naïve d'un facteur dans un texte, justifier les bornes, et donner son coût dans le cas le pire avec un exemple qui le réalise.
- Écrire la recherche des deux valeurs les plus proches par examen des paires, et montrer qu'elle fait comparaisons.
- Expliquer comment trier le tableau permet de faire mieux pour ce problème.
- Écrire le tri à bulles, énoncer ses deux invariants, et compter ses comparaisons.
- Écrire la version avec arrêt anticipé et donner son coût sur une liste déjà triée.
- Définir la complexité quadratique et prévoir l'effet d'un doublement de la taille sur le temps d'exécution.
- Proposer un jeu de tests pour une fonction donnée et placer des assertions pertinentes.
- Donner un variant pour une boucle
whileet expliquer ce qu'il prouve.
Méthodes et exercices
Les résultats à connaître
Le programme cite ces algorithmes comme exemples d'activités. Il ne les impose pas, mais tout sujet suppose qu'on sache les réécrire, les compter et les justifier.
- Sommes usuelles. On a 1 + 2 + … + n = n(n + 1)/2. Il y a n(n − 1)/2 paires i < j et n(n − 1)(n − 2)/6 triplets i < j < k. Calculs exigibles.
- Coût d'une boucle. C'est la somme des coûts de chaque tour, exprimés en fonction de l'indice extérieur. Méthode exigible.
- Recherche naïve d'un facteur. Pour m de longueur p et t de longueur n, on fait au plus (n − p + 1) × p comparaisons, borne atteinte par un exemple explicite. À savoir réécrire.
- Deux valeurs les plus proches par examen des paires. Pour au moins deux éléments, on calcule exactement n(n − 1)/2 écarts. À savoir réécrire.
- Lemme des voisins. Dans une liste triée, l'écart minimal est atteint par deux cases consécutives. À savoir démontrer.
- Tri à bulles. Il fait exactement n(n − 1)/2 comparaisons. Son nombre d'échanges égale le nombre d'inversions initial (exercice 9). À savoir réécrire.
- Tri à bulles avec arrêt anticipé. Coût linéaire sur une liste triée, quadratique dans le pire cas.
- Complexité quadratique. Coût dans le pire cas de l'ordre de n², où n mesure la taille de l'entrée. Seul le pire cas est au programme.
- Taille de l'entrée. Pour une matrice n × n, la taille est n². Un coût en n² y est linéaire en la taille.
- Faits de langage. range(a, b) est vide si a ≥ b. Réaffecter l'indice d'une boucle for est sans effet sur le tour suivant. Un return quitte toutes les boucles de la fonction. [[0] * n] * n crée n références vers la même ligne.
- Hors programme. Les algorithmes linéaires de recherche de motif et le coût moyen.
Clés de démonstration
Nombre de paires. Pour i fixé, for j in range(i + 1, n) fait n − 1 − i tours. La somme de i = 0 à n − 1 vaut n(n − 1)/2. Astuce : chaque tour correspond à une paire {i, j}, vue une seule fois.
Pire cas de la recherche de facteur. La structure des boucles donne la majoration. Pour conclure au coût quadratique, il faut une entrée qui atteint la borne. Astuce : un texte de a et un mot de a terminé par b obligent à lire tout le mot à chaque position.
Correction de la recherche de facteur. Invariant du for : au début du tour i, m n'apparaît à aucune position inférieure à i. Invariant du while : t[i + j] == m[j] pour tout j < k. Astuce : en sortie, k == p signifie que tout le mot concorde.
Lemme des voisins. Soit t trié et i < j. Alors t[j] ≥ t[i + 1], donc t[j] − t[i] ≥ t[i + 1] − t[i]. Astuce : on minore l'écart de toute paire par celui d'une paire consécutive, sans valeur absolue.
Tri à bulles avec arrêt. Un parcours de t[0], …, t[f] sans échange prouve t[j] ≤ t[j + 1] pour tout j < f, donc la zone est triée. Avec l'invariant « la fin contient les plus grands éléments, triés », toute la liste l'est. Astuce : l'absence d'échange renseigne sur toutes les paires voisines à la fois.
Échanges et inversions. Échanger deux voisins t[j] > t[j + 1] ne change l'ordre relatif que de ces deux éléments. Le nombre d'inversions baisse donc d'exactement 1. Astuce : c'est un variant, qui finit à 0 pour une liste triée.
Exemples et contre-exemples à connaître
- Texte « aaaaaa » et mot « aab ». Chaque position coûte 3 comparaisons, c'est le modèle du pire cas.
- Le mot vide est facteur aux n + 1 positions de range(n + 1). Un compteur d'occurrences doit fixer cette convention.
- Dans la recherche de doublons,
range(n)au lieu derange(i + 1, n)compare t[i] à lui-même, et la fonction répond toujours True. m = [[0] * 3] * 3puism[0][0] = 1modifie les trois lignes. On crée les lignes une par une avec append.- Une boucle de n tours contenant une boucle de 5 tours fait 5n tours. Imbriquer ne rend pas forcément quadratique.
for i in range(n)contenant un while qui double j jusqu'à n coûte de l'ordre de n log n.- Avec >= au lieu de >, le tri à bulles n'est plus stable. Dans une version avec drapeau qui ne raccourcit pas la zone, [1, 1] fait même boucler indéfiniment.
- Tester la symétrie d'une matrice n × n est quadratique en n mais linéaire en le nombre de coefficients.
- Une liste strictement décroissante a n(n − 1)/2 inversions, et le tri à bulles échange à chaque comparaison.
Techniques du chapitre
Technique 1 : nommer les indices avant d'écrire. Mini-exercice : tester si deux listes a et b ont un élément commun. i parcourt a, j parcourt b, les boucles sont indépendantes. On renvoie True dès que a[i] == b[j], et False après les deux boucles. Coût au pire : len(a) × len(b).
Technique 2 : compter les tours intérieurs, puis sommer. Mini-exercice : for i in range(n) contenant for j in range(2 * i). L'intérieure fait 2i tours, le total vaut 2 × (0 + … + (n − 1)) = n(n − 1).
Technique 3 : majorer, puis exhiber un pire cas. Mini-exercice : la recherche de doublons fait au plus n(n − 1)/2 tests. Une liste d'éléments distincts ne déclenche aucun return et atteint la borne.
Technique 4 : sortir de deux boucles avec return. Mini-exercice : renvoyer la première paire i < j avec t[i] + t[j] == 0. Dans une fonction, return (i, j) quitte tout, et return None final traite l'absence.
Technique 5 : mettre la réponse partielle dans l'invariant extérieur. Mini-exercice : pour les doublons, au début du tour i, aucune paire a < b avec a < i ne vérifie t[a] == t[b]. En fin de boucle, toutes les paires sont couvertes.
Technique 6 : repérer les boucles cachées. Mini-exercice : for i in range(n) contenant if t[i] in t[:i]. La tranche et le test in coûtent chacun de l'ordre de i, le total est de l'ordre de n².
Technique 7 : casser le quadratique par un précalcul. Mini-exercice : calculer toutes les sommes t[0] + … + t[k]. On ajoute t[k] à une variable s à chaque tour au lieu de tout recalculer, et l'on passe de n² à un coût linéaire.
Exercices classiques corrigés
Choisis ton niveau. Difficulté indicative ; les exercices qui se suivent restent dans l'ordre.
Exercice 1. Compter des tours
Classiques de khôlle et concours · Khôlle
Donner les valeurs finales de c et d en fonction de n.
c = 0
for i in range(n):
for j in range(i, n):
c = c + 1
d = 0
for i in range(n):
for j in range(i):
for k in range(j):
d = d + 1
Technique sommer les tours intérieurs, ou compter des objets.
Voir le corrigé
- Pour i fixé, j prend n − i valeurs. Donc c = n + (n − 1) + … + 1 = n(n + 1)/2.
- Chaque tour du second programme correspond à un unique triplet k < j < i entre 0 et n − 1. Donc d = n(n − 1)(n − 2)/6, un coût cubique.
- Vérification pour n = 3 : c = 6, et d = 1, pour le seul triplet (2, 1, 0).
piège : Confondre range(i, n) et range(i + 1, n). L'écart de n tours ne change pas l'ordre de grandeur mais fausse la valeur exacte.
Exercice 2. Présence d'un doublon
Classiques de khôlle et concours · Khôlle
Écrire une fonction qui indique si deux cases d'indices distincts d'une liste ont même valeur. Donner son coût dans le pire cas.
Technique examen des paires avec sortie anticipée.
Voir le corrigé
- On compare chaque case à celles qui la suivent.
def a_doublon(t):
n = len(t)
for i in range(n):
for j in range(i + 1, n):
if t[i] == t[j]:
return True
return False
- L'invariant de la technique 5, en fin de boucle, justifie le return False.
- Au plus n(n − 1)/2 comparaisons, borne atteinte pour des éléments distincts. Le coût est quadratique.
- Trier d'abord, puis comparer les voisins, fait mieux avec un tri efficace.
piège : Indenter return False dans la boucle extérieure. On conclut après les seules paires commençant par la case 0.
Exercice 3. Matrice symétrique
Classiques de khôlle et concours · Khôlle
Une matrice carrée est une liste de n lignes de longueur n. Tester si elle est symétrique avec le moins de comparaisons possible.
Technique ne parcourir que le triangle strictement inférieur.
Voir le corrigé
- La diagonale est inutile, et comparer m[i][j] à m[j][i] rend inutile la comparaison inverse. On se limite à j < i.
def est_symetrique(m):
n = len(m)
for i in range(n):
for j in range(i):
if m[i][j] != m[j][i]:
return False
return True
- Au pire, 0 + 1 + … + (n − 1) = n(n − 1)/2 comparaisons.
- C'est quadratique en n, mais linéaire en la taille de l'entrée, qui compte n² coefficients.
piège : Écrire range(n) pour j. Le résultat reste juste, mais on double le travail.
Exercice 4. Nombre d'occurrences d'un facteur
Classiques de khôlle et concours · Khôlle
Compter les positions où le mot m apparaît dans le texte t, chevauchements compris. Par exemple, « aa » apparaît 3 fois dans « aaaa ».
Technique recherche naïve sans arrêt.
Voir le corrigé
- On garde « position de départ, puis lecture », et un compteur remplace le return.
def nb_occurrences(m, t):
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
- Pour le mot vide, la fonction renvoie n + 1, ce qui est cohérent et à signaler.
- Le coût au pire reste au plus (n − p + 1) × p.
piège : Sauter p positions après une occurrence. On compte alors les occurrences disjointes, 2 au lieu de 3. De plus, i = i + p dans un for n'a aucun effet.
Exercice 5. Tri à bulles avec arrêt anticipé
Classiques de khôlle et concours · D'après un écrit CCINP MP
Écrire un tri à bulles qui s'arrête dès qu'un parcours ne fait aucun échange. Prouver qu'il termine et trie. Donner son coût sur une liste triée et dans le pire cas.
Technique drapeau booléen et variant.
Voir le corrigé
- fin est l'indice de la dernière comparaison du parcours, et la zone raccourcit d'une case à chaque parcours.
def tri_bulles_arret(t):
fin = len(t) - 1
echange = True
while echange:
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
- Terminaison : fin diminue de 1 à chaque tour. Dès que fin ≤ 0, range(fin) est vide, echange reste False et la boucle s'arrête. Il y a au plus len(t) tours.
- Correction : après chaque tour, les cases situées après la zone parcourue contiennent les plus grands éléments, triés. Au dernier tour, aucun échange n'a eu lieu, donc la zone parcourue est triée aussi.
- Coût : n − 1 comparaisons sur une liste triée. Sur une liste décroissante, chaque parcours échange et l'on fait n(n − 1)/2 comparaisons.
piège : Initialiser echange à False avant le while. La boucle ne s'exécute jamais.
Exercice 6. Tri par sélection
Classiques de khôlle et concours · D'après un écrit CCINP MP
Pour chaque i, placer en case i le plus petit élément de t[i], …, t[n − 1]. Compter comparaisons et échanges. Ce tri est-il stable ?
Technique recherche d'un minimum dans une boucle.
Voir le corrigé
- k retient l'indice du minimum de la zone restante, puis on échange t[i] et t[k].
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]
- Invariant : après le tour i, les cases 0 à i contiennent les i + 1 plus petits éléments, triés. La dernière case contient alors forcément le plus grand.
- On fait toujours n(n − 1)/2 comparaisons, mais au plus n − 1 échanges, contre n(n − 1)/2 pour le tri à bulles.
- Il n'est pas stable. Pour [2a, 2b, 1], le premier échange donne [1, 2b, 2a].
piège : Échanger dans la boucle j à chaque nouveau minimum. On perd l'avantage d'un seul échange par tour.
Exercice 7. Plus proches dans une liste triée
Classiques de khôlle et concours · D'après un écrit CCINP MP
Soit t triée par ordre croissant, de longueur au moins 2. Écrire une fonction de coût linéaire renvoyant un couple d'indices réalisant l'écart minimal. Discuter le cas d'une liste non triée.
Technique lemme des voisins.
Voir le corrigé
- D'après le lemme, il suffit de comparer les écarts t[i + 1] − t[i], tous positifs.
def plus_proches_trie(t):
k = 0
for i in range(1, len(t) - 1):
if t[i + 1] - t[i] < t[k + 1] - t[k]:
k = i
return (k, k + 1)
- On fait n − 2 comparaisons, donc un coût linéaire.
- Pour une liste quelconque, on trie d'abord. Avec le tri à bulles, le total reste quadratique. Il faut un tri efficace, étudié plus tard.
- Les indices renvoyés sont ceux de la liste triée. Pour les positions d'origine, on trie des couples (valeur, indice).
piège : Faire aller i jusqu'à len(t) − 1 inclus. t[i + 1] sort alors de la liste.
Exercice 8. Classer des coûts
Classiques de khôlle et concours · D'après un écrit CCINP MP
Donner le coût dans le pire cas de chaque fonction, pour une liste de longueur n ≥ 1.
def f1(t):
c = 0
for i in range(len(t)):
if t[i] in t[:i]:
c = c + 1
return c
def f2(t):
c = 0
for i in range(len(t)):
j = 1
while j < len(t):
j = 2 * j
c = c + 1
return c
Technique coût réel de chaque tour.
Voir le corrigé
- f1 : au tour i, la tranche et le test in coûtent chacun de l'ordre de i. Le total est de l'ordre de n², donc quadratique.
- f2 : après r tours du while, j vaut 2 puissance r. La boucle s'arrête au plus petit r tel que 2 puissance r ≥ n, soit ⌈log₂ n⌉ tours.
- f2 fait donc n × ⌈log₂ n⌉ tours, de l'ordre de n log n, pas quadratique.
- Contrôle pour n = 3 : j passe de 1 à 2 puis 4, soit 2 tours, et ⌈log₂ 3⌉ = 2.
piège : Juger f2 quadratique sur sa forme, ou f1 linéaire parce qu'elle n'a qu'une boucle visible.
Exercice 9. Inversions et tri à bulles
Approfondissement · D'après un écrit Mines-Ponts
Une inversion de t est une paire i < j avec t[i] > t[j]. Écrire une fonction qui les compte. Montrer que le tri à bulles fait autant d'échanges que la liste initiale a d'inversions. En déduire le nombre maximal d'échanges.
Technique examen des paires, puis variant.
Voir le corrigé
- On reprend la double boucle sur les paires.
def inversions(t):
n = len(t)
c = 0
for i in range(n):
for j in range(i + 1, n):
if t[i] > t[j]:
c = c + 1
return c
- Un échange de t[j] > t[j + 1] ne modifie l'ordre relatif d'aucune autre paire. Seule leur inversion disparaît, donc le nombre d'inversions baisse d'exactement 1.
- Une comparaison sans échange ne change rien. La liste finale est triée, donc sans inversion. Le nombre d'échanges égale donc le nombre initial d'inversions.
- Il est au plus n(n − 1)/2, atteint pour une liste strictement décroissante.
piège : Ne pas justifier que les autres paires gardent leur ordre. C'est parce que les deux cases sont voisines qu'aucune autre ne passe de l'autre côté.
Exercice 10. Somme maximale d'une tranche
Approfondissement · Type oral Centrale
Pour une liste non vide, trouver la plus grande somme t[i] + … + t[j] avec i ≤ j. Donner le coût de la méthode qui recalcule chaque somme, puis écrire une méthode quadratique.
Technique précalcul incrémental dans la boucle intérieure.
Voir le corrigé
- Recalculer chaque somme de j − i + 1 termes coûte de l'ordre de n³/6, un coût cubique.
- Pour i fixé, la somme jusqu'à t[j + 1] s'obtient en ajoutant t[j + 1] à la précédente.
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
- Invariant intérieur : après le tour j, s = t[i] + … + t[j]. Invariant extérieur : au début du tour i, meilleur est le maximum de t[0] et des sommes des tranches commençant avant i.
- On fait n(n + 1)/2 additions, un coût quadratique. Une méthode linéaire existe, hors du cadre de ce chapitre.
piège : Initialiser meilleur à 0. Pour [−3, −1], on renverrait 0 au lieu de −1.
Erreurs fréquentes
- Des bornes fausses. range(n) au lieu de range(n − p + 1) sort du texte, et range(n) au lieu de range(i + 1, n) compare une case à elle-même. On vérifie sur le plus petit cas utile.
- Juger le coût sur la forme du code. Deux boucles imbriquées peuvent coûter 5n ou n log n, et une seule boucle peut coûter n². Il faut sommer le coût réel de chaque tour.
- Annoncer un coût quadratique sans entrée qui l'atteint, ou sans dire ce que mesure n. Une majoration seule ne suffit pas.
- Un return mal indenté. Dans la boucle extérieure, il conclut trop tôt, et un return oublié renvoie None.
- Initialiser un meilleur candidat à 0, ou créer une matrice par [[0] * n] * n. Le premier défaut fausse les cas négatifs, le second fait partager la même ligne à toutes les lignes.
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 programme cite comme activités la recherche d'un facteur dans un texte, la recherche des deux valeurs les plus proches dans un tableau et le tri à bulles, et demande de dégager la notion de complexité quadratique et de proposer des outils pour valider la correction des algorithmes. Ces exemples d'activités sont au choix du professeur et seuls les éléments de l'annexe « Langage Python » sont exigibles. On ne présente que l'estimation asymptotique du coût dans le cas le pire.