Boucles imbriquées et complexité quadratique
Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 3 · prépas scientifiques, tronc commun
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>3.1 Introduction et motivation
Quand une question porte non plus sur les éléments d'un tableau mais sur ses couples — quelles sont les deux mesures les plus proches ? ce motif apparaît-il dans ce texte ? ces éléments sont-ils dans le bon ordre ? — un seul parcours ne suffit plus : il faut une boucle dans la boucle. Cette structure, la plus naturelle qui soit, a un prix : le nombre d'opérations ne croît plus comme mais comme . Doubler l'entrée quadruple le temps ; la multiplier par mille le multiplie par un million.
Ce chapitre apprend à manier les boucles imbriquées sur trois problèmes classiques — la paire la plus proche, la recherche d'un facteur dans un texte, le tri à bulles — et, à chaque fois, à faire les deux gestes du métier : estimer le coût (en comptant ce que l'on exécute vraiment) et valider la correction (par les invariants du chapitre 1, qui deviennent ici indispensables : avec deux indices qui bougent, l'intuition seule ne suffit plus).
3.2 La complexité quadratique
3.2.1 Compter les tours d'une double boucle
for i in range(n):
for j in range(n):
... # corps en O(1)
Le corps s'exécute fois : coût . Variante essentielle, le triangle — quand ne parcourt que les indices après :
for i in range(n):
for j in range(i + 1, n):
... # corps en O(1)
Le nombre de tours est : la somme des premiers entiers, vue en cours de mathématiques. C'est moitié moins que , mais c'est toujours — la constante disparaît dans le .
Un algorithme est de complexité quadratique si son coût dans le cas le pire est — typiquement, un travail à coût constant effectué pour chaque couple d'indices. Ordres de grandeur, à opérations par seconde :
| ms | ms | ||
| ms | s | heures |
Un algorithme quadratique est parfaitement honorable jusqu'à , pénible vers , et inutilisable au-delà : savoir situer son entrée sur cette échelle est le premier réflexe de l'analyse.
Toutes les doubles boucles ne sont pas quadratiques, et tous les algorithmes quadratiques n'ont pas deux boucles visibles. Le coût se calcule en comptant les exécutions du corps, pas en comptant les lignes for : une boucle interne de longueur constante ( fixé) donne du ; une boucle simple dont le corps contient v in t (chapitre 2) donne du .
3.3 Tous les couples : la paire la plus proche
On cherche deux indices minimisant — par exemple les deux mesures les plus concordantes d'une série d'expériences. L'algorithme naïf examine tous les couples :
def paire_la_plus_proche(t: list) -> tuple:
"""Renvoie (i, j), i < j, minimisant |t[i] - t[j]|.
Précondition : len(t) >= 2."""
mi, mj = 0, 1
for i in range(len(t)):
for j in range(i + 1, len(t)):
# Invariant : (mi, mj) est le meilleur couple parmi ceux
# déjà examinés (tous les couples qui précèdent (i, j))
if abs(t[i] - t[j]) < abs(t[mi] - t[mj]):
mi, mj = i, j
return (mi, mj)
assert paire_la_plus_proche([10, 3, 8, 1]) == (0, 2) # |10 - 8| = 2
assert paire_la_plus_proche([5, 5]) == (0, 1) # écart nul
C'est le « maximum » du chapitre 2 transposé aux couples : un champion courant, mis à jour à chaque candidat. Coût : comparaisons, .
Méthode : Énumérer les couples sans doublon ni diagonale
Pour visiter chaque paire une seule fois, la borne interne commence à :
for j in range(i + 1, n): les couples — paires non ordonnées, sans la diagonale ; c'est le réglage voulu dans la quasi-totalité des cas ;for j in range(n): tous les couples ordonnés, y compris et chaque paire vue deux fois — rarement ce qu'on veut, source de faux résultats ( gagnerait toujours ci-dessus !).
Relire les bornes d'une double boucle en se demandant « diagonale incluse ? chaque paire vue combien de fois ? » évite la moitié des bogues du chapitre.
Pour ce problème précis, on peut faire mieux que : trier d'abord le tableau (chapitre 9), car les deux valeurs les plus proches sont alors voisines — coût . Retenir le schéma : l'algorithme naïf d'abord, correct et prouvé ; l'optimisation ensuite, quand la taille l'exige.
3.4 Recherche d'un facteur dans un texte
Un mot (le motif) est un facteur d'un texte s'il apparaît dans comme une suite de caractères consécutifs : "ana" est un facteur de "banane" (aux positions et ), pas de "avance".
À chaque position de départ possible dans le texte, on compare le motif caractère par caractère :
def cherche_facteur(m: str, s: str):
"""Renvoie la plus petite position i telle que s[i:i+len(m)] == m,
ou None si m n'est pas un facteur de s."""
for i in range(len(s) - len(m) + 1):
# Invariant : m n'apparaît à aucune position < i
j = 0
while j < len(m) and s[i + j] == m[j]:
j += 1
if j == len(m): # tout le motif a coïncidé
return i
return None
assert cherche_facteur("ana", "banane") == 1
assert cherche_facteur("anas", "banane") is None
assert cherche_facteur("", "abc") == 0 # le mot vide est facteur partout
La borne externe mérite un instant : la dernière position de départ possible est , d'où le len(s) - len(m) + 1 — un motif qui « dépasserait » du texte ne peut pas coïncider.
Complexité : Recherche naïve de facteur
Notons et . Pour chaque position de départ (au plus ), la boucle interne effectue au plus comparaisons : coût . Le pire cas est atteint sur des entrées répétitives — chercher dans : chaque position lit caractères avant d'échouer sur le dernier. En pratique, sur du texte ordinaire, la boucle interne échoue presque toujours dès le premier caractère et le comportement observé est proche du linéaire : le pire cas est une garantie, pas une prédiction.
L'opérateur Python m in s sur les chaînes fait exactement ce travail (en mieux optimisé) : on a le droit de l'utiliser — mais l'écrire soi-même une fois est obligatoire, car c'est l'archétype de la double boucle « positions vérification », que l'on retrouvera tel quel dans les images (chapitre 8).
3.5 Le tri à bulles
3.5.1 L'algorithme
Le tri à bulles trie un tableau en place en répétant un geste unique : comparer deux voisins, les échanger s'ils sont dans le mauvais ordre. À chaque passe, le plus grand élément non encore classé « remonte » comme une bulle jusqu'à sa place définitive :
def tri_bulles(t: list) -> None:
"""Trie t en place, en ordre croissant."""
n = len(t)
for k in range(n - 1):
# Invariant : t[n-k..n-1] contient les k plus grands éléments,
# à leur place définitive et triés
for i in range(n - 1 - k):
if t[i] > t[i + 1]:
t[i], t[i + 1] = t[i + 1], t[i]
Noter le contrat inhabituel : la fonction renvoie None et modifie son argument — c'est le sens de « tri en place », et la docstring doit le dire (chapitre 1 : la mutation fait partie du contrat).
Démonstration (Correction)
Lemme (la bulle remonte). Après une passe complète de la boucle interne sur , le maximum de se trouve en position . En effet, considérons la première position où se trouve ce maximum : dès que atteint , le test échange (ou laisse) le maximum en , et il sera entraîné par tous les échanges suivants — un mini-invariant de la boucle interne : « après le tour , est le maximum de ».
Invariant de la boucle externe. : « contient les plus grands éléments du tableau, triés et à leur place définitive ». Initialisation : , suffixe vide. ✓ Conservation : la passe travaille sur et, par le lemme, amène son maximum en position ; ce maximum est inférieur ou égal à tous les éléments du suffixe déjà classé, donc le suffixe trié s'étend d'une case. ✓ Après passes, est trié et contient les plus grands : , le minimum, est aussi à sa place — le tableau est trié. La terminaison est immédiate (deux boucles for).
Complexité : Tri à bulles
La passe effectue comparaisons : au total
quel que soit le tableau — même déjà trié. Le nombre d'échanges, lui, dépend de l'entrée : nul sur un tableau trié, maximal () sur un tableau strictement décroissant.
3.5.2 L'arrêt anticipé
Si une passe entière ne fait aucun échange, tous les voisins sont en ordre : le tableau est trié, inutile de continuer.
def tri_bulles_ameliore(t: list) -> None:
n = len(t)
k = 0
echange = True
while echange: # variant : n - 1 - k (k croît strictement)
echange = False
for i in range(n - 1 - k):
if t[i] > t[i + 1]:
t[i], t[i + 1] = t[i + 1], t[i]
echange = True
k += 1
Sur un tableau déjà trié, une seule passe ( comparaisons) suffit : le meilleur cas devient linéaire. Le pire cas reste : la garantie ne change pas, mais le comportement sur les entrées favorables s'améliore — distinction à toujours formuler.
Pourquoi prouver si soigneusement un tri médiocre ? Parce que le tri à bulles est le terrain d'entraînement idéal : deux indices, un invariant à deux étages, un raisonnement de type « le suffixe est définitivement classé » que l'on retrouvera dans les tris sérieux du chapitre 9 (sélection, insertion) et qu'aucune intuition ne remplace. Le programme dit : on propose des outils pour valider la correction — c'est ici qu'on apprend à s'en servir.
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>3.6 Exercices résolus
Niveau (Application directe du cours)
Pour chaque fragment, donner le nombre exact d'exécutions du corps, puis le coût asymptotique :
# (a) # (b) # (c)
for i in range(n): for i in range(n): for i in range(n):
for j in range(10): for j in range(i): for j in range(i, n):
... ... ...
Démonstration (Solution)
(a) exécutions : la boucle interne est de longueur constante — coût , linéaire malgré les deux for.
(b) : le triangle strict — .
(c) : le triangle avec diagonale — aussi. (Les trois réponses tiennent en une règle : sommer la longueur de la boucle interne sur toutes les valeurs de — l'asymptotique tombe ensuite toute seule.)
Écrire tous_distincts(t) par double boucle, prouver sa correction, donner son coût — puis le comparer à la version dictionnaire du chapitre 2.
Démonstration (Solution)
def tous_distincts(t: list) -> bool:
for i in range(len(t)):
for j in range(i + 1, len(t)):
# Invariant : aucun couple examiné avant (i, j) n'est un doublon
if t[i] == t[j]:
return False
return True
Correction : si la fonction renvoie False, elle vient d'exhiber avec : il y a bien un doublon. Si elle renvoie True, les deux boucles ont épuisé tous les couples sans en trouver : l'invariant final dit qu'aucun doublon n'existe. La borne range(i + 1, ...) est essentielle : avec range(len(t)), le couple donnerait et la fonction répondrait toujours False.
Coût : comparaisons au pire (tableau sans doublon) : . La version dictionnaire (a_un_doublon, chapitre 2) fait le même travail en — mais exige des éléments utilisables comme clés ; la double boucle, elle, ne demande que l'égalité. (Quand les deux outils s'appliquent, le dictionnaire gagne ; savoir écrire et prouver la double boucle reste le socle.)
Adapter cherche_facteur en positions_facteur(m, s) qui renvoie la liste de toutes les positions où apparaît dans — y compris les occurrences qui se chevauchent. Vérifier sur positions_facteur("aa", "aaaa").
Démonstration (Solution)
Il suffit de ne plus s'arrêter à la première trouvaille :
def positions_facteur(m: str, s: str) -> list:
pos = []
for i in range(len(s) - len(m) + 1):
j = 0
while j < len(m) and s[i + j] == m[j]:
j += 1
if j == len(m):
pos.append(i)
return pos
assert positions_facteur("aa", "aaaa") == [0, 1, 2] # chevauchements comptés !
assert positions_facteur("ana", "banane") == [1, 3]
assert positions_facteur("x", "banane") == []
Le test "aaaa" illustre le choix de contrat : les occurrences aux positions , , se chevauchent, et notre spécification les compte toutes — l'autre convention (reprendre la recherche après l'occurrence trouvée, donnant ) existe aussi ; l'énoncé doit trancher. Coût inchangé : . (Transformer un « premier trouvé » en « tous trouvés » : remplacer le return par un append — un geste de refactorisation à connaître.)
Niveau (Application avec raisonnement intermédiaire)
Dérouler à la main tri_bulles sur : donner l'état du tableau après chaque passe, vérifier l'invariant, et compter comparaisons et échanges.
Démonstration (Solution)
Passe (sur , comparaisons) : , échange ; , échange ; , échange . Invariant : , le plus grand, en place. ✓
Passe ( comparaisons) : , rien ; , échange . Invariant : , les deux plus grands, triés. ✓
Passe ( comparaison) : , rien. Invariant : . ✓ — et est le minimum : trié.
Total : comparaisons (c'est , conforme), échanges. (Dérouler un algorithme à la main sur un petit exemple en vérifiant l'invariant à chaque étape : le meilleur test qui soit avant même d'allumer la machine.)
Justifier soigneusement l'arrêt anticipé du tri à bulles : montrer que si une passe complète n'effectue aucun échange, alors le tableau est trié — et exhiber le variant de la boucle while de tri_bulles_ameliore.
Démonstration (Solution)
Aucun échange trié. Si la passe n'a rien échangé, c'est que chaque test a constaté , pour tout parcouru. Or « chaque voisin en ordre » entraîne « tout en ordre » : pour , par transitivité — le tableau est croissant.
Variant. La quantité convient : tant que la boucle tourne, le tableau n'est pas encore certifié trié et (après passes, l'invariant du cours garantit le tri, donc une passe sans échange, donc l'arrêt) ; et augmente de à chaque tour, donc décroît strictement. La boucle while termine en au plus passes. (Le drapeau echange transforme une propriété mathématique — « plus rien à faire » — en condition d'arrêt observable : c'est tout l'art des boucles while bien construites.)
Écrire meilleure_tranche(t) qui renvoie la somme maximale d'une tranche non vide (), en — sans recalculer chaque somme de zéro. Exemple : (la tranche ).
Démonstration (Solution)
L'astuce anti-cube : pour fixé, la somme de se déduit de celle de en ajoutant — la somme s'accumule le long de la boucle interne :
def meilleure_tranche(t: list) -> float:
"""Précondition : t non vide."""
meilleur = t[0]
for i in range(len(t)):
s = 0
for j in range(i, len(t)):
s += t[j] # Invariant interne : s = somme de t[i..j]
if s > meilleur:
meilleur = s
return meilleur
assert meilleure_tranche([2, -8, 3, -2, 4, -10]) == 5
assert meilleure_tranche([-3, -1, -7]) == -1 # tout négatif : la moins mauvaise case
La version naïve à trois étages — pour chaque couple , recalculer la somme par une troisième boucle — coûterait ; l'accumulation la ramène à . Le second test garde la spécification honnête : « tranche non vide » signifie qu'un tableau tout négatif rend son plus grand élément, pas . (Il existe un algorithme linéaire — Kadane — hors programme : la version quadratique propre est le livrable attendu.)
Combiner les chapitres 1 et 3 : tester tri_bulles par propriété sur mille tableaux aléatoires, en vérifiant les deux clauses du contrat (résultat croissant, mêmes éléments) — et expliquer pourquoi la seconde clause attraperait une faute d'échange typique.
Démonstration (Solution)
import random
for essai in range(1000):
t = [random.randint(-20, 20) for _ in range(random.randint(0, 15))]
copie = list(t) # référence intacte (tri en place !)
tri_bulles(t)
assert all(t[i] <= t[i + 1] for i in range(len(t) - 1)), f"non trie : {t}"
assert sorted(copie) == t, f"elements modifies : {copie} -> {t}"
La faute d'échange typique est t[i] = t[i + 1] puis t[i + 1] = t[i] (sans variable temporaire ni affectation simultanée) : la première écriture détruit , et le tableau final contient un doublon venu de nulle part — il peut être parfaitement croissant, donc la première assertion passe, mais sorted(copie) == t échoue. La forme assert condition, message (hors annexe du programme, fournie avec sa documentation) affiche le tableau fautif dès qu'un essai échoue. Noter aussi la copie avant l'appel : le tri étant en place, sans elle on comparerait le résultat à lui-même. (Tester une fonction mutante exige de photographier l'entrée d'abord — l'oubli classique qui rend un test toujours vert.)
Niveau (Raisonnement subtil ou plusieurs étapes)
Une inversion de est un couple avec . Écrire nb_inversions(t) en , puis démontrer : (a) le nombre d'échanges du tri à bulles est exactement le nombre d'inversions du tableau initial ; (b) le tableau est trié si et seulement s'il n'a aucune inversion.
Démonstration (Solution)
def nb_inversions(t: list) -> int:
c = 0
for i in range(len(t)):
for j in range(i + 1, len(t)):
if t[i] > t[j]:
c += 1
return c
(b) d'abord : « trié » signifie pour tous , c'est-à-dire zéro inversion — équivalence par définition.
(a) Chaque échange du tri à bulles porte sur deux voisins mal ordonnés : il corrige l'inversion , et ne modifie l'ordre relatif d'aucun autre couple (tout autre élément reste du même côté des deux échangés, ou des deux à la fois). Le nombre d'inversions diminue donc d'exactement par échange. À la fin, il vaut par (b) : le nombre total d'échanges est le nombre initial d'inversions. (Joli sous-produit : le nombre d'inversions est un variant naturel du tri à bulles — il prouve à lui seul que l'algorithme avec drapeau termine, et mesure le « désordre » d'un tableau ; le tri à bulles est lent parce qu'il ne détruit qu'une inversion par échange, quand un tableau peut en contenir .)
Pour la recherche naïve de facteur, construire une famille d'entrées réalisant asymptotiquement le pire cas — c'est-à-dire forçant environ comparaisons — et calculer le nombre exact de comparaisons effectuées.
Démonstration (Solution)
Prenons (la lettre a répétée fois) et (des a, puis un b final). À chaque position de départ , la boucle interne lit coïncidences (a contre a) puis une discordance sur le b : exactement comparaisons, et l'échec n'arrive qu'à la toute fin. Nombre total :
comparaisons — pour , cela fait : le comportement quadratique est atteint, pas seulement majoré. (Construire l'entrée adverse est l'autre moitié de l'analyse : la borne majore, l'exemple méchant prouve qu'on ne peut pas annoncer mieux. Les algorithmes de recherche rapide — Boyer-Moore, Knuth-Morris-Pratt, hors programme — sont nés précisément de ces entrées répétitives : ils mémorisent ce que la version naïve oublie, à savoir que les caractères déjà lus sont connus.)
Démontrer que dans le tri à bulles, un élément ne se déplace vers la gauche que d'une position par passe. En déduire que si le minimum du tableau se trouve initialement en dernière position, l'algorithme (même avec drapeau) effectue ses passes — le pire cas structurel — et proposer le remède du tri shaker.
Démonstration (Solution)
Petits pas vers la gauche. Pendant une passe, l'indice avance de gauche à droite. Un élément ne se déplace vers la gauche que s'il est le plus petit des deux comparés, c'est-à-dire lors de l'unique comparaison où il est en position : il recule alors d'une case, puis l'indice le dépasse et ne le touchera plus de la passe. Un recul par passe, au maximum. (Vers la droite, en revanche, un grand élément peut être entraîné sur toute la passe — c'est la « bulle ».)
Conséquence. Si le minimum est en position , il doit reculer de cases pour atteindre sa place : il faut passes, et le drapeau ne s'éteint jamais avant — sur , un tableau « presque trié » (une seule valeur mal placée !), le tri à bulles est aussi lent que sur un tableau quelconque : comparaisons.
Le tri shaker alterne le sens des passes : une passe gauche-droite (les grands montent), une passe droite-gauche (les petits descendent — et un petit élément peut alors traverser tout le tableau en une passe). Sur l'exemple ci-dessus, deux passes suffisent. Le pire cas général reste , mais l'asymétrie pathologique disparaît. (Analyser un algorithme, c'est aussi comprendre la forme de ses mauvaises entrées — ici, l'asymétrie gauche-droite du mouvement — car c'est elle qui suggère le remède.)
- Compter les tours : le coût d'une double boucle se calcule en sommant la longueur de la boucle interne — carré plein , triangle , tous deux ; boucle interne de longueur constante : — on compte les exécutions du corps, pas les lignes
for. - Quadratique en pratique : confortable jusqu'à , inutilisable au-delà de – ; toujours situer la taille de l'entrée sur l'échelle.
- Énumérer les couples :
for j in range(i + 1, n)pour chaque paire une fois, sans diagonale — relire les bornes en se demandant « diagonale ? doublons ? » ; champion courant pour les problèmes de meilleur couple (paire la plus proche). - Facteur dans un texte : positions de départ vérification caractère par caractère ; borne externe ; coût , pire cas atteint sur les entrées répétitives ( contre ) ; en pratique souvent quasi linéaire — la garantie n'est pas une prédiction.
- Tri à bulles : échanges de voisins ; invariant externe « le suffixe contient les plus grands, triés, en place » + lemme « la bulle remonte » ; comparaisons quoi qu'il arrive ; drapeau d'arrêt anticipé (aucun échange trié, par transitivité) : meilleur cas linéaire, pire cas inchangé.
- Inversions : couples mal ordonnés ; trié zéro inversion ; chaque échange de voisins détruit exactement une inversion — d'où la lenteur du tri à bulles et un variant naturel.
- Valider : dérouler à la main sur un petit exemple en vérifiant l'invariant ; tester par propriété avec copie préalable (fonctions mutantes !) et les deux clauses du contrat ; construire l'entrée adverse pour prouver que le pire cas est atteint.
3.7 Exercices d'entraînement
Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.
A. Compter et estimer
- () Donner le nombre exact de tours puis le coût :
for i in range(n): for j in range(n - 1 - i): ...;for i in range(0, n, 2): for j in range(n): ...; trois boucles imbriquées enrange(n). - () Un algorithme effectue opérations. Montrer qu'il est ; à partir de quel le terme quadratique dépasse-t-il les deux autres réunis ?
- ( ) Estimer le temps d'exécution de
tous_distinctspour à comparaisons par seconde, puis vérifier expérimentalement (moduletime) sur des tableaux sans doublon — et expliquer pourquoi « sans doublon » est indispensable à la mesure du pire cas. - () La boucle
for i in range(n): t = t + [i](concaténation, qui copie) est-elle linéaire ? Compter les copies effectuées, conclure ( !), et corriger avecappend.
B. Doubles boucles sur tableaux
- () Écrire
paire_de_somme_nulle(t): existe-t-il avec ? Double boucle, puis comparer à la version dictionnaire (chapitre 2, deux-sommes). - () Écrire
produit_maximal(t): le plus grand produit , — attention aux négatifs (jeu de tests imposé : ). - ( ) Écrire
plus_proches_voisines(points): la paire de points du plan la plus proche (distance euclidienne, points en couples ), en — et expliquer pourquoi comparer les distances au carré évite des appels inutiles à la racine. - () Un tableau est un palindrome s'il se lit identiquement dans les deux sens. L'écrire avec une seule boucle et deux indices convergents ; invariant et variant exigés.
- () Écrire
triplet_pythagoricien(t): existe-t-il dans avec ? Version naïve, puis à l'aide d'un dictionnaire des carrés. - () La convolution de deux listes et : . L'écrire par double boucle, vérifier sur , donner le coût — elle reviendra au chapitre 8 pour le flou des images.
C. Chaînes et facteurs
- () Écrire
nb_occurrences_facteur(m, s)(avec chevauchements) et la tester sur("aa", "aaaa"). - () Écrire
est_prefixe(m, s)etest_suffixe(m, s)sans tranches, par boucle — puis exprimercherche_facteurà l'aide deest_prefixesur les positions successives. - ( ) Le plus long préfixe commun de deux chaînes : l'écrire en , avec invariant.
- () Écrire
cherche_sous_suite(m, s): les caractères de apparaissent dans dans l'ordre mais pas nécessairement consécutifs ("bne"dans"banane"). Un seul parcours de suffit — pourquoi est-ce là où le facteur coûtait ? - () La plus longue répétition : le plus long facteur apparaissant au moins deux fois dans . Version cubique naïve, puis en comparant les suffixes deux à deux caractère par caractère.
D. Tris quadratiques et inversions
- () Dérouler
tri_bulles_amelioresur : combien de passes, de comparaisons, d'échanges ? Comparer à la version sans drapeau. - ( ) Quel tableau de longueur maximise le nombre d'échanges du tri à bulles ? Le minimiser ? Justifier par le lien échanges-inversions.
- () Modifier
tri_bullespour trier en ordre décroissant, puis pour trier des couples (nom, note) par note croissante — l'ordre relatif des ex æquo est-il préservé (stabilité, chapitre 9) ? - ( ) Prouver : un tableau de longueur a au plus inversions, avec égalité si et seulement s'il est strictement décroissant.
- () Implémenter le tri shaker (exercice résolu 10), vérifier qu'il trie en deux passes, et tester par propriété contre
sorted. - ( ) On mélange un jeu de cartes par échanges de voisins uniquement. Montrer, par l'argument des inversions, qu'au moins échanges sont nécessaires dans le pire cas pour trier — et qu'aucune astuce d'implémentation n'y changera rien : c'est une borne inférieure sur le problème, pas sur un algorithme.