Aller au contenu

Accueil › MPSI › Informatique

MPSI · Informatique · semaine 1

Bases de Python et recherche séquentielle

Éléments exigibles du langage Python, types de base, listes, chaînes, dictionnaires, structures de contrôle et fonctions. Recherche séquentielle dans un tableau, maximum et second maximum, comptage à l'aide d'un dictionnaire, coût constant et coût linéaire.

Sommaire

Le cours

Résumé en cinq points

  1. Le programme limite Python à un noyau précis : typage dynamique, indentation significative, portée lexicale, appel par valeur, types int, float, bool, chaînes, tuples, listes et dictionnaires.
  2. Les listes sont mutables et une affectation ne les copie pas. Une tranche fait une copie superficielle, qui partage encore les sous-listes.
  3. La recherche séquentielle parcourt le tableau case par case. Son coût est linéaire dans le pire cas, et le return du cas négatif se place après la boucle.
  4. Maximum et second maximum se calculent en un seul parcours, justifiés par un invariant : « les variables contiennent les plus grandes valeurs déjà vues ».
  5. Un dictionnaire, utilisé en boîte noire, offre accès et insertion en coût constant en pratique. Il transforme un comptage quadratique en comptage linéaire.
Pourquoi ce chapitre

Tout le programme d'informatique repose sur un petit noyau de Python, volontairement limité. Le connaître exactement évite deux pertes de points classiques aux concours :

  • utiliser une fonction hors programme sans pouvoir l'expliquer ;
  • se tromper sur ce que fait réellement une instruction, par exemple une copie de liste.

La recherche séquentielle est l'algorithme le plus simple : parcourir un tableau case par case. On s'en sert pour poser les bons réflexes :

  • écrire une fonction propre ;
  • dire ce qu'elle renvoie dans tous les cas ;
  • estimer combien d'opérations elle coûte.

Ces réflexes servent ensuite dans chaque chapitre.

1Objectifs du premier semestre

Le programme fixe cinq objectifs aux travaux pratiques du premier semestre.

  • Consolider la programmation en Python commencée au lycée.
  • Mettre en place un environnement de travail : un éditeur, un interpréteur, l'exécution d'un fichier et l'usage de la console.
  • Mettre en place une discipline de programmation : spécification précise des fonctions, annotations et commentaires, jeux de tests.
  • Introduire les premiers éléments de complexité. On ne présente que l'estimation asymptotique du coût dans le cas le pire.
  • Introduire des outils de validation : variants et invariants.

L'ordre des thèmes du semestre n'est pas imposé, et aucune connaissance sur les modules rencontrés en travaux pratiques n'est exigible.

2Traits généraux du langage Python

2.1Typage dynamique

Définition 2.1Typage dynamique

Le typage dynamique consiste pour l'interpréteur à déterminer le type d'une valeur à la volée, au moment de l'exécution.

On n'écrit donc pas de déclaration de type, et une même variable peut désigner successivement un entier puis une chaîne. La contrepartie est qu'une erreur de type n'apparaît qu'à l'exécution de la ligne fautive. D'où l'intérêt, demandé par le programme, de préciser la signature de chaque fonction en commentaire : types attendus en entrée et type renvoyé.

2.2Principe d'indentation

En Python, les blocs d'instructions (corps d'une boucle, d'une fonction ou d'une conditionnelle) sont délimités par l'indentation, conventionnellement quatre espaces, après une ligne terminée par deux-points. Changer l'indentation change le sens du programme.

Attention

Une instruction return décalée d'un cran vers la gauche sort de la boucle. C'est une source d'erreurs fréquente dans les recherches séquentielles.

2.3Portée lexicale

Quand une expression à l'intérieur d'une fonction fait référence à une variable, Python cherche d'abord la valeur définie à l'intérieur de la fonction, puis, à défaut, la valeur dans l'espace global du module. Une variable affectée dans une fonction est locale à cette fonction. Elle disparaît à la fin de l'appel et ne modifie pas une variable globale de même nom.

2.4Appel de fonction par valeur

L'exécution de f(x) évalue d'abord l'argument x, puis exécute le corps de f avec la valeur calculée.

Attention

Pour une liste, la valeur transmise désigne la liste elle-même et non une copie. Une fonction qui modifie une case de la liste reçue modifie donc la liste de l'appelant. Réaffecter le paramètre, en revanche, ne change rien pour l'appelant.

3Types de base

3.1Entiers et flottants

Sur les entiers, type int, les opérations exigibles sont :

  • l'addition, la soustraction, la multiplication ;
  • la division entière // ;
  • l'exponentiation ** ;
  • le reste %, avec des opérandes positifs.

Par exemple 17 // 5 vaut 33 et 17 % 5 vaut 22. Les entiers de Python ne débordent pas : leur taille s'adapte.

Sur les flottants, type float, on dispose de +, -, *, / et **. La division / renvoie toujours un flottant : 6 / 3 vaut 2,02{,}0.

Attention

Les flottants sont des approximations : 0.1 + 0.2 == 0.3 est faux. On y reviendra au chapitre sur la représentation des nombres, mais le réflexe est dès maintenant de ne jamais tester l'égalité de deux flottants.

3.2Booléens et comparaisons

Le type bool a deux valeurs, True et False, et trois opérations : not, and, or. Les comparaisons exigibles sont ==, !=, <, >, <=, >=.

Propriété 3.1Opérateurs paresseux

Les opérateurs and et or sont paresseux. Dans a and b, si a est faux, b n'est pas évalué.

Cela permet d'écrire i < len(t) and t[i] != x sans risque de sortir du tableau.

4Types structurés

4.1Chaînes et tuples, structures indicées immuables

Les chaînes de caractères et les tuples sont des structures indicées immuables. On peut :

  • y accéder par indice positif valide, de 00 à len(s)−1\texttt{len(s)} - 1 ;
  • les concaténer avec + ;
  • les répéter avec * ;
  • en extraire une tranche avec s[i:j], qui contient les éléments d'indices ii inclus à jj exclu.

Immuable signifie qu'on ne peut pas modifier une case. Pour « changer » une chaîne, on en construit une nouvelle.

4.2Listes

Les listes sont des tableaux dynamiques mutables. Les constructions exigibles sont la compréhension [e for x in s], la répétition [e] * n et les append successifs. On dispose de :

  • len ;
  • l'accès et la modification par indice positif valide ;
  • la concaténation + ;
  • l'extraction de tranche ;
  • la copie ;
  • pop en dernière position.

Le point délicat est la copie.

Attention

  • L'affectation m = t ne copie rien : m et t désignent la même liste, et modifier m[0] modifie t[0].
  • Une tranche t[:] fabrique une nouvelle liste, mais cette copie est superficielle. Pour une liste de listes, les sous-listes restent partagées.
  • [[0] * 3] * 2 est un piège : les deux lignes sont la même liste. [[0] * 3 for i in range(2)] crée deux lignes distinctes.

4.3Dictionnaires

Un dictionnaire associe des valeurs à des clés. Les opérations sont :

  • création : {c1: v1, c2: v2} ;
  • accès : d[c] ;
  • insertion ou modification : d[c] = v ;
  • test de présence : c in d ;
  • nombre d'associations : len(d) ;
  • copie : d.copy().

Au premier semestre, le programme demande de les utiliser en boîte noire, sans connaître leur fonctionnement interne. L'accès, l'insertion et le test de présence sont admis de coût constant en pratique, sans garantie dans le pire cas. Le principe du hachage, qui explique ce coût et les restrictions sur les clés, est vu en deuxième année.

Remarque

Les clés d'un dictionnaire ne peuvent pas être des objets quelconques (une liste n'est pas utilisable comme clé) : la raison est vue en deuxième année.

5Structures de contrôle

5.1Affectation et dépaquetage de tuples

L'affectation se fait avec =. Le dépaquetage permet d'affecter plusieurs variables d'un coup : a, b = b, a échange deux valeurs, et c'est une façon sûre d'écrire une mise à jour simultanée.

5.2Instruction conditionnelle

On utilise if, elif, else. Les branches sont testées dans l'ordre et une seule est exécutée. Un elif n'est examiné que si toutes les conditions précédentes étaient fausses.

5.3Boucles while et for

La boucle while répète son corps tant que sa condition est vraie. Elle convient quand on ne connaît pas à l'avance le nombre de tours.

La boucle for parcourt :

  • range(a, b), c'est-à-dire les entiers de aa inclus à bb exclu ;
  • une chaîne, un tuple ou une liste ;
  • un dictionnaire, à travers ses méthodes keys et items.

Les instructions break et return dans le corps d'une boucle en sortent immédiatement. La forme else d'une boucle n'est pas au programme.

5.4Définition d'une fonction

def f(p1, …, pn) introduit une fonction, return termine son exécution et renvoie une valeur. Une fonction sans return renvoie la valeur spéciale None.

On sépare toujours le calcul, dans une fonction qui renvoie un résultat, de l'affichage avec print. Celui-ci s'utilise sans paramètre facultatif. Les commentaires commencent par #, et le programme demande qu'ils éclairent le code sans le paraphraser.

6Recherche séquentielle dans un tableau unidimensionnel

6.1Recherche d'un élément

Problème : étant donné une liste t et une valeur x, dire si x apparaît dans t. L'algorithme séquentiel parcourt les cases dans l'ordre et s'arrête dès qu'il trouve x.

def appartient(t, x):
    # t : list, x : valeur quelconque.
    # Renvoie True si x est un élément de t.
    for y in t:
        if y == x:
            return True
    return False

Attention

Le return False est après la boucle, au niveau de la définition. Placé dans la boucle, il ferait conclure dès la première case différente de x.

Variante utile : renvoyer l'indice de la première occurrence, ou −1-1 si x est absent, avec une boucle sur range(len(t)), qui va de 00 à n−1n - 1.

def indice_for(t, x):
    # renvoie le plus petit i tel que t[i] == x,
    # ou -1 si x n'est pas dans t
    for i in range(len(t)):
        if t[i] == x:
            return i
    return -1

Version avec une boucle while, qui renvoie l'indice. On avance tant qu'on n'a ni dépassé la fin ni trouvé x.

def indice(t, x):
    # renvoie le plus petit i tel que t[i] == x,
    # ou -1 si x n'est pas dans t
    i = 0
    while i < len(t) and t[i] != x:
        i = i + 1
    if i < len(t):
        return i
    return -1

Attention

L'ordre des deux conditions compte. Grâce au caractère paresseux de and, t[i] n'est jamais lu quand ii vaut len(t)\texttt{len(t)}. Écrites dans l'autre ordre, elles provoqueraient une erreur d'indice quand x est absent.

La terminaison se justifie par un variant : len(t)−i\texttt{len(t)} - i est un entier positif qui diminue de 11 à chaque tour.

Propriété 6.1Coût de la recherche d'un élément

Pour une liste de longueur nn :

  • dans le cas le pire (x absent ou en dernière position), on fait nn comparaisons : le coût est linéaire ;
  • dans le meilleur cas (x en première position), il est constant.

Le programme ne s'intéresse qu'au pire cas.

6.2Recherche du maximum et du second maximum

Maximum d'une liste non vide : on garde dans une variable m le plus grand élément vu jusqu'ici.

def maximum(t):
    # t : liste non vide de nombres.
    # Renvoie le plus grand élément.
    m = t[0]
    for x in t:
        if x > m:
            m = x
    return m

Propriété 6.2Invariant de boucle

Après le traitement des kk premiers éléments, m est le maximum de ces kk éléments.

L'invariant est vrai avant la boucle avec k=1k = 1, et chaque tour le conserve. Le premier tour retraite t[0] sans changer m. À la fin, k=nk = n, donc m est le maximum de t.

Le parcours complet fait nn comparaisons, dont n−1n - 1 sont utiles (on peut parcourir t[1:] pour n'en faire que n−1n - 1). Le coût est linéaire.

MéthodeSecond maximum

La convention est d'abord fixée : c'est la deuxième valeur de la liste triée par ordre décroissant, doublons compris. Le second maximum de [5, 5, 3] est donc 55.

  1. Initialiser deux variables m1 ⩾\geqslant m2 avec les deux premiers éléments rangés dans le bon ordre. Cela suppose au moins deux éléments.
  2. Pour chaque nouvel élément x, si x > m1, l'ancien m1 devient m2 et x devient m1.
  3. Sinon, si x > m2, x devient m2.

Un seul parcours suffit : le coût reste linéaire.

Attention

L'erreur classique est d'oublier de faire descendre l'ancien maximum en m2.

6.3Comptage des éléments à l'aide d'un dictionnaire

Pour compter les occurrences de chaque valeur d'une liste, on parcourt la liste et on tient un dictionnaire dont les clés sont les valeurs rencontrées et les valeurs leurs effectifs.

def effectifs(t):
    d = {}
    for x in t:
        if x in d:
            d[x] = d[x] + 1
        else:
            d[x] = 1
    return d

Chaque tour de boucle fait un test de présence et une insertion, de coût constant en pratique. Le coût total est donc linéaire.

Sans dictionnaire, compter les occurrences de chaque valeur en appelant une recherche séquentielle pour chacune coûterait de l'ordre de n2n^2 : un coût quadratique, étudié avec les boucles imbriquées dans le thème suivant. C'est le premier exemple où le choix de la structure de données change l'ordre de grandeur du coût.

6.4Coût constant et coût linéaire

Définition 6.3Coût

Le coût d'un algorithme est le nombre d'opérations élémentaires qu'il effectue, exprimé en fonction de la taille nn de l'entrée, dans le cas le pire et en ordre de grandeur.

  • Un coût est constant s'il est borné indépendamment de nn.
  • Il est linéaire s'il est de l'ordre de nn.

Coûts à connaître pour les listes de Python :

  • constant : len(t), l'accès et la modification t[i], append, pop en dernière position ;
  • linéaire en la taille concernée : x in t, la copie, la concaténation, l'extraction d'une tranche.

Pour les dictionnaires, accès, insertion et test de présence sont constants en pratique.

Exemple 6.4Détecter deux éléments égaux

  • Méthode naïve : comparer chaque élément à tous les suivants, soit de l'ordre de n2/2n^2/2 comparaisons, coût quadratique.
  • Méthode avec un dictionnaire : parcourir la liste une seule fois en enregistrant chaque valeur vue, et s'arrêter dès qu'une valeur est déjà présente, coût linéaire.

Pour une liste d'un million d'éléments, la première méthode fait de l'ordre de cinq cents milliards de comparaisons, la seconde un million d'opérations.

Attention

Un x in t dans une boucle de nn tours n'est pas une opération élémentaire. Il cache une boucle, et le coût total devient quadratique. Lire le coût d'un programme, c'est repérer ces boucles cachées.

Méthodes

MéthodeRéflexes du chapitre

  1. Écrire la signature et la spécification en commentaire avant le corps de la fonction : types d'entrée, type de sortie, cas particuliers comme la liste vide.
  2. Pour une recherche, se demander où placer le return du cas « non trouvé ». Presque toujours après la boucle.
  3. Pour un calcul de maximum ou de comptage, énoncer l'invariant en une phrase : « m est le maximum des éléments déjà vus ».
  4. Tester à la main sur trois cas : un cas général, un cas limite comme la liste à un élément, et un cas où la réponse est négative.
  5. Pour estimer un coût, compter les tours de boucle et repérer les opérations qui cachent une boucle : in sur une liste, copie, concaténation, tranche.
  6. N'utiliser que les éléments de l'annexe. Toute autre fonction (max, sorted, index) doit être accompagnée de sa documentation dans un sujet, et on ne peut pas s'en servir pour éviter d'écrire l'algorithme demandé.

Ce qu'il faut savoir refaire en khôlle

  1. Écrire sans erreur une fonction de recherche d'un élément, en version booléenne puis en version qui renvoie un indice, et justifier la place de chaque return.
  2. Écrire la recherche du maximum avec son invariant, puis celle du second maximum en un seul parcours, en précisant la convention sur les doublons.
  3. Compter les occurrences des éléments d'une liste avec un dictionnaire, et comparer au coût d'une méthode sans dictionnaire.
  4. Expliquer ce qu'affiche un programme qui fait m = t puis modifie m, puis le même avec m = t[:], et le cas d'une liste de listes.
  5. Donner le coût dans le pire cas d'un petit programme en repérant les opérations de coût linéaire.
  6. Expliquer la portée d'une variable affectée dans une fonction et l'effet d'une fonction qui modifie une liste reçue en paramètre.

Méthodes et exercices

Les résultats à connaître

En informatique, les « résultats » sont des algorithmes avec leur spécification et leur coût, et des faits de langage qu'il faut savoir expliquer. Le programme range la recherche d'un élément, le maximum, le second maximum et le comptage parmi les exemples d'activités. Il ne les exige pas comme connaissances, mais tout sujet suppose qu'on sache les réécrire en quelques minutes.

  • Recherche d'un élément. Entrée : une liste t de longueur n et une valeur x. Sortie : True si x est égal à l'un des t[i], False sinon. Coût linéaire dans le pire cas, au plus n comparaisons. À savoir réécrire et justifier.
  • Première occurrence. Même entrée, sortie : le plus petit i tel que t[i] == x, ou −1. Terminaison par le variant n − i. À savoir réécrire.
  • Maximum. Hypothèse : liste non vide de valeurs comparables. Coût linéaire, correction par invariant. À savoir réécrire.
  • Second maximum. Hypothèse : au moins deux éléments, convention sur les doublons fixée à l'avance. Un seul parcours, coût linéaire. À savoir réécrire.
  • Comptage par dictionnaire. Coût linéaire, en admettant que le test de présence, l'accès et l'insertion dans un dictionnaire ont un coût constant en pratique. Ce coût est admis, sa justification par le hachage relève de la deuxième année.
  • Coûts des opérations sur les listes : constants pour len, l'accès et la modification par indice, append et pop en fin. Linéaires pour in, la copie, la concaténation et la tranche. À connaître, sans démonstration.
  • Terminaison par variant. Si une quantité entière positive décroît strictement à chaque tour, la boucle while s'arrête. Outil de validation exigible.
  • Correction par invariant. Une propriété vraie avant la boucle et conservée par chaque tour est vraie à la sortie. Outil exigible.
  • Faits de langage exigibles : and et or sont paresseux, une affectation ne copie pas une liste, une tranche fait une copie superficielle, une fonction qui modifie une case de la liste reçue modifie la liste de l'appelant, une variable affectée dans une fonction est locale.
  • Hors programme ou sous condition : l'étude du coût moyen et du meilleur cas, la forme else d'une boucle, les indices négatifs. Les fonctions max, sorted, index et count ne sont utilisables que si le sujet les documente.

Clés de démonstration

Recherche booléenne avec une boucle for. Invariant : au début du tour qui examine t[i], x n'est aucun des éléments t[0], …, t[i − 1]. Si la fonction renvoie True, c'est qu'un test y == x a réussi, donc la réponse est juste. Si la boucle se termine, l'invariant avec i = n dit que x n'est nulle part. Astuce : l'invariant décrit la partie déjà parcourue, jamais la partie restante.

Recherche avec while. Le variant n − i est un entier positif qui diminue de 1 à chaque tour, d'où la terminaison. À la sortie, la condition est fausse. Par De Morgan, on a donc i ≥ n ou t[i] == x. Dans le premier cas, x est absent. Dans le second, l'invariant garantit que i est la première occurrence. Astuce : lire la sortie d'une boucle while comme la négation de sa condition.

Maximum. Invariant : m est le maximum des éléments déjà vus. Astuce : initialiser avec t[0], jamais avec 0, sinon l'invariant est faux dès le départ pour une liste de négatifs.

Second maximum. Invariant : m1 et m2 sont les deux premiers termes, dans l'ordre décroissant, des éléments vus. Pour la conservation, on distingue trois cas, x > m1, puis m2 < x ≤ m1, puis x ≤ m2. Astuce : dans le premier cas, l'ancien m1 descend en m2, c'est l'étape qu'on oublie.

Comptage. Invariant : après k tours, les clés de d sont exactement les valeurs présentes parmi les k premiers éléments, et chacune est associée à son nombre d'occurrences parmi eux. Astuce : l'invariant a deux moitiés, « les bonnes clés » et « les bons effectifs », et le test x in d sert la première.

Coût linéaire. La boucle fait n tours, et chaque tour coûte au plus une constante c. Le coût total est donc au plus c × n. Astuce : vérifier ligne par ligne que le corps ne contient aucune opération cachée de coût linéaire.

Exemples et contre-exemples à connaître

  1. Un maximum initialisé à 0 renvoie 0 pour [−3, −1]. Le cas « tous négatifs » est le test qui démasque cette erreur.
  2. Un return False placé dans la boucle, après le if, fait répondre False pour [1, 2] et x = 2. La première case différente de x suffit à conclure à tort.
  3. while t[i] != x and i < len(t) provoque une erreur d'indice quand x est absent. L'ordre des conditions n'est pas décoratif.
  4. Pour la liste [5, 5, 3], le second maximum vaut 5 avec doublons et 3 au sens strict. Sans convention annoncée, la question n'a pas de réponse.
  5. maximum([]) provoque une erreur d'indice sur t[0]. La spécification doit exclure la liste vide ou la traiter.
  6. Dans une fonction, t[0] = 0 modifie la liste de l'appelant, mais t = [0] ne la modifie pas. La première instruction agit sur la liste, la seconde change seulement ce que désigne le nom local.
  7. Lire d[c] quand c n'est pas une clé provoque une erreur. C'est pour cela que le comptage teste c in d avant de lire d[c].
  8. Une fonction qui contient n = n + 1 alors que n n'est défini que globalement provoque une erreur à l'exécution. L'affectation rend n local dans toute la fonction, et sa lecture précède alors toute affectation.
  9. Une boucle qui contient t = t + [x] a un coût quadratique, alors que t.append(x) donne un coût linéaire. La concaténation recopie toute la liste à chaque tour.

Techniques du chapitre

Technique 1 : écrire la spécification avant le code. Mini-exercice : compter les occurrences de x dans t. On écrit la signature en commentaire, puis on vérifie le cas de la liste vide.

def occurrences(t, x):
    # t : list, x : valeur. Renvoie le nombre d'indices i tels que t[i] == x
    c = 0
    for y in t:
        if y == x:
            c = c + 1
    return c

Pour la liste vide, la boucle ne tourne pas et la fonction renvoie 0. Le coût est linéaire.

Technique 2 : choisir entre for et while. On prend for quand on parcourt tout, while quand on s'arrête sur une condition. Mini-exercice : trouver le premier indice d'un nombre négatif. On écrit while i < len(t) and t[i] >= 0: suivi de i = i + 1, puis on renvoie i si i < len(t), et −1 sinon.

Technique 3 : vérifier un invariant en trois temps, initialisation, conservation, conclusion. Mini-exercice : la somme s des éléments. Invariant : après k tours, s = t[0] + … + t[k − 1]. Il est vrai pour k = 0 avec s = 0. Le tour suivant ajoute t[k]. À la fin, k = n.

Technique 4 : exhiber un variant pour une boucle while. Mini-exercice : compter les chiffres d'un entier n ≥ 1 avec while n > 0: suivi de n = n // 10. Le variant est n lui-même. C'est un entier positif, et n // 10 < n dès que n ≥ 1. La boucle termine donc.

Technique 5 : remplacer une recherche répétée par un dictionnaire. Mini-exercice : trouver le premier caractère d'une chaîne qui apparaît une deuxième fois. On parcourt la chaîne avec un dictionnaire vus. Dès que c in vus, on renvoie c. Sinon, on fait vus[c] = True. Le coût est linéaire au lieu de quadratique.

Technique 6 : lire le coût en repérant les boucles cachées. Mini-exercice : for i in range(len(t)): suivi de if t[i] in t[i + 1:]:. La tranche et le test in coûtent chacun de l'ordre de n − i. La somme donne un coût de l'ordre de n², donc quadratique.

Technique 7 : tester à la main sur trois cas. Mini-exercice : tester un maximum sur [3, 1, 2], sur [4] et sur [−5, −2]. Le troisième test révèle toute initialisation à 0.

Exercices classiques corrigés

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

Exercice 1. Dernière occurrence

Classiques de khôlle et concours · Khôlle

Écrire une fonction derniere(t, x) qui renvoie le plus grand indice i tel que t[i] == x, ou −1 si x n'apparaît pas dans t.

Technique parcours complet avec mémorisation, puis parcours à rebours.

Voir le corrigé
  1. Idée : on ne peut pas s'arrêter à la première rencontre. On parcourt tout et on retient le dernier indice trouvé.
def derniere(t, x):
    # t : list. Renvoie le plus grand i tel que t[i] == x, ou -1
    r = -1
    for i in range(len(t)):
        if t[i] == x:
            r = i
    return r
  1. Invariant : au début du tour i, r est le plus grand j < i tel que t[j] == x, ou −1 s'il n'y en a pas. À la fin, i vaut n, ce qui donne la spécification.
  2. Variante qui s'arrête plus tôt. On part de i = len(t) − 1, et on décrémente i tant que i >= 0 et t[i] != x. Puis on renvoie i, qui vaut −1 exactement quand x est absent.
  3. Les deux versions ont un coût linéaire dans le pire cas.

piège : Placer un return i dans la boucle, ce qui renvoie la première occurrence.

Exercice 2. Prévoir un affichage

Classiques de khôlle et concours · Khôlle

Qu'affiche le programme suivant ? Justifier.

def f(u):
    u[0] = 7
    u = [0, 0]
    u[1] = 9

t = [1, 2]
m = t
f(m)
c = t[:]
c[1] = 5
print(t, m, c)

Technique distinguer modifier une liste et réaffecter un nom.

Voir le corrigé
  1. m = t ne copie rien, m et t désignent la même liste.
  2. L'appel f(m) transmet cette liste. L'instruction u[0] = 7 la modifie, t vaut donc [7, 2].
  3. u = [0, 0] fait désigner à u une nouvelle liste locale. u[1] = 9 modifie cette nouvelle liste, pas t.
  4. c = t[:] est une copie. c[1] = 5 ne touche pas t.
  5. Affichage : [7, 2] [7, 2] [7, 5].

piège : Croire que u = [0, 0] remet t à zéro, ou que m est une copie de t.

Exercice 3. Indice du minimum

Classiques de khôlle et concours · Khôlle

Écrire une fonction qui renvoie le plus petit indice où le minimum d'une liste non vide est atteint.

Technique invariant sur l'indice du meilleur candidat.

Voir le corrigé
  1. Idée : on retient l'indice k du plus petit élément vu, plutôt que sa valeur.
def indice_min(t):
    # t : liste non vide de nombres
    k = 0
    for i in range(1, len(t)):
        if t[i] < t[k]:
            k = i
    return k
  1. Invariant : au début du tour i, t[k] est le minimum de t[0], …, t[i − 1], et k est le plus petit indice qui l'atteint.
  2. L'inégalité stricte préserve la seconde moitié de l'invariant. En cas d'égalité, k ne bouge pas.
  3. Coût : n − 1 comparaisons, linéaire.

piège : Écrire <= au lieu de <. On obtient alors la dernière position du minimum.

Exercice 4. Second maximum au sens strict

Classiques de khôlle et concours · Khôlle

Pour une liste non vide, renvoyer la plus grande valeur strictement inférieure au maximum, ou None si toutes les valeurs sont égales. Un seul parcours est demandé.

Technique deux variables et un booléen qui indique si m2 a un sens.

Voir le corrigé
  1. Idée : on maintient m1, le maximum des éléments vus, et m2, la meilleure valeur strictement inférieure à m1, si elle existe.
def second_strict(t):
    m1 = t[0]
    m2 = t[0]         # sans signification tant que trouve est faux
    trouve = False
    for x in t:
        if x > m1:
            m2, m1 = m1, x
            trouve = True
        elif x < m1 and (not trouve or x > m2):
            m2 = x
            trouve = True
    if trouve:
        return m2
    return None
  1. Si x > m1, tous les éléments vus sont au plus l'ancien m1. L'ancien m1 devient donc la plus grande valeur strictement inférieure au nouveau maximum.
  2. Si x == m1, rien ne change. C'est ce qui écarte les doublons du maximum.
  3. Tests : [5, 5, 3] donne 3, [4, 4] donne None, [2, 7] donne 2. Le coût est linéaire.

piège : Oublier la condition x < m1 dans le elif. Un doublon du maximum deviendrait alors m2.

Exercice 5. Élément majoritaire

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

Un élément d'une liste de longueur n est majoritaire s'il y apparaît strictement plus de n/2 fois. Montrer qu'il y en a au plus un, puis écrire une fonction de coût linéaire qui le renvoie, ou qui renvoie None s'il n'existe pas.

Technique comptage par dictionnaire, puis parcours des items.

Voir le corrigé
  1. Unicité : deux valeurs distinctes majoritaires totaliseraient strictement plus de n/2 + n/2 = n occurrences, ce qui est impossible.
  2. On calcule les effectifs, puis on cherche une clé d'effectif suffisant.
def majoritaire(t):
    d = {}
    for x in t:
        if x in d:
            d[x] = d[x] + 1
        else:
            d[x] = 1
    for c, v in d.items():
        if 2 * v > len(t):
            return c
    return None
  1. Le test 2 × v > n évite toute division. La première boucle fait n tours de coût constant. La seconde parcourt au plus n clés. Le coût est linéaire.
  2. Sans dictionnaire, compter chaque élément par une recherche séquentielle coûterait de l'ordre de n².

piège : Écrire ≥ au lieu de >. Pour [1, 2], chaque valeur apparaît exactement n/2 fois et aucune n'est majoritaire.

Exercice 6. Deux éléments de somme donnée

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

Soit t une liste d'entiers et s un entier. Déterminer s'il existe deux indices i < j tels que t[i] + t[j] = s. Donner le coût d'une méthode naïve, puis une méthode linéaire.

Technique chercher le complément parmi les valeurs déjà vues.

Voir le corrigé
  1. Méthode naïve : on teste toutes les paires i < j. Cela fait n(n − 1)/2 tests, coût quadratique.
  2. Idée linéaire : en arrivant sur x, on se demande si s − x est déjà apparu avant. Si oui, on a trouvé une paire.
def somme_deux(t, s):
    vus = {}
    for x in t:
        if s - x in vus:
            return True
        vus[x] = True
    return False
  1. Invariant : au début du tour d'indice j, les clés de vus sont les valeurs t[0], …, t[j − 1], et aucune paire d'indices inférieurs à j ne convient.
  2. Chaque tour a un coût constant, le coût total est linéaire.

piège : Insérer x avant de tester s − x. Pour t = [3] et s = 6, la fonction répondrait True en appariant 3 avec lui-même.

Exercice 7. Coût de deux fonctions

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

Donner le coût dans le pire cas de chacune des fonctions suivantes, puis proposer une version linéaire qui renvoie le même résultat.

def sans_doublons(t):
    r = []
    for x in t:
        if x not in r:
            r.append(x)
    return r

def renverse(t):
    r = []
    for x in t:
        r = [x] + r
    return r

Technique sommer le coût de chaque tour.

Voir le corrigé
  1. sans_doublons : au tour k, r contient au plus k éléments, et le test x not in r coûte jusqu'à k. Dans le pire cas, tous les éléments sont distincts, et le total vaut 0 + 1 + … + (n − 1) = n(n − 1)/2. Le coût est quadratique.
  2. Version linéaire : on garde la liste r pour conserver l'ordre, et on ajoute un dictionnaire vus. On teste x in vus, de coût constant, avant de faire r.append(x) et vus[x] = True.
  3. renverse : au tour k, [x] + r fabrique une nouvelle liste de longueur k + 1. Le total est de l'ordre de n²/2, donc quadratique.
  4. Version linéaire : on parcourt i dans range(len(t)) et on fait r.append(t[len(t) − 1 − i]).

piège : Compter une ligne comme une opération. Une seule ligne peut contenir une boucle cachée.

Exercice 8. Plus long plateau

Approfondissement · D'après un écrit Mines-Ponts

Pour une liste non vide, renvoyer la longueur du plus long bloc de cases consécutives de même valeur. Par exemple, [1, 1, 2, 2, 2, 1] donne 3. Un seul parcours est demandé.

Technique deux compteurs, le bloc en cours et le meilleur bloc.

Voir le corrigé
  1. Idée : courant est la longueur du bloc qui se termine à la case i, et meilleur est la plus grande longueur rencontrée.
def plus_long_plateau(t):
    courant = 1
    meilleur = 1
    for i in range(1, len(t)):
        if t[i] == t[i - 1]:
            courant = courant + 1
        else:
            courant = 1
        if courant > meilleur:
            meilleur = courant
    return meilleur
  1. Invariant : après le tour i, courant est la longueur du bloc qui finit en i, et meilleur est le maximum des longueurs de blocs contenus dans t[0], …, t[i].
  2. Coût : n − 1 tours de coût constant, donc linéaire.

piège : Mettre à jour meilleur seulement quand un bloc se termine, dans la branche else. Le dernier bloc est alors ignoré, et [1, 2, 2] donne 1.

Exercice 9. Plus petit entier naturel absent

Approfondissement · Type oral Centrale

Soit t une liste de n entiers naturels, éventuellement très grands. Renvoyer le plus petit entier naturel qui n'apparaît pas dans t, avec un coût linéaire en n.

Technique borner la réponse, puis utiliser un tableau de booléens.

Voir le corrigé
  1. Observation : n valeurs ne peuvent pas couvrir les n + 1 entiers 0, 1, …, n. La réponse est donc au plus n, et les valeurs strictement supérieures à n sont inutiles.
  2. On marque dans une liste de n + 1 booléens les valeurs présentes qui sont au plus n.
def plus_petit_absent(t):
    n = len(t)
    present = [False] * (n + 1)
    for x in t:
        if x <= n:
            present[x] = True
    k = 0
    while present[k]:
        k = k + 1
    return k
  1. Terminaison sans erreur d'indice : au plus n cases valent True, donc il existe un premier indice p ≤ n tel que present[p] est False. Le variant p − k est un entier positif qui décroît de 1 à chaque tour, et la boucle s'arrête en k = p.
  2. Coût : création de la liste en n + 1, n tours, puis au plus n + 1 tours de while. Le coût est linéaire.
  3. La méthode naïve qui teste 0, 1, 2 et ainsi de suite avec k in t coûte de l'ordre de n².

piège : Dimensionner le tableau avec la plus grande valeur de t. Le coût dépend alors des valeurs et non de n.

Erreurs fréquentes

  1. Placer le return du cas négatif dans la boucle, ou mal indenter un return. La fonction conclut dès la première case, et tout l'algorithme est faux.
  2. Initialiser un maximum ou un minimum avec une constante, ou oublier le cas de la liste vide dans la spécification. Le correcteur teste ces cas limites en premier.
  3. Confondre affectation et copie. m = t partage la liste, et une fonction qui modifie la liste reçue modifie celle de l'appelant.
  4. Annoncer un coût linéaire alors qu'un in sur une liste, une tranche ou une concaténation se trouve dans une boucle. Le coût réel est alors quadratique.
  5. Utiliser max, index, count ou sorted sans documentation, ou afficher avec print au lieu de renvoyer avec return. L'algorithme demandé n'est alors pas écrit, et la fonction ne renvoie rien d'utilisable.

Fiche de cours rédigée à partir du programme officiel d'informatique commune des filières MPSI, PCSI, PTSI, MP, PC, PSI et PT (2021), premier semestre, et de son annexe « Langage Python », qui liste limitativement les éléments du langage exigibles. Elle couvre les objectifs du premier semestre, les traits généraux de Python, les types de base et structurés, les structures de contrôle et les fonctions, puis le premier thème du semestre : la recherche séquentielle dans un tableau unidimensionnel, avec l'usage d'un dictionnaire en boîte noire et les notions de coût constant et de coût linéaire. La lecture de fichiers, les assertions et la dichotomie sont traitées dans les chapitres suivants. Les boucles imbriquées, la recherche d'un facteur dans un texte, le tri à bulles et le coût quadratique forment le thème suivant du semestre.