NSI · Terminale

Algorithmique en Terminale NSI : arbres, graphes, tri et recherche

L'algorithmique de Terminale ne consiste pas à collectionner des recettes. Elle apprend à choisir une structure et une stratégie, à formuler l'invariant qui rend chaque étape légitime, puis à mesurer temps et mémoire. Sur un arbre binaire, les parcours préfixe, infixe, suffixe et en largeur n'ordonnent pas les nœuds de la même manière.

Explications et exemples en accès libre. Ateliers, quiz et cartes avec l’abonnement.

Étude en accès libre
Environ 55 min à 1 h 40
Progression
6 étapes guidées
Vérification
12 questions
Rappel actif
12 cartes
Prévoir mon tempsExplications et exemples en accès libre

Une première étude des explications, schémas, exemples résolus et erreurs expliquées. Les activités, productions, quiz et cartes réservés à l’abonnement ne sont pas comptés ici.

Comprendre2987 mots d’explication et 6 schémas
19 à 31 min
Étudier les exemples et les erreurs18 cas, exemples et activités guidés
36 à 66 min

Étude du cours en accès libre, environ55 min à 1 h 40

Voir le calcul et le temps par étape

Le calcul suit automatiquement les explications et les tâches présentes dans le cours. Ses coefficients sont des repères de planification, pas des temps mesurés auprès d’élèves.

  • Lecture active : 2987 mots, à raison de 160 à 220 mots par minute.
  • Schémas : 6, avec 1 à 2 min pour lire chacun.
  • Exemple guidé : 6 × 2 à 4 min.
  • Suivre le code résolu et ses tests : 6 × 3 à 5 min.
  • Comprendre une erreur expliquée : 6 × 1 à 2 min.
  1. Arbres binaires : mesurer et parcourir sans confondre les ordres9 à 16 min
  2. Arbres binaires de recherche : ordre, insertion et hauteur9 à 16 min
  3. Graphes : marquer, choisir une frontière, reconstruire un chemin9 à 17 min
  4. Diviser pour régner : prouver le tri fusion et lire n log₂ n8 à 16 min
  5. Programmation dynamique : mémoriser et reconstruire une solution9 à 17 min
  6. Recherche textuelle : prétraiter le motif pour justifier les décalages9 à 16 min

Les sous-totaux sont arrondis à la minute, puis additionnés. La fourchette totale est élargie aux cinq minutes voisines. Les étapes ci-dessus comprennent leurs explications et exemples ; elles ne s’ajoutent pas une seconde fois au total.

Adapte ce repère à tes acquis et au soin apporté aux exercices. Les pauses, les reprises, la consultation des sources externes et les révisions suivantes s’ajoutent selon tes besoins.

Avec les ateliers, le projet, le quiz et les cartes : environ 2 h 55 à 4 h 55, à répartir sur plusieurs séances.

Objectifs du cours

Ce que tu vas savoir faire

  • Calculer taille et hauteur d'un arbre binaire et exécuter ses quatre parcours au programme.
  • Rechercher et insérer une clé dans un arbre binaire de recherche en reliant le coût à sa hauteur.
  • Parcourir un graphe en largeur et en profondeur, chercher un chemin et repérer un cycle.
  • Concevoir et justifier un algorithme de type diviser pour régner avec le tri fusion.
  • Construire une table de programmation dynamique, reconstruire une solution et discuter son coût mémoire.
  • Prétraiter un motif et tracer les décalages d'une recherche de Boyer-Moore-Horspool.
  • Distinguer correction, terminaison, coût dans le pire cas et comportement mesuré sur une entrée.
01

Étape du cours · 9 à 16 min

Arbres binaires : mesurer et parcourir sans confondre les ordres

Un arbre binaire est soit vide, soit un nœud composé d’une valeur, d’un sous-arbre gauche et d’un sous-arbre droit. La représentation du cours est donc None ou le tuple (valeur, gauche, droite). Deux mesures se déduisent de cette définition. La taille vérifie taille(vide)=0 et taille(nœud)=1+taille(gauche)+taille(droite). La hauteur compte les arêtes du plus long chemin : hauteur(vide)=−1 et hauteur(nœud)=1+max(hauteur(gauche),hauteur(droite)). Une feuille a ainsi hauteur 0, sans convention cachée.

Un parcours répond à une autre question : dans quel ordre lis-tu les nœuds ? Préfixe lit racine, gauche, droite. Infixe lit gauche, racine, droite. Suffixe lit gauche, droite, racine. La largeur utilise une file : elle retire tous les nœuds d’un niveau avant le suivant. Ces quatre sorties ne sont pas quatre écritures du même résultat ; elles servent à rendre visible une décision de lecture.

Sur l’arbre A de racine 4, avec 2 et 6 comme fils, puis 1,3,5,7 comme feuilles, taille(A)=7 et hauteur(A)=2. Préfixe rend 4,2,1,3,6,5,7. Infixe rend 1,2,3,4,5,6,7. Suffixe rend 1,3,2,5,7,6,4. Largeur rend 4,2,6,1,3,5,7. Compare ces quatre listes : la structure est identique, seul le moment où une racine est produite change.

La récursion est justifiée par la forme de l’arbre. Pour une mesure ou un parcours, chaque appel non vide délègue aux deux sous-arbres, qui ont une profondeur plus petite. Les cas vides ferment les branches. Le programme n’a pas besoin de deviner où finir : la structure fournit à la fois le cas de base et la décomposition. Une trace de largeur, elle, montre la file restante après chaque retrait, ce qui explique pourquoi 2 et 6 précèdent 1 et 3.

Avant de raisonner, le laboratoire vérifie que chaque nœud est bien un tuple ternaire, que les valeurs sont des entiers stricts et qu’aucune référence cyclique ne transforme une prétendue structure en boucle. Cette validation protège les données d’entrée. Le noyau de taille, hauteur ou parcours suit ensuite directement les formules ci-dessus. Distinguer contrôle de forme et calcul rend l’erreur lisible sans cacher le mécanisme de l’arbre. Le laboratoire accepte une hauteur de −1 à 8 et des valeurs entières de −1000 à 1000 ; un même sous-arbre partagé à deux emplacements est compté deux fois, comme deux branches. Avec deque, le retrait de file ne décale pas tous les éléments. Sans les instantanés, ces parcours visitent chacun des n nœuds en O(n) ; conserver une copie de la sortie après chaque retrait peut en revanche prendre O(n²) places dans la trace.

Voir pour comprendre

Arbre et parcours

Les fils gauche et droit, les feuilles et les niveaux se lisent sur le même arbre de sept nœuds.

Arbre et parcours 4 a pour enfant gauche 2. 2 a pour enfant gauche 1. 2 a pour enfant droit 3. 4 a pour enfant droit 6. 6 a pour enfant gauche 5. 6 a pour enfant droit 7. 4213657

Taille : 7 nœuds · hauteur : 2 arêtes
Feuilles : 1, 3, 5, 7

  • Profondeur 0 : 4
  • Profondeur 1 : 2, 6
  • Profondeur 2 : 1, 3, 5, 7

Lis le schéma. Suis les fils avant de former une liste.

Une convention de hauteur fixe le cas vide ; un ordre de parcours fixe le moment où la racine est visitée relativement aux sous-arbres.

Exemples résolus et erreurs expliquées

Mesurer A et lire ses quatre parcours

  1. A contient sept nœuds, donc taille(A)=7.
  2. Ses feuilles ont hauteur 0 ; les nœuds 2 et 6 ont hauteur 1.
  3. La racine 4 a hauteur 1+max(1,1)=2.
  4. Préfixe, infixe, suffixe et largeur rendent respectivement 4,2,1,3,6,5,7 ; 1,2,3,4,5,6,7 ; 1,3,2,5,7,6,4 ; 4,2,6,1,3,5,7.

Conclusion. Les quatre résultats lisent une même structure selon quatre règles explicites.

Laboratoire de code

Languepython

Butcalculer taille et hauteur puis produire les parcours préfixe, infixe, suffixe et en largeur

Code solution
from collections import deque

def verifier_arbre(arbre):
    def verifier(n, profondeur, actifs):
        if n is None:
            return None
        if profondeur > 8 or type(n) is not tuple or len(n) != 3:
            raise ValueError('tuple ternaire, hauteur au plus 8')
        valeur, gauche, droite = n
        if type(valeur) is not int or not -1000 <= valeur <= 1000:
            raise ValueError('valeur entière entre -1000 et 1000')
        if id(n) in actifs:
            raise ValueError('cycle interdit')
        actifs.add(id(n))
        copie = (valeur, verifier(gauche, profondeur + 1, actifs),
                 verifier(droite, profondeur + 1, actifs))
        actifs.remove(id(n))
        return copie
    return verifier(arbre, 0, set())

def hauteur(arbre):
    arbre = verifier_arbre(arbre)
    def calculer(n):
        return -1 if n is None else 1 + max(calculer(n[1]), calculer(n[2]))
    return calculer(arbre)

def taille(arbre):
    arbre = verifier_arbre(arbre)
    def compter(n):
        return 0 if n is None else 1 + compter(n[1]) + compter(n[2])
    return compter(arbre)

def parcours_profondeur(arbre, ordre):
    arbre = verifier_arbre(arbre)
    if type(ordre) is not str or ordre not in ('prefixe', 'infixe', 'suffixe'):
        raise ValueError('ordre de parcours inconnu')
    sortie = []
    def visiter(n):
        if n is None:
            return
        valeur, gauche, droite = n
        if ordre == 'prefixe':
            sortie.append(valeur)
        visiter(gauche)
        if ordre == 'infixe':
            sortie.append(valeur)
        visiter(droite)
        if ordre == 'suffixe':
            sortie.append(valeur)
    visiter(arbre)
    return sortie

def parcours_largeur(arbre):
    arbre = verifier_arbre(arbre)
    if arbre is None:
        return (), ()
    file, sortie, trace = deque([arbre]), [], []
    while file:
        n = file.popleft()
        sortie.append(n[0])
        for enfant in n[1:]:
            if enfant is not None:
                file.append(enfant)
        trace.append((n[0], tuple(q[0] for q in file), tuple(sortie)))
    return tuple(sortie), tuple(trace)

Tests

A=(4,(2,(1,None,None),(3,None,None)),(6,(5,None,None),(7,None,None)))
assert hauteur(None)==-1 and hauteur((2,None,None))==0 and hauteur(A)==2
assert parcours_profondeur(A,'infixe')==[1,2,3,4,5,6,7]
for mauvais in ([1,None,None],(True,None,None),(1,None), (1,[],None)):
    try: hauteur(mauvais)
    except ValueError: pass
    else: raise AssertionError('invalide accepté')
assert taille(A)==7 and parcours_largeur(A)[0]==(4,2,6,1,3,5,7)

Trace

  • Pour A=(4,(2,(1,None,None),(3,None,None)),(6,(5,None,None),(7,None,None))), le retrait de 4 laisse la file (2,6) et la sortie (4,).
  • Le retrait de 2 laisse la file (6,1,3) et la sortie (4,2). Retirer 6 laisse ensuite (1,3,5,7).
  • La dernière sortie est (4,2,6,1,3,5,7), et la file est vide. Les instantanés ne sont pas changés par les retraits suivants.

Clinique de bogue

Indice observéAvec le programme ci-dessous, une feuille est annoncée à la hauteur 1. La convention du cours, en arêtes, donne 0.

CauseLe cas vide renvoie 0, puis une unité est ajoutée pour la feuille. Ce programme compte des niveaux, pas des arêtes. La formule récursive n'est pas en cause : pour obtenir 0 sur une feuille avec 1+max, les deux sous-arbres vides doivent fournir −1.

02

Étape du cours · 9 à 16 min

Arbres binaires de recherche : ordre, insertion et hauteur

Un ABR est un arbre binaire auquel s’ajoute un invariant d’ordre : toute clé du sous-arbre gauche est strictement inférieure à la racine, toute clé du sous-arbre droit lui est strictement supérieure. Chercher 5 dans une racine 4 n’examine donc pas la gauche : 5 ne peut s’y trouver. À chaque comparaison, un sous-arbre entier, pas nécessairement la moitié des nœuds est écartée parce que l’invariant l’autorise, non parce qu’un parcours serait magique.

L’insertion suit exactement le même chemin. Dans l’ABR 4,2,6, insérer 3 compare 3 à 4, va à gauche ; compare 3 à 2, va à droite ; la place vide devient le nœud 3. Avec le contrat du cours, une clé égale est ignorée : insérer 3 une seconde fois rend le même arbre. Le retour de nouveaux tuples conserve l’ancien arbre, ce qui permet de vérifier qu’une insertion ne l’a pas muté.

Le noyau recherche ou insertion suit un chemin de longueur h, où h est la hauteur : son nombre de comparaisons est au plus h+1, donc O(h+1). Si l’arbre est équilibré, h est de l’ordre de log n ; si les clés 1,2,3,4 sont insérées dans cet ordre, l’arbre est une chaîne et h=3. Il ne suffit donc pas de dire « ABR, donc logarithmique ». La validation avant la recherche ou l’insertion, puis le contrôle du résultat de l’insertion, parcourent la structure entière. La prévalidation complète de l’objet du laboratoire peut parcourir tous ses nœuds et être linéaire ; elle protège le contrat, mais ne change pas le raisonnement sur le noyau de recherche une fois la structure validée.

L’infixe est un contrôle adapté à l’invariant : gauche, racine, droite rend les clés croissantes pour un ABR valide. Il ne prouve pas à lui seul que la forme est équilibrée. Inversement, une hauteur faible ne vérifie pas l’ordre des clés. Lis l’infixe et mesure la hauteur pour répondre à deux questions distinctes : « les comparaisons éliminent-elles la bonne branche ? » et « quelle longueur peut avoir ce chemin ? »

Les clés sont des entiers stricts et bornés dans le laboratoire ; True, NaN et une valeur non comparable ne deviennent pas des clés par accident. Les cas utiles sont l’arbre vide, la feuille, une clé absente, un doublon et l’ordre d’insertion trié. Chaque cas éclaire une partie précise du contrat. Le test ne remplace pas l’invariant : il vérifie que les exemples observables restent cohérents avec lui. Pour garder la récursion lisible, une construction accepte au plus 100 clés et un arbre fourni a une hauteur d’au plus 100. Un résultat d’insertion dépassant cette hauteur est refusé ; le constructeur peut donc toujours traiter ses 100 clés.

Voir pour comprendre

Ordre et hauteur d’un ABR

Même ensemble de clés, deux formes : presque équilibrée ou chaîne.

Ordre et hauteur d’un ABR
InsertionFormeHauteur
4,2,6,1,3,5,7médiane2
1,2,3,4,5,6,7chaîne6

Lis le schéma. Lis l’ordre d’insertion puis la hauteur obtenue.

L'ordre des clés justifie une seule branche ; la hauteur, et non le seul nombre de nœuds, détermine le pire chemin.

Exemples résolus et erreurs expliquées

Insérer 3

  1. Dans 4,2,6, comparer 3 à 4 impose la gauche.
  2. Comparer 3 à 2 impose la droite.
  3. Cette place est vide : le nouveau nœud est 3.
  4. L’infixe devient 2,3,4,6 ; la hauteur passe de 1 à 2, car le chemin 4→2→3 contient deux arêtes.

Conclusion. Le noyau a suivi deux comparaisons, sans parcourir l’autre branche.

Laboratoire de code

Languepython

Butinsérer sans doublon puis rechercher une clé avec son chemin de comparaisons

Code solution
def verifier_cle_abr(cle):
    if type(cle) is not int or not -1000 <= cle <= 1000:
        raise ValueError('clé entière entre -1000 et 1000')
    return cle

def verifier_abr(arbre):
    def verifier(n, minimum, maximum, profondeur):
        if n is None:
            return
        if profondeur > 100 or type(n) is not tuple or len(n) != 3:
            raise ValueError('ABR de hauteur au plus 100 attendu')
        valeur, gauche, droite = n
        verifier_cle_abr(valeur)
        if not minimum < valeur < maximum:
            raise ValueError('ordre strict de l’ABR violé')
        verifier(gauche, minimum, valeur, profondeur + 1)
        verifier(droite, valeur, maximum, profondeur + 1)
    verifier(arbre, -1001, 1001, 0)
    return arbre

def verifier_valeurs_abr(valeurs):
    if type(valeurs) not in (list, tuple) or len(valeurs) > 100:
        raise ValueError('liste ou tuple de 100 clés au plus')
    return tuple(verifier_cle_abr(v) for v in valeurs)

def inserer_abr(arbre, cle):
    cle = verifier_cle_abr(cle)
    arbre = verifier_abr(arbre)
    def inserer(n):
        if n is None:
            return (cle, None, None)
        valeur, gauche, droite = n
        if cle == valeur:
            return n
        if cle < valeur:
            nouveau = inserer(gauche)
            return n if nouveau is gauche else (valeur, nouveau, droite)
        nouveau = inserer(droite)
        return n if nouveau is droite else (valeur, gauche, nouveau)
    resultat = inserer(arbre)
    verifier_abr(resultat)
    return resultat

def construire_abr(valeurs):
    arbre = None
    for cle in verifier_valeurs_abr(valeurs):
        arbre = inserer_abr(arbre, cle)
    return arbre

def recherche_abr(arbre, cle):
    cle = verifier_cle_abr(cle)
    arbre = verifier_abr(arbre)
    chemin = []
    while arbre is not None:
        valeur, gauche, droite = arbre
        chemin.append(valeur)
        if cle == valeur:
            return True, tuple(chemin)
        arbre = gauche if cle < valeur else droite
    return False, tuple(chemin)

def contient_abr(arbre, cle):
    return recherche_abr(arbre, cle)[0]

def infixe_abr(arbre):
    arbre = verifier_abr(arbre)
    sortie = []
    def visiter(n):
        if n is not None:
            visiter(n[1])
            sortie.append(n[0])
            visiter(n[2])
    visiter(arbre)
    return tuple(sortie)

def hauteur_abr(arbre):
    arbre = verifier_abr(arbre)
    def calculer(n):
        return -1 if n is None else 1 + max(calculer(n[1]), calculer(n[2]))
    return calculer(arbre)

Tests

A=construire_abr([4,2,6,1,3,5,7,3])
assert list(infixe_abr(A))==[1,2,3,4,5,6,7] and hauteur_abr(A)==2
assert construire_abr([1,2,3,4])==(1,None,(2,None,(3,None,(4,None,None))))
assert inserer_abr(A,3)==A and contient_abr(A,5)
for bad in (True,1.5,float('nan'),float('inf')):
    try: inserer_abr(None,bad)
    except ValueError: pass
    else:raise AssertionError('clé invalide')

Trace

  • A=construire_abr([4,2,6,1,3,5,7]) : recherche_abr(A,7) renvoie (True,(4,6,7)), soit trois comparaisons de clés.
  • La même recherche de 8 suit (4,6,7), puis atteint un sous-arbre vide : (False,(4,6,7)). L'absence n'ajoute pas une comparaison avec une clé.
  • Le contrôle préalable de tout A et celui du résultat d'insertion ne sont pas inclus dans ce compteur de comparaisons du noyau.

Clinique de bogue

Indice observéchercher_mauvais((2,(1,None,None),None),1) renvoie False : la clé existe, mais le programme l’élimine.

CauseAprès la comparaison 1<2, le programme suit le fils droit. Il confond la condition « plus petit » et l'indice du fils. L'invariant d'ordre justifie au contraire de conserver uniquement le sous-arbre gauche. La correction reprend le chemin de comparaison et vérifie préalablement que l'entrée est bien un ABR.

03

Étape du cours · 9 à 17 min

Graphes : marquer, choisir une frontière, reconstruire un chemin

Un graphe associe à chaque sommet ses voisins. Le laboratoire utilise un dictionnaire de 1 à 100 sommets nommés par des chaînes de 1 à 20 caractères ; tous les voisins doivent être déclarés, sans boucle sur soi ni voisin répété. L’ordre des listes de voisins est conservé. La largeur marque un sommet quand il entre dans la file. La profondeur conserve, dans une pile de cadres, le sommet courant et les voisins qu’il reste à examiner : elle descend vers un seul voisin non visité avant de revenir aux autres.

Depuis A dans le graphe A→B, A→C, B→D, C→D, la largeur commence avec la file [A]. Elle retire A, marque B et C, puis sa file est [B,C]. Elle retire B, marque D, puis sa file est [C,D]. Elle retire C sans réinsérer D, enfin D. La trace A,B,C,D montre les niveaux : A est à distance 0, B et C à distance 1, D à distance 2. A est le parent de B et de C ; B est ici le premier parent enregistré de D.

La profondeur du cours suit A,B,D,C : le cadre de A garde C en attente pendant la descente dans B puis D. Le sommet est marqué quand son cadre est empilé, avant l’exploration de ses propres voisins. Il ne sera donc pas visité une seconde fois. Cette implantation reproduit une exploration récursive complète ; empiler tous les voisins d’un coup en les marquant immédiatement peut produire un autre arbre de parcours. Contrairement à la largeur, la profondeur ne garantit pas que son premier chemin vers un sommet minimise le nombre d’arêtes.

Pourquoi le premier chemin de largeur est-il minimal en nombre d’arêtes ? Tous les sommets à distance k sont retirés avant les sommets à distance k+1. Quand D reçoit son premier parent B, B est au niveau 1 : le chemin A,B,D a donc deux arêtes. Aucun chemin vers D d’une arête n’existe, car seuls B et C étaient voisins de A. Le dictionnaire parents permet de remonter D,B,A puis d’inverser la liste. La fonction renvoie None pour une arrivée déclarée mais inaccessible. Si départ et arrivée sont identiques, elle renvoie le chemin à un seul sommet. Un nom non déclaré provoque ValueError.

Le cycle du laboratoire est non orienté. Il vérifie donc que chaque arête est symétrique. Pendant une profondeur, atteindre un voisin déjà vu signale un cycle si ce voisin n’est pas le parent par lequel on est arrivé ; l’arête de retour vers ce parent est normale. Cette règle ne s’importe pas telle quelle dans un graphe orienté. Teste séparément une chaîne, un sommet isolé, un cycle, deux composantes et un graphe non fermé : ce sont des modèles qui mettent l’invariant à l’épreuve. La file deque et la pile de cadres permettent au noyau de visiter chaque sommet et chaque arc une fois, soit O(V+E). Le contrôle de symétrie affiché utilise des recherches dans les tuples de voisins ; son coût préalable est distinct et peut être plus élevé.

Voir pour comprendre

Largeur et parents

A rejoint B et C, qui convergent vers D dans un graphe orienté.

Graphe orientéSommets : A, B, D, C. A vers B. A vers C. B vers D. C vers D. ABDC
Ligne = départ · colonne = arrivée
De / versABDC
A0101
B0010
D0000
C0010

Lis le schéma. La file visite A, puis B et C, puis D.

La file ou la pile fixe l'ordre ; le marquage garantit la terminaison ; les parents transforment un parcours en chemin explicite.

Exemples résolus et erreurs expliquées

Reconstruire A vers D

  1. La file initiale contient A, de distance 0.
  2. Retirer A donne B et C, tous deux avec parent A.
  3. Retirer B découvre D et fixe parent[D]=B.
  4. Remonter D,B,A donne le chemin A,B,D de deux arêtes.

Conclusion. C ne remplace pas le premier parent de D.

Laboratoire de code

Languepython

Butparcourir un graphe en largeur et en profondeur, reconstruire un chemin et détecter un cycle non orienté

Code solution
from collections import deque

def verifier_graphe(graphe):
    if type(graphe) is not dict or not 1 <= len(graphe) <= 100:
        raise ValueError('graphe de 1 à 100 sommets attendu')
    if any(type(s) is not str or not 1 <= len(s) <= 20 for s in graphe):
        raise ValueError('nom de sommet de 1 à 20 caractères')
    resultat = {}
    for s, voisins in graphe.items():
        if type(voisins) not in (list, tuple):
            raise ValueError('liste ou tuple de voisins attendu')
        if any(type(v) is not str or v not in graphe for v in voisins):
            raise ValueError('chaque voisin doit être déclaré')
        if len(set(voisins)) != len(voisins) or s in voisins:
            raise ValueError('pas de boucle ni de voisin répété')
        resultat[s] = tuple(voisins)
    return resultat

def verifier_sommet(graphe, sommet):
    if type(sommet) is not str or sommet not in graphe:
        raise ValueError('sommet absent')
    return sommet

def parcours_largeur(graphe, depart):
    graphe = verifier_graphe(graphe)
    depart = verifier_sommet(graphe, depart)
    parents, file, ordre = {depart: None}, deque([depart]), []
    while file:
        sommet = file.popleft()
        ordre.append(sommet)
        for voisin in graphe[sommet]:
            if voisin not in parents:
                parents[voisin] = sommet
                file.append(voisin)
    return tuple(ordre), parents

def chemin_minimal(graphe, depart, arrivee):
    graphe = verifier_graphe(graphe)
    verifier_sommet(graphe, depart)
    verifier_sommet(graphe, arrivee)
    ordre, parents = parcours_largeur(graphe, depart)
    if arrivee not in parents:
        return None
    chemin, sommet = [], arrivee
    while sommet is not None:
        chemin.append(sommet)
        sommet = parents[sommet]
    return tuple(reversed(chemin))

def parcours_profondeur(graphe, depart):
    graphe = verifier_graphe(graphe)
    depart = verifier_sommet(graphe, depart)
    # Chaque cadre garde les voisins qui restent à examiner.
    vus, ordre = {depart}, [depart]
    pile = [(depart, iter(graphe[depart]))]
    while pile:
        sommet, voisins = pile[-1]
        voisin = next(voisins, None)
        if voisin is None:
            pile.pop()
        elif voisin not in vus:
            vus.add(voisin)
            ordre.append(voisin)
            pile.append((voisin, iter(graphe[voisin])))
    return tuple(ordre)

def a_un_cycle_non_oriente(graphe):
    graphe = verifier_graphe(graphe)
    if any(s not in graphe[v] for s in graphe for v in graphe[s]):
        raise ValueError('arêtes symétriques attendues')
    vus = set()
    def visiter(sommet, parent):
        vus.add(sommet)
        for voisin in graphe[sommet]:
            if voisin == parent:
                continue
            if voisin in vus or visiter(voisin, sommet):
                return True
        return False
    return any(visiter(s, None) for s in graphe if s not in vus)

Tests

G={'A':['B','C'],'B':['A','D'],'C':['A','D'],'D':['B','C'],'E':[]}
assert parcours_largeur(G,'A')[0]==('A','B','C','D')
assert parcours_profondeur(G,'A')==('A','B','D','C')
assert chemin_minimal(G,'A','D')==('A','B','D') and chemin_minimal(G,'A','E') is None
assert a_un_cycle_non_oriente(G)
for bad in ({'A':['Z']},{'A':'B'}):
    try: verifier_graphe(bad)
    except ValueError:pass
    else:raise AssertionError('graphe invalide')

Trace

  • G={'A':['B','C'],'B':['D'],'C':['D'],'D':[],'E':[]} : la largeur retire A, puis B et C, puis D. Les parents sont A:None, B:A, C:A, D:B.
  • La profondeur conserve les voisins restant à examiner dans chaque cadre de pile. Elle descend A→B→D, remonte, puis découvre C : (A,B,D,C).
  • E est déclaré mais isolé : chemin_minimal(G,'A','E') renvoie None. Un nom absent tel que Z est au contraire une entrée invalide.

Clinique de bogue

Indice observéSur A→B→A, dfs_mauvais continue à empiler A puis B. Le marquage n’est jamais utilisé pour refuser un voisin déjà visité.

CauseAjouter un sommet à vus ne suffit pas : l'ajout d'un voisin doit être conditionné par son absence de cet ensemble. Ici pile.extend ajoute même les sommets connus, donc un cycle suffit à faire durer la boucle. La version corrigée marque chaque découverte, conserve les cadres de profondeur et n'empile jamais un sommet déjà marqué.

04

Étape du cours · 8 à 16 min

Diviser pour régner : prouver le tri fusion et lire n log₂ n

Diviser pour régner suit trois gestes : diviser une suite, résoudre les moitiés, puis combiner leurs résultats. Tri fusion s’arrête sur une suite de longueur zéro ou un, déjà triée. Le cas vide est indispensable : si une suite vide était encore divisée, la taille ne diminuerait plus. Le variant est la longueur de la partie traitée ; chaque appel non terminal le réduit.

La fusion est le cœur de la correction. Elle reçoit deux suites déjà triées, compare leurs têtes et ajoute la plus petite à la sortie. Son invariant est double : la sortie est triée, et elle contient exactement les occurrences consommées des deux entrées. Quand une moitié est épuisée, l’autre fin est déjà triée et peut être ajoutée sans comparaison supplémentaire. Cette justification locale est réutilisée à chaque remontée récursive.

Pour fusionner 1,4 et 2,3, comparer 1 et 2 donne 1 ; comparer 4 et 2 donne 2 ; comparer 4 et 3 donne 3 ; puis 4 restant est ajouté. La sortie est 1,2,3,4 et trois comparaisons ont été nécessaires à cette fusion. Sur 4,1,3,2, les deux petites fusions ajoutent encore une comparaison chacune : la trace entière compte donc cinq comparaisons, pas quatre.

La stabilité concerne des objets dont les clés peuvent être égales. Si la tête gauche et la tête droite ont la même clé, choisir la gauche avec <= conserve l’ordre relatif des occurrences. Des entiers seuls ne permettent pas de le voir : les paires (2,'gauche') et (2,'droite') le montrent. Le noyau numérique du laboratoire garde des entiers stricts pour son contrat ; le transfert étiqueté isole l’observation de stabilité sans brouiller ce domaine.

Il y a environ log₂ n niveaux de division et, à chaque niveau, les fusions parcourent au total n valeurs : le pire cas est donc de l’ordre de n log₂ n. Cette formule décrit la croissance du travail, non un chronomètre. Une trace et des tests sur vide, singleton, doublons, ordre inversé et ordre déjà trié relient enfin cette analyse au comportement concret. L’entrée accepte au plus 100 entiers de −1000 à 1000. Le noyau avec fusions et copies usuelles a un pic de mémoire auxiliaire O(n), hors pile O(log n). Cette version conserve toutefois les entrées et sorties de toutes les fusions dans trace : leur volume total peut atteindre O(n log n). Supprimer ce journal après observation n’est donc pas une modification de l’algorithme de tri, mais de sa mémoire d’observation.

Voir pour comprendre

Tri fusion stable

4,1,3,2 est divisé puis les sous-listes triées sont fusionnées.

  1. Entrée[4, 1, 3, 2]
  2. Diviser
    [4, 1][3, 2]
  3. Une valeur par sous-liste
    4132
  4. Fusionner les paires
    [1, 4][2, 3]
  5. Fusion finale[1, 2, 3, 4]
Coût de chaque fusion : nombre de comparaisons entre les premières valeurs restantes
FusionEntréesSortieCoût
Paire 1[4] et [1][1, 4]1
Paire 2[3] et [2][2, 3]1
Finale[1, 4] et [2, 3][1, 2, 3, 4]3

Résultat : [1, 2, 3, 4]. 5 comparaisons entre deux premières valeurs ; à égalité, la valeur de gauche est prise d’abord.

Lis le schéma. Lis des feuilles vers la fusion finale et compte les comparaisons.

La division ne suffit pas : les sous-problèmes doivent diminuer, le cas de base les arrêter et la combinaison conserver l'invariant du résultat.

Exemples résolus et erreurs expliquées

Fusionner le dernier niveau

  1. Les moitiés triées sont 1,4 et 2,3.
  2. 1 est pris avant 2, puis 2 avant 4.
  3. 3 est pris avant 4.
  4. 4 restant complète 1,2,3,4 ; cette fusion compte trois comparaisons.

Conclusion. Avec les deux fusions de singletons, 4,1,3,2 compte cinq comparaisons.

Laboratoire de code

Languepython

Buttrier par fusion, tracer la décomposition et compter les comparaisons de clés

Code solution
def verifier_valeurs_tri(valeurs):
    if type(valeurs) not in (list, tuple) or len(valeurs) > 100:
        raise ValueError('liste ou tuple de 100 valeurs au plus')
    if any(type(v) is not int or not -1000 <= v <= 1000 for v in valeurs):
        raise ValueError('entiers entre -1000 et 1000 attendus')
    return tuple(valeurs)

def tri_fusion_trace(valeurs):
    valeurs = verifier_valeurs_tri(valeurs)
    trace = []
    def fusion(gauche, droite):
        i, j, comparaisons, sortie = 0, 0, 0, []
        while i < len(gauche) and j < len(droite):
            comparaisons += 1
            if gauche[i] <= droite[j]:
                sortie.append(gauche[i])
                i += 1
            else:
                sortie.append(droite[j])
                j += 1
        sortie.extend(gauche[i:])
        sortie.extend(droite[j:])
        return tuple(sortie), comparaisons
    def trier(partie):
        if len(partie) <= 1:
            return partie, 0
        milieu = len(partie) // 2
        gauche, cg = trier(partie[:milieu])
        droite, cd = trier(partie[milieu:])
        resultat, c = fusion(gauche, droite)
        trace.append((gauche, droite, resultat, c))
        return resultat, cg + cd + c
    resultat, comparaisons = trier(valeurs)
    return {'sorted': resultat, 'trace': tuple(trace), 'comparisons': comparaisons}

Tests

R=tri_fusion_trace([4,1,3,2])
assert R['sorted']==(1,2,3,4) and R['comparisons']==5
assert tri_fusion_trace([])=={'sorted':(), 'trace':(), 'comparisons':0}
for bad in ([1,True],(1,1.5),range(2)):
    try:tri_fusion_trace(bad)
    except ValueError:pass
    else:raise AssertionError('valeur invalide')
assert tri_fusion_trace([2])['comparisons'] == 0

Trace

  • Dans tri_fusion_trace([4,1,3,2]), les deux divisions conduisent à quatre singletons. La première fusion inscrit ((4,),(1,),(1,4),1).
  • La seconde fusion inscrit ((3,),(2,),(2,3),1). Le dernier entier compte seulement les comparaisons entre deux têtes.
  • La fusion finale inscrit ((1,4),(2,3),(1,2,3,4),3). La somme des compteurs vaut 5 ; ce n'est ni le nombre d'appels ni le temps total.

Clinique de bogue

Indice observéDans cette variante, tri_fusion_trace([]) finit par lever RecursionError. Remplacer uniquement <= par == dans le cas de base suffit à créer le défaut.

CauseLe cas de base ne couvre que la taille un. Une entrée vide est pourtant valide, et ses deux moitiés sont toujours vides : la récursion ne diminue plus. La fusion correcte n'est jamais atteinte. Arrêter les tailles zéro et un restaure le variant, tout en conservant la même combinaison des résultats.

05

Étape du cours · 9 à 17 min

Programmation dynamique : mémoriser et reconstruire une solution

La programmation dynamique s’emploie lorsque plusieurs choix ramènent aux mêmes sous-problèmes. Pour rendre un montant M, on pourrait essayer toutes les suites de pièces, mais beaucoup de branches demanderaient de nouveau de rendre la même somme. On définit donc opt[a] comme le nombre minimal de pièces pour rendre a. La base est opt[0]=0 ; None signale une somme impossible, ce qui n’est pas la même chose que zéro pièce.

Pour une pièce p≤a, si la somme a−p est possible, ajouter p donne une candidate opt[a−p]+1. La récurrence est donc opt[a]=min(1+opt[a−p]) sur les candidats possibles, ou None s’il n’en existe aucun. Les pièces sont strictement positives : a−p<a, donc un remplissage par montants croissants dispose toujours des cases nécessaires. La justification d’optimalité vient du dernier choix : retirer la dernière pièce d’une solution optimale laisse une solution optimale de la somme restante, sinon on pourrait améliorer l’ensemble.

Avec les pièces 1,3,4, les nombres de pièces pour les montants 0 à 6 sont 0,1,2,1,1,2,2. À la case 6, choisir 1 complète la case 5 et donne trois pièces ; choisir 3 complète la case 3 et donne deux pièces ; choisir 4 complète la case 2 et en donne trois. On garde donc 3, puis on remonte de 6 à 3 et de 3 à 0 : la solution est (3,3). La stratégie gloutonne aurait pris 4, puis 1,1 et aurait utilisé une pièce de trop.

Le nombre minimal ne suffit pas à dire quelles pièces rendre. La table derniere conserve la pièce qui a donné chaque amélioration stricte ; le prédécesseur est alors a−derniere[a]. À égalité, la première pièce essayée est conservée, donc la plus petite puisque les pièces sont triées. Pour 5, choisir 1 après la solution de 4 ou choisir 4 après celle de 1 donne deux pièces : on conserve 1 et la reconstruction renvoie (1,4), dans le sens montant→zéro. Plusieurs solutions optimales peuvent exister sans rendre le résultat incorrect.

Pour k valeurs de pièces et un montant M, le noyau examine au plus kM candidats. Les deux tables occupent O(M) cases et la reconstruction au plus M étapes si la pièce 1 est disponible. Cette version stocke nombres et prédécesseurs, pas une copie de toute la solution dans chaque case. Le laboratoire accepte de 1 à 9 pièces entières distinctes comprises entre 1 et 9, triées, et un montant entier de 0 à 30. Les bornes rendent les tables faciles à inspecter ; elles ne sont pas une limite théorique de la méthode.

Le transfert passe à un autre problème : une plus longue sous-séquence commune. Une sous-séquence conserve l’ordre, mais peut sauter des éléments ; elle n’est ni un ensemble ni nécessairement un bloc contigu. Cette fois les sous-problèmes dépendent de deux préfixes, donc la mémoire prend la forme d’un tableau à deux dimensions. La même démarche reste utile : définir exactement une case, expliquer les transitions, puis reconstruire un témoin de la longueur calculée.

Voir pour comprendre

Table de monnaie

Avec 1,3,4, le montant 6 reconstruit deux pièces de 3.

Pièces disponibles : 1, 3, 4. Chaque ligne reprend le meilleur total déjà calculé.

Optimum et dernière pièce choisie. Avant : montant avant l’ajout de cette pièce.
MontantOptimumPièceAvant
00 pièceBaseBase
11 pièce10
22 pièces11
31 pièce30
41 pièce40
52 pièces14
62 pièces33
  1. 6retirer 33
  2. 3retirer 30

6 = 3 + 3 : 2 pièces au minimum.

Lis le schéma. Chaque montant lit seulement des montants déjà remplis.

La table remplace les recalculs par une dépendance explicite entre sous-problèmes ; un pointeur de choix permet de reconstruire, pas seulement de compter.

Exemples résolus et erreurs expliquées

Calculer la case 6, puis retrouver les pièces

  1. Les cases 0,1,2,3,4,5 contiennent respectivement les nombres de pièces 0,1,2,1,1,2. La case 6 est d’abord impossible.
  2. Essayer la pièce 1 donne opt[5]+1=3 ; essayer 3 donne opt[3]+1=2 ; essayer 4 donne opt[2]+1=3.
  3. La meilleure candidate utilise deux pièces : opt[6]=2 et derniere[6]=3.
  4. Partir de 6, retirer 3, puis lire derniere[3]=3 et atteindre 0. La somme 3+3=6 et le nombre de pièces 2 vérifient la reconstruction.

Conclusion. La table prouve le minimum par ses transitions ; le chemin 6→3→0 fournit une solution qui l’atteint.

Laboratoire de code

Languepython

Butcalculer un rendu minimal par table ascendante et reconstruire les pièces choisies

Code solution
def verifier_pieces_montant(pieces, montant):
    if type(pieces) not in (list, tuple) or not 1 <= len(pieces) <= 9:
        raise ValueError('liste ou tuple de 1 à 9 pièces')
    if any(type(p) is not int or not 1 <= p <= 9 for p in pieces):
        raise ValueError('pièces entières de 1 à 9')
    if tuple(pieces) != tuple(sorted(set(pieces))):
        raise ValueError('pièces strictement croissantes')
    if type(montant) is not int or not 0 <= montant <= 30:
        raise ValueError('montant entier de 0 à 30')
    return tuple(pieces), montant

def monnaie_minimale(pieces, montant):
    pieces, montant = verifier_pieces_montant(pieces, montant)
    optimum = [None] * (montant + 1)
    derniere = [None] * (montant + 1)
    optimum[0] = 0
    for somme in range(1, montant + 1):
        for p in pieces:
            if p <= somme and optimum[somme-p] is not None:
                candidat = optimum[somme-p] + 1
                if optimum[somme] is None or candidat < optimum[somme]:
                    optimum[somme] = candidat
                    derniere[somme] = p
    if optimum[montant] is None:
        return None
    solution, reste = [], montant
    while reste > 0:
        p = derniere[reste]
        solution.append(p)
        reste -= p
    return {'count': optimum[montant], 'solution': tuple(solution)}

Tests

assert monnaie_minimale([1,3,4],6)=={'count':2,'solution':(3,3)}
assert monnaie_minimale([2,4],3) is None and monnaie_minimale([1,3,4],0)=={'count':0,'solution':()}
for bad in (([1,1],2),([1,True],2),([1,3],True),([1,3],1.0),((p for p in [1,3]),6)):
    try:monnaie_minimale(*bad)
    except ValueError:pass
    else:raise AssertionError('domaine invalide')
assert monnaie_minimale([1,3,4],5) == {'count':2,'solution':(1,4)}

Trace

  • dp[0]=0 fournit le seul sous-problème initial certain
  • pour 3, la transition depuis 0 avec la pièce 3 remplace les trois pièces de 1 par une seule
  • pour 6, la transition depuis dp[3] choisit une seconde pièce de 3 et la reconstruction remonte 6,3,0

Clinique de bogue

Indice observéPour les pièces 1,3,4 et le montant 6, la méthode gloutonne ci-dessous renvoie (4,1,1). La somme est correcte, mais le nombre de pièces n’est pas minimal.

CauseLe choix local de la plus grande pièce n'est pas toujours compatible avec le meilleur résultat global. Ici la première pièce 4 force deux pièces de 1, tandis que 3 laisse encore 3. Comparer les sous-sommes et mémoriser leur optimum corrige le problème ; contrôler uniquement la somme aurait laissé passer le défaut.

06

Étape du cours · 9 à 16 min

Recherche textuelle : prétraiter le motif pour justifier les décalages

Horspool cherche ici la première occurrence exacte d’un motif non vide dans un texte ASCII majuscule. Le domaine est volontairement fermé : pas de normalisation Unicode, pas de liste de caractères, pas de convention silencieuse pour le motif vide. La fonction rend l’indice 0 si le motif commence le texte, et −1 s’il est absent. Si le motif est plus long que le texte, aucune fenêtre n’est possible et le résultat est −1. Le texte peut être vide et contient au plus 50 lettres ; le motif en contient de 1 à 10. Ces limites de laboratoire gardent les traces courtes.

Pour le motif ABC de longueur 3, la table prépare A→2 et B→1, leurs distances à la dernière position, et donne 3 pour les autres caractères. Le dernier C est exclu du prétraitement pour éviter un saut nul. Après un échec, on lit la lettre x du texte sous la fin du motif. Une occurrence plus loin devrait replacer cette même lettre sur une position compatible du motif : la plus proche est son occurrence la plus à droite avant la fin. On peut donc sauter directement jusqu’à elle ; si x n’y figure pas, les trois alignements partiels sont impossibles et le saut vaut la longueur du motif. Des lettres répétées imposent de conserver l’occurrence préparée la plus à droite.

Dans ABAAABCD, la fenêtre 0 compare d’abord A à C et échoue. Le A placé sous la fin possède le décalage 2, donc la fenêtre suivante commence à 2. Elle échoue encore sur A et saute de nouveau de 2. À l’offset 4, la comparaison de droite à gauche réussit sur C, puis B, puis A : le résultat est 4. La trace est alignements (0,2,4) et sauts (2,2). Aucun saut ne suit ce succès.

La borne de boucle start≤n−m inclut la dernière fenêtre légitime. Elle évite le défaut classique qui oublie un motif placé exactement à la fin du texte. Le tableau de sauts est une préparation du motif ; la comparaison de droite à gauche et le saut sous la fin sont les mécanismes que tu dois pouvoir expliquer sur une fenêtre, pas une formule de coût à réciter.

Horspool est la variante de recherche étudiée ici. Boyer-Moore peut aussi exploiter le caractère au point d’échec et un bon suffixe déjà reconnu ; ces tables ont des règles distinctes. Le prétraitement de Horspool parcourt le motif en O(m) et peut épargner beaucoup d’alignements, mais son pire cas reste O(nm) comparaisons, par exemple sur un texte répétitif avec un motif presque concordant. Le transfert compte séparément comparaisons et alignements, sans chronomètre, pour expliquer un gain sur un exemple et un contre-exemple sans annoncer une supériorité universelle.

Voir pour comprendre

Fenêtre Horspool

La fenêtre ABC commence successivement aux indices 0, 2 puis 4 dans ABAAABCD.

Indice
0
1
2
3
4
5
6
7
Texte
A
B
A
A
A
B
C
D
Motif
A
B
C

Comparer de droite à gauche

  1. texte[2] = Amotif[2] = C

Décalages préparés

Décalage lu sous la fin du motif après un échec
LettreDécalage
A2
B1
Autre3

Échec : la lettre sous la fin du motif est A. Décalage de 2, depuis 0 vers 2.

Première occurrence à l’indice 4. Alignements atteints : 0, 2, 4.

Lis le schéma. Lis le caractère sous la fin de la fenêtre après un échec.

Le prétraitement ne devine pas l'occurrence : il transforme la structure du motif en exclusions sûres de certains alignements.

Exemples résolus et erreurs expliquées

Décaler ABC dans ABAAABCD

  1. La table donne A→2, B→1, autre caractère→3.
  2. À 0, A est sous C : saut sûr de 2.
  3. À 2, même échec et même saut de 2.
  4. À 4, C,B,A correspondent : première occurrence 4.

Conclusion. Les sauts sont seulement ceux des échecs 0 et 2.

Laboratoire de code

Languepython

Butconstruire une table de décalages et tracer une recherche de Boyer-Moore-Horspool

Code solution
def verifier_texte_motif(texte, motif):
    if type(texte) is not str or type(motif) is not str:
        raise ValueError('chaînes attendues')
    if len(texte) > 50 or not 1 <= len(motif) <= 10:
        raise ValueError('texte de 50 lettres au plus, motif de 1 à 10 lettres')
    if any(not 'A' <= c <= 'Z' for c in texte + motif):
        raise ValueError('lettres ASCII majuscules attendues')
    return texte, motif

def horspool_premiere_occurrence(texte, motif):
    texte, motif = verifier_texte_motif(texte, motif)
    longueur, table = len(motif), {}
    for i, lettre in enumerate(motif[:-1]):
        table[lettre] = longueur - 1 - i
    debut, alignements, sauts = 0, [], []
    while debut <= len(texte) - longueur:
        alignements.append(debut)
        j = longueur - 1
        while j >= 0 and texte[debut+j] == motif[j]:
            j -= 1
        if j < 0:
            return {'index': debut, 'alignments': tuple(alignements), 'shifts': tuple(sauts)}
        saut = table.get(texte[debut+longueur-1], longueur)
        sauts.append(saut)
        debut += saut
    return {'index': -1, 'alignments': tuple(alignements), 'shifts': tuple(sauts)}

Tests

R=horspool_premiere_occurrence('ABAAABCD','ABC')
assert R=={'index':4,'alignments':(0,2,4),'shifts':(2,2)}
assert horspool_premiere_occurrence('AAAA','AA')['index']==0
for bad in ((['A'],'A'),('ABC',''),('é','E'),('ABC',('A',))):
    try:horspool_premiere_occurrence(*bad)
    except ValueError:pass
    else:raise AssertionError('texte invalide')
assert horspool_premiere_occurrence('ABC','BC')['index'] == 1

Trace

  • Pour ABAAABCD et ABC : la table est A:2, B:1 ; tout autre caractère donne 3. La dernière position du motif est exclue du prétraitement.
  • À l'alignement 0 puis à l'alignement 2, le caractère sous la fin est A : les deux sauts valent 2.
  • À l'alignement 4, comparer C, puis B, puis A réussit. Le résultat exact est index=4, alignments=(0,2,4), shifts=(2,2), sans saut après le succès.

Clinique de bogue

Indice observéAvec la borne ci-dessous, chercher_naif('ABC','BC') renvoie −1. L’occurrence finale qui commence à l’indice 1 n’a pas été examinée.

CauseLa borne supérieure de range est exclue. Les alignements possibles vont de 0 à n−m inclus : il faut donc range(n−m+1). Sans le +1, un motif placé à la fin est oublié, et même un texte égal au motif ne teste aucune fenêtre. Cette erreur concerne la recherche naïve, pas la construction de la table Horspool.

Poursuivre avec l’abonnement

Passe des traces à tes propres algorithmes

Lis les explications, les schémas et les exemples résolus pour suivre les invariants et les cas limites.

Entraîne-toi avec ateliers, diagnostics, projet, questions et cartes de révision.

12 questions · 12 cartes. Ta reprise et tes révisions sont enregistrées dans ce navigateur. Elles ne se synchronisent pas entre appareils.

Accéder à l’entraînement

Vérifier et prolonger

Sources du cours

Édition Maxdecours · Vérifié le .

Programme de spécialité NSI Terminale, BO spécial du 25 juillet 2019

  1. Programme de numérique et sciences informatiques de Terminale généraleMinistère de l'Éducation nationale et de la Jeunesse · consulté le 2026-09-06
  2. Programmes et ressources en numérique et sciences informatiques, voie généraleÉduscol · consulté le 2026-09-06
  3. L'algorithme de Boyer et MooreÉduscol · consulté le 2026-09-06
  4. Programmation dynamiqueÉduscol · consulté le 2026-09-06
  5. Diviser pour régnerÉduscol · consulté le 2026-09-06
  6. Arbres binaires de rechercheÉduscol · consulté le 2026-09-06
  7. Généralités sur les graphesÉduscol · consulté le 2026-09-06
  8. Practical Fast Searching in StringsR. Nigel Horspool, Software: Practice and Experience · consulté le 2026-09-06