Adloun

Recherche séquentielle et dictionnaires

Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 2 · 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>2.1 Introduction et motivation

Chercher : voilà sans doute l'opération la plus exécutée de toute l'informatique. Un nom dans un répertoire, un mot dans un texte, le plus grand échantillon d'une série de mesures — derrière chacune de ces questions se cache un parcours de tableau, l'algorithme le plus simple qui soit et pourtant le premier où tout se joue : la spécification du chapitre 1 s'y applique, l'invariant y fait sa première vraie preuve, et une question nouvelle surgit — combien de temps cela prend-il ?

Ce chapitre installe donc le second pilier du cours : la complexité, c'est-à-dire l'estimation asymptotique du coût d'un algorithme dans le cas le pire. On y rencontre les deux premières classes de coût — constant et linéaire — et un personnage qui les illustre à merveille : le dictionnaire de Python, utilisé en boîte noire, qui retrouve une valeur en temps constant là où le tableau exige un parcours complet.

2.2 Le coût d'un algorithme

2.2.1 Compter quoi, et dans quel cas ?

Définition 2.1Modèle de coût

Le coût d'une exécution est le nombre d'opérations élémentaires effectuées : affectations, comparaisons, opérations arithmétiques, accès à une case de tableau — chacune comptée pour un temps constant. On exprime ce coût en fonction de la taille de l'entrée (notée : longueur du tableau, nombre de caractères du texte, etc.).

Définition 2.2Coût dans le cas le pire

Pour une taille donnée, toutes les entrées ne coûtent pas le même prix : chercher un élément placé en tête est immédiat, le chercher en vain oblige à tout lire. Le programme retient le coût dans le cas le pire :

C'est une garantie : aucune entrée de taille ne coûtera davantage. Un algorithme rapide « en général » mais catastrophique sur certaines entrées ne vaut rien pour qui doit s'engager sur un temps de réponse.

2.2.2 L'estimation asymptotique

Compter les opérations une à une est aussi fastidieux qu'inutile : entre et opérations, la différence relève du détail d'implémentation — du langage, de la machine. Ce qui compte est le comportement quand grandit : doubler la taille de l'entrée double-t-il le temps, le quadruple-t-il, ou ne change-t-il presque rien ?

Définition 2.3Notation

Soient et deux fonctions de dans . On dit que s'il existe une constante et un rang tels que

Autrement dit : à une constante multiplicative près, ne croît pas plus vite que . Ainsi , : les deux algorithmes correspondants ont le même comportement asymptotique, dit linéaire.

Important

La notation jette volontairement deux informations : les constantes multiplicatives et les termes d'ordre inférieur. On écrit donc aussi bien pour que pour opérations ; et , car pour grand le terme écrase le reste. Cette perte est un gain : elle rend la mesure indépendante de la machine et du langage, et ne retient que la nature de l'algorithme.

Définition 2.4Coût constant, coût linéaire
  • Un traitement est à coût constant, noté , si son nombre d'opérations est borné indépendamment de : accéder à t[i], comparer deux nombres, affecter une variable.
  • Un algorithme est à coût linéaire, noté , si son coût dans le cas le pire est au plus proportionnel à : c'est la signature d'un nombre borné de parcours du tableau.

Complexité : L'échelle de lecture

Pour se représenter ce que ces classes signifient, supposons une machine exécutant opérations élémentaires par seconde, et (un tableau d'un milliard de cases) :

CoûtOpérations pour Temps approximatif
quelques-unesinstantané
seconde
(chapitre 3) ans

La complexité n'est pas une élégance théorique : c'est la frontière entre ce qui répond dans la seconde et ce qui ne répondra jamais.

2.3 Manipulations élémentaires d'un tableau

Méthode : Les gestes de base et leur coût

Pour une liste Python t de longueur :

GesteÉcritureCoût
Lire ou écrire une case`t[i]`, `t[i] = v`
Longueur`len(t)`
Ajouter en queue`t.append(v)`
Parcourir`for x in t` ou `for i in range(n)`
Tester l'appartenance`v in t` — un parcours caché !
Copier`list(t)`

La ligne piège est l'avant-dernière : v in t a l'air d'un test anodin, c'est une recherche séquentielle complète. L'écrire dans une boucle transforme silencieusement un algorithme linéaire en algorithme quadratique.

Exemple 2.5Deux parcours, deux écritures

Calculer la somme des éléments se fait par valeurs ou par indices :


def somme(t: list) -> float:
    s = 0
    for x in t:        # parcours par VALEURS
        s += x
    return s

def somme_indices(t: list) -> float:
    s = 0
    for i in range(len(t)):    # parcours par INDICES
        s += t[i]
    return s

Les deux sont en . Le parcours par valeurs est plus lisible quand on ne se sert pas de la position ; le parcours par indices devient nécessaire dès qu'on compare des cases entre elles ( et ), qu'on écrit dans le tableau, ou que la position fait partie du résultat.

2.4 La recherche séquentielle

2.4.1 L'algorithme et sa preuve

Définition 2.6Recherche séquentielle

La recherche séquentielle (ou linéaire) d'une valeur dans un tableau examine les cases une à une, dans l'ordre, et s'arrête à la première occurrence :


def recherche(v, t: list):
    """Renvoie le plus petit i tel que t[i] == v, ou None si v est absent."""
    for i in range(len(t)):
        # Invariant : v n'apparaît pas dans t[0..i-1]
        if t[i] == v:
            return i
    return None
Démonstration (Correction)

L'invariant : « » est vrai initialement (préfixe vide) et conservé : on n'atteint le tour que si le test t[i] == v a échoué, ce qui étend l'invariant d'une case. Deux sorties possibles. Si la fonction renvoie : alors et, par l'invariant, aucun indice plus petit ne convient — c'est bien le plus petit indice. Si la boucle s'achève : dit que n'apparaît nulle part, et None est conforme au contrat. La terminaison est acquise (boucle for).

Complexité : Recherche séquentielle

Chaque tour coûte (un test, une comparaison d'égalité). Dans le cas le pire — élément absent, ou présent en dernière position — la boucle fait tours : le coût est , linéaire. Dans le meilleur cas ( en tête), un seul tour suffit : le cas le pire et le meilleur cas peuvent être très éloignés, et c'est bien le pire que l'on garantit.

iRemarque

Peut-on faire mieux que comparaisons dans le cas le pire ? Pas sans hypothèse supplémentaire : si le tableau est quelconque, toute case non examinée pourrait contenir , et un adversaire malicieux placerait précisément là. Il faudra une structure — un tableau trié (chapitre 5) ou un dictionnaire (section suivante) — pour battre la recherche séquentielle.

2.4.2 Maximum, et second maximum

Exemple 2.7Le maximum, version définitive

Le chapitre 1 a spécifié, testé et prouvé la fonction ; on retient sa forme canonique, en :


def maximum(t: list) -> float:
    """Renvoie le plus grand élément de t. Précondition : t non vide."""
    m = t[0]
    for i in range(1, len(t)):
        # Invariant : m est le maximum de t[0..i-1]
        if t[i] > m:
            m = t[i]
    return m

Elle effectue exactement comparaisons d'éléments — et l'on peut démontrer qu'aucun algorithme ne peut faire moins : pour certifier un maximum, chacun des autres éléments doit avoir perdu au moins une comparaison.

Exemple 2.8Le second maximum en un seul passage

Le second maximum (le plus grand élément du tableau privé d'une occurrence du maximum) se calcule naïvement en deux passages : trouver le max, puis le max du reste. Un seul passage suffit, en maintenant deux champions :


def deux_maximums(t: list) -> tuple:
    """Renvoie (m1, m2) : le maximum de t et le second maximum.
    Précondition : len(t) >= 2."""
    if t[0] >= t[1]:
        m1, m2 = t[0], t[1]
    else:
        m1, m2 = t[1], t[0]
    for i in range(2, len(t)):
        # Invariant : (m1, m2) sont les deux plus grands de t[0..i-1]
        if t[i] > m1:
            m1, m2 = t[i], m1      # nouveau champion : l'ancien devient second
        elif t[i] > m2:
            m2 = t[i]              # nouveau second seulement
    return (m1, m2)

Le point délicat est la ligne du nouveau champion : l'ancien maximum ne disparaît pas, il descend à la deuxième place. L'oublier — écrire m1 = t[i] seul — est le bogue classique, que le jeu de tests deux_maximums([1, 5, 9]) attrape aussitôt ( doit valoir , pas ).

iRemarque

Avec des doublons, le contrat doit trancher (chapitre 1 !) : pour , notre spécification donne — le second maximum est la deuxième occurrence de . L'autre contrat (« le plus grand élément strictement inférieur au maximum », donnant ) est tout aussi défendable : c'est l'énoncé qui décide, jamais l'implémentation.

2.5 Les dictionnaires

2.5.1 Une boîte noire à accès direct

Définition 2.9Dictionnaire

Un dictionnaire (type dict) associe des valeurs à des clés : là où un tableau est indexé par les entiers , un dictionnaire est indexé par des clés arbitraires (nombres, chaînes, tuples…). Les gestes de base :


d = {}                      # dictionnaire vide
d["pommes"] = 3             # associer la valeur 3 à la clé "pommes"
d["pommes"]                 # lire la valeur associée -> 3 (KeyError si absente)
"poires" in d               # la clé existe-t-elle ?  -> False
d.get("poires", 0)          # lire avec valeur par défaut -> 0
del d["pommes"]             # supprimer une association
for cle in d:               # parcourir les clés
    print(cle, d[cle])

L'annexe du programme exige l'itération via d.keys() et d.items() ; le raccourci for cle in d leur est équivalent.

ImportantLe contrat de coût, admis

On utilise le dictionnaire en boîte noire : son mécanisme interne (la table de hachage) est hors programme. On admet son contrat de coût : l'insertion, la lecture, le test d'appartenance et la suppression s'effectuent en temps constant — contre pour le test v in t sur une liste. C'est l'écart entre ouvrir un annuaire à la bonne page et le lire ligne à ligne.

2.5.2 Le comptage : l'idiome fondamental

Exemple 2.10Compter les éléments d'un tableau

Combien de fois chaque valeur apparaît-elle dans ? Le dictionnaire répond en un seul parcours :


def comptage(t: list) -> dict:
    """Renvoie le dictionnaire {valeur: nombre d'occurrences dans t}."""
    c = {}
    for x in t:
        # Invariant : c[v] est le nb d'occurrences de v dans la partie déjà vue
        c[x] = c.get(x, 0) + 1
    return c

assert comptage([1, 2, 1, 1]) == {1: 3, 2: 1}
assert comptage([]) == {}

L'idiome c.get(x, 0) + 1 traite d'un même geste la première apparition (défaut ) et les suivantes (get est hors annexe — la version exigible est le test if x in c). Coût : tours à chacun, soit — alors que compter chaque valeur par t.count(x) dans une boucle coûterait parcours complets, un gaspillage quadratique.

Exemple 2.11Le doublon en temps linéaire

Le tableau contient-il deux fois la même valeur ? Version dictionnaire, :


def a_un_doublon(t: list) -> bool:
    vus = {}
    for x in t:
        if x in vus:            # O(1) : c'est un dictionnaire
            return True
        vus[x] = True
    return False

La même fonction avec if x in deja_vus sur une liste serait correcte… et quadratique : chaque test cacherait un parcours. Le choix de la structure de données est le choix de la complexité.

Méthode : Quand penser au dictionnaire ?

Trois signaux dans un énoncé appellent un dictionnaire :

  • « combien de fois… » : comptage (c[x] = c.get(x, 0) + 1) ;
  • « a-t-on déjà vu… » : mémoire des éléments rencontrés (test d'appartenance en ) ;
  • « associer à chaque… » : table d'association clé valeur (index, annuaire, inventaire).

Dans les trois cas, le dictionnaire remplace un parcours répété ( par question) par un accès direct () — et fait souvent passer l'algorithme entier de quadratique à linéaire.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>2.6 Exercices résolus

Niveau (Application directe du cours)

Exercice 1 : Lire des coûts

Donner le coût asymptotique, en fonction de len(t), de chacun des fragments :


# (a)
x = t[0] + t[-1]

# (b)
s = 0
for x in t:
    if x > 0:
        s += x

# (c)
p = 0
for x in t:
    if x in t:      # !
        p += 1
Démonstration (Solution)

(a) Deux accès et une addition : , quel que soit — t[-1] désigne la dernière case (l'indexation négative est une commodité Python, hors annexe du programme).

(b) Un parcours, chaque tour en : — le test if ne change rien à l'asymptotique, il borne juste le travail de chaque tour.

(c) Le piège : x in t est une recherche séquentielle, à chaque tour. Au total : quadratique — pour calculer ce qui vaut toujours len(t) ! (Un coût se lit en multipliant le nombre de tours par le coût d'un tour, en n'oubliant aucun parcours caché.)

Exercice 2 : Recherche de la dernière occurrence

Écrire derniere_occurrence(v, t) (plus grand indice tel que , ou None) en un seul parcours, donner son invariant et son coût.

Démonstration (Solution)

On parcourt tout le tableau en retenant la dernière position vue :


def derniere_occurrence(v, t: list):
    pos = None
    for i in range(len(t)):
        # Invariant : pos est le plus grand indice j < i tel que t[j] == v,
        #             ou None si v est absent de t[0..i-1]
        if t[i] == v:
            pos = i
    return pos

L'invariant est conservé : si , le plus grand indice devient ; sinon il ne change pas. À la sortie (), pos est le plus grand indice de tout le tableau, ou None. Coût : , et l'on ne peut pas s'arrêter plus tôt — contrairement à la première occurrence, la dernière exige d'avoir tout vu (une occurrence pourrait se cacher dans la partie non lue). (Parcourir à l'envers et s'arrêter à la première trouvaille est l'alternative : même pire cas , mais meilleur cas .)

Exercice 3 : Premiers pas de dictionnaire

Un texte est donné comme une chaîne s. Écrire frequences(s) qui renvoie le dictionnaire des fréquences de chaque caractère, puis l'utiliser pour trouver le caractère le plus fréquent de &quot;abracadabra&quot;.

Démonstration (Solution)

def frequences(s: str) -> dict:
    """Renvoie {caractère: nombre d'apparitions dans s}."""
    f = {}
    for c in s:
        f[c] = f.get(c, 0) + 1
    return f

f = frequences("abracadabra")
# f == {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
plus_frequent = None
for c in f:
    if plus_frequent is None or f[c] > f[plus_frequent]:
        plus_frequent = c
# plus_frequent == 'a'

Deux étages, tous deux linéaires : le comptage parcourt les caractères (), la recherche du maximum parcourt les clés du dictionnaire — au plus , donc aussi. (C'est le « maximum » du début du chapitre, appliqué non plus à un tableau mais aux clés d'un dictionnaire : les algorithmes se composent.)

Niveau (Application avec raisonnement intermédiaire)

Exercice 4 : Le maximum et son indice, les égalités en prime

Écrire indices_du_maximum(t) qui renvoie la liste de tous les indices où le maximum est atteint, en un seul parcours. Exemple : .

Démonstration (Solution)

On maintient le maximum courant et la liste de ses positions ; un nouveau champion remet la liste à zéro :


def indices_du_maximum(t: list) -> list:
    """Précondition : t non vide."""
    m, pos = t[0], [0]
    for i in range(1, len(t)):
        # Invariant : m = max(t[0..i-1]) et pos = liste des j < i avec t[j] == m
        if t[i] > m:
            m, pos = t[i], [i]      # nouveau maximum : on repart de lui seul
        elif t[i] == m:
            pos.append(i)           # une position de plus pour le même maximum
    return pos

assert indices_du_maximum([3, 7, 1, 7]) == [1, 3]
assert indices_du_maximum([5]) == [0]
assert indices_du_maximum([2, 2, 2]) == [0, 1, 2]

La subtilité est la remise à zéro pos = [i] : les anciennes positions concernaient un maximum désormais battu, elles ne valent plus rien. Coût . (Trois branches — strictement plus grand, égal, plus petit — et chacune mérite son test : c'est le tri des cas du chapitre 1 en action.)

Exercice 5 : L'élément majoritaire, en deux passages

Un élément est majoritaire dans s'il apparaît strictement plus de fois. Écrire majoritaire(t) qui le renvoie, ou None s'il n'existe pas, en coût .

Démonstration (Solution)

Le comptage par dictionnaire donne tout en deux parcours linéaires :


def majoritaire(t: list):
    c = {}
    for x in t:                    # passage 1 : compter
        c[x] = c.get(x, 0) + 1
    for v in c:                    # passage 2 : chercher un compte > n/2
        if c[v] > len(t) / 2:
            return v
    return None

assert majoritaire([2, 5, 2, 2]) == 2
assert majoritaire([2, 5, 2, 5]) is None    # 2 fois sur 4 : pas STRICTEMENT plus de n/2
assert majoritaire([]) is None

Coût : . La version sans dictionnaire — pour chaque élément, compter ses occurrences par un parcours — coûterait : le dictionnaire divise le travail par un facteur . (Au plus un élément peut être majoritaire — deux comptes ne peuvent excéder chacun — c'est pourquoi renvoyer le premier trouvé est correct.)

Exercice 6 : Inverser un annuaire

Un dictionnaire annuaire associe à chaque nom un numéro de téléphone (les numéros sont supposés distincts). Construire le dictionnaire inverse qui : numéro nom, puis adapter au cas où plusieurs noms partagent un numéro (le résultat associe alors à chaque numéro la liste des noms).

Démonstration (Solution)

def inverse(annuaire: dict) -> dict:
    """Précondition : les valeurs de annuaire sont deux à deux distinctes."""
    qui = {}
    for nom in annuaire:
        qui[annuaire[nom]] = nom
    return qui

def inverse_multi(annuaire: dict) -> dict:
    """Version générale : numéro -> liste des noms (sans précondition)."""
    qui = {}
    for nom in annuaire:
        num = annuaire[nom]
        if num not in qui:
            qui[num] = []
        qui[num].append(nom)
    return qui

Coût pour entrées dans les deux cas. La première version exige sa précondition : si deux noms partagent un numéro, l'écriture qui[num] = nom écrase silencieusement le premier — une erreur de logique sans aucun message, exactement le scénario contre lequel le chapitre 1 mettait en garde. La version multi remplace l'écrasement par l'accumulation dans une liste. (L'idiome « si la clé est neuve, créer une liste vide, puis ajouter » est le second réflexe-dictionnaire à connaître, après le comptage.)

Exercice 7 : Deux sommes pour une cible

Écrire deux_sommes(t, cible) qui détermine s'il existe deux indices tels que cible, en coût — et justifier le coût.

Démonstration (Solution)

L'idée : pour chaque rencontré, le partenaire idéal est ; un dictionnaire mémorise les valeurs déjà vues pour interroger le passé en :


def deux_sommes(t: list, cible: float) -> bool:
    vus = {}
    for x in t:
        # Invariant : vus contient exactement les valeurs de la partie déjà parcourue
        if (cible - x) in vus:
            return True
        vus[x] = True
    return False

assert deux_sommes([3, 8, 2, 5], 10) == True      # 8 + 2
assert deux_sommes([3, 8, 2, 5], 4) == False
assert deux_sommes([5, 5], 10) == True            # deux indices distincts, valeurs égales
assert deux_sommes([5], 10) == False              # un seul 5 : pas de paire

Chaque tour effectue un test d'appartenance et une insertion, tous deux : total . La version naïve à deux boucles imbriquées (tester tous les couples) coûte — le chapitre 3 lui est consacré. Noter que l'invariant règle finement le cas avec cible : quand on examine , le dictionnaire ne contient que le passé strict, donc pas lui-même — les deux indices sont automatiquement distincts. (Interroger « le passé » mémorisé dans un dictionnaire plutôt que re-parcourir : c'est le geste qui fait tomber un facteur .)

Niveau (Raisonnement subtil ou plusieurs étapes)

Exercice 8 : Le second maximum et son compte de comparaisons

La fonction deux_maximums du cours effectue, dans le cas le pire, comparaisons d'éléments. Le vérifier, puis montrer comment obtenir le maximum et le second maximum en environ comparaisons, par la méthode du tournoi.

Démonstration (Solution)

Compte de la version du cours : l'initialisation coûte une comparaison ; chacun des tours coûte une comparaison si , deux sinon. Le pire cas — par exemple un tableau où presque aucun élément ne bat — donne comparaisons.

Le tournoi : faire s'affronter les éléments deux à deux comme dans une coupe — matchs désignent le champion . Observation clé : le second maximum a nécessairement perdu contre le champion (sinon, qui l'aurait éliminé ?). Or le champion n'a disputé que matchs (un par tour de la coupe) : il suffit de chercher le maximum parmi ses victimes, soit comparaisons de plus. Total : , contre . Pour : environ comparaisons au lieu de — presque deux fois moins. (Les deux algorithmes restent : l'asymptotique ne voit pas la différence, mais le compte exact des comparaisons, lui, se moque des constantes — les deux niveaux d'analyse coexistent, et le programme demande le premier.)

Exercice 9 : Les anagrammes, ou la clé canonique

Deux mots sont des anagrammes s'ils contiennent les mêmes lettres avec les mêmes multiplicités (chien / niche). Écrire sont_anagrammes(u, v) en , puis groupes_anagrammes(mots) qui partitionne une liste de mots en groupes d'anagrammes.

Démonstration (Solution)

Deux mots sont anagrammes si et seulement si leurs dictionnaires de fréquences coïncident :


def sont_anagrammes(u: str, v: str) -> bool:
    return frequences(u) == frequences(v)      # exercice 3 ; comparaison de dicts

Pour grouper, il faut une clé canonique commune à tous les anagrammes d'un même groupe — le mot trié fait l'affaire :


def groupes_anagrammes(mots: list) -> list:
    groupes = {}
    for m in mots:
        cle = "".join(sorted(m))        # "chien" -> "cehin", "niche" -> "cehin"
        if cle not in groupes:
            groupes[cle] = []
        groupes[cle].append(m)
    return list(groupes.values())

g = groupes_anagrammes(["chien", "niche", "art", "rat", "tour"])
# [['chien', 'niche'], ['art', 'rat'], ['tour']]

sont_anagrammes est en : deux comptages linéaires et une comparaison de dictionnaires (linéaire en leur taille). (L'idée de la clé canonique — représenter toute une classe d'équivalence par un représentant calculable — resservira sans cesse : c'est elle qui transforme « être équivalents » en « avoir la même clé », testable en par dictionnaire.)

Exercice 10 : Le pire cas n'est pas la moyenne

On considère la recherche séquentielle d'un élément présent, en supposant sa position uniformément distribuée parmi les cases. Calculer le nombre moyen de tours de boucle, comparer au pire cas, et expliquer pourquoi le programme d'informatique retient néanmoins le cas le pire.

Démonstration (Solution)

Si l'élément est en position (indices de à ), la boucle fait tours. La position étant uniforme :

En moyenne, on lit donc la moitié du tableau — deux fois mieux que le pire cas , mais toujours : la classe asymptotique ne change pas. Le programme retient le pire cas pour trois raisons. D'abord, c'est une garantie : « au plus tours » est vrai pour toute entrée, quand la moyenne suppose un modèle probabiliste (ici l'uniformité — qui la justifie ?). Ensuite, les entrées réelles sont rarement uniformes : dans bien des applications, le cas défavorable est précisément le plus fréquent (chercher un mot absent d'un index, par exemple, coûte toujours le pire cas ). Enfin, le pire cas se calcule par un simple maximum, quand la moyenne exige de probabiliser l'espace des entrées — un outil que le cours de mathématiques ne fournira qu'en fin d'année. (L'analyse en moyenne n'est pas au programme, mais savoir qu'elle existe évite un contresens : « linéaire dans le cas le pire » ne signifie pas « toujours aussi lent ».)

Synthèse du chapitre (à retenir)
  • Coût : nombre d'opérations élémentaires, fonction de la taille de l'entrée ; le programme retient le cas le pire sur les entrées de taille — une garantie.
  • Notation : si à partir d'un rang ; on jette constantes et termes d'ordre inférieur () pour ne garder que la nature de l'algorithme, indépendante de la machine.
  • Coût constant : accès t[i], len, append, opérations arithmétiques. Coût linéaire : nombre borné de parcours. Piège : v in liste est un parcours caché () — dans une boucle, il rend l'algorithme quadratique.
  • Recherche séquentielle : premier indice de , invariant « absent du préfixe lu » ; pire cas (absent ou en dernière position) : ; sans structure supplémentaire, on ne peut pas faire mieux.
  • Maximum : comparaisons, optimal ; second maximum en un passage avec deux champions (l'ancien maximum descend, ne disparaît pas) ; variante tournoi comparaisons.
  • Dictionnaire (boîte noire) : clés arbitraires valeurs ; insertion, lecture, in, suppression en admis ; trois réflexes — comptage (c[x] = c.get(x, 0) + 1), mémoire du déjà-vu, table d'association (et son inverse, avec listes en cas de collisions).
  • Le choix de la structure est le choix de la complexité : remplacer un parcours répété par un accès dictionnaire fait passer de à (doublons, deux-sommes, majoritaire, anagrammes par clé canonique).

2.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. Coûts et notation

B. Parcours de tableaux

C. Dictionnaires

D. Études et démonstrations

Continuer sur Adloun : animation, QCM, fiches, exercices