Adloun

La notion de liste

Cours complet · algorithmique et programmation (première), chapitre 2 · première, algorithmique et programmation

Travailler ce chapitre sur Adloun

Voici la nouveauté de l'année : la liste, structure qui rassemble plusieurs valeurs en un seul objet. Elle est partout en mathématiques — termes d'une suite, tableau de valeurs d'une fonction, série statistique, échantillon simulé — et son écriture en compréhension est le décalque exact de la notation des ensembles. Conformément au programme, on se limite aux listes, sans présenter d'autres collections.

2.1 Qu'est-ce qu'une liste ?

Définition 2.1Liste

Une liste est une suite finie et ordonnée de valeurs, écrites entre crochets et séparées par des virgules. Chaque élément est repéré par son indice, qui commence à :


L = [12, 8, 15, 10]
print(len(L))    # 4 : longueur de la liste
print(L[0])      # 12 : premier élément (indice 0 !)
print(L[3])      # 10 : dernier élément (indice len(L) - 1)
print(L[-1])     # 10 : indice négatif = on compte depuis la fin

Une liste est une rangée de cases numérotées à partir de : accéder à L[len(L)] provoque une IndexError.

iRemarqueListe ou ensemble ?

Contrairement à un ensemble (chapitre 1 du manuel de mathématiques), une liste tient compte de l'ordre et des répétitions : [1, 2, 2] et [2, 1, 2] sont deux listes différentes, de longueur . C'est la bonne structure pour une suite de valeurs ; l'analogie avec les ensembles se joue ailleurs — dans la génération en compréhension.

2.2 Générer une liste : trois façons

2.2.1 En extension

On énumère tous les éléments, comme un ensemble écrit en extension :


diviseurs_de_12 = [1, 2, 3, 4, 6, 12]
notes = [12.5, 9, 14, 11]
vide = []                      # la liste vide, prête à être remplie

2.2.2 Par ajouts successifs

On part de la liste vide et on ajoute au fil d'une boucle avec append — le mode de génération naturel pour les suites récurrentes, où chaque valeur dépend de la précédente :


L = []
u = 5
for i in range(10):
    L.append(u)          # ajoute u À LA FIN de L
    u = 0.8 * u + 4
# L contient les 10 premiers termes de la suite u_{n+1} = 0.8 u_n + 4

2.2.3 En compréhension

On décrit les éléments par une formule et un domaine, exactement comme un ensemble écrit en compréhension :

[n**2 for n in range(10)]


carres   = [n**2 for n in range(10)]        # [0, 1, 4, 9, ..., 81]
pairs    = [2*k for k in range(5)]          # [0, 2, 4, 6, 8]
images   = [x**2 - 3*x for x in range(-2, 4)]   # tableau de valeurs de f
Définition 2.2Compréhension avec condition

On peut filtrer le domaine par une condition, introduite par if :


multiples_de_3 = [n for n in range(30) if n % 3 == 0]
diviseurs = [d for d in range(1, 13) if 12 % d == 0]   # [1, 2, 3, 4, 6, 12]

C'est le décalque de : la condition est une proposition au sens du chapitre de logique, que l'on peut combiner avec and, or, not.

Exemple 2.3Conditions combinées : la logique en action

# multiples de 3 OU de 5 (le « ou » inclusif de la logique)
L1 = [n for n in range(20) if n % 3 == 0 or n % 5 == 0]
# [0, 3, 5, 6, 9, 10, 12, 15, 18]

# pairs ET non multiples de 4
L2 = [n for n in range(20) if n % 2 == 0 and not n % 4 == 0]
# [2, 6, 10, 14, 18]

Écrire la condition d'une compréhension, c'est traduire une proposition mathématique — l'occasion de réviser négation, « et », « ou ».

Méthode : Choisir son mode de génération

  • extension : peu d'éléments, connus explicitement (données d'un énoncé) ;
  • ajouts successifs : les éléments se calculent de proche en proche (suites récurrentes, accumulations) ;
  • compréhension : les éléments se calculent par une formule appliquée à un domaine, éventuellement filtré (suites explicites, tableaux de valeurs, sélections).

2.3 Manipuler une liste et ses indices

ImportantBoîte à outils des listes

L = [5, 3, 8, 3]
L[1] = 7           # modification par indice : [5, 7, 8, 3]
L.append(10)       # ajout en fin :            [5, 7, 8, 3, 10]
L.insert(1, 99)    # insertion à l'indice 1 :  [5, 99, 7, 8, 3, 10]
L.remove(3)        # supprime la PREMIÈRE occurrence de la valeur 3
x = L.pop()        # retire ET renvoie le dernier élément
del L[0]           # supprime l'élément d'indice 0
print(8 in L)      # True : test d'appartenance (l'analogue du "appartient")
print(L.count(7))  # nombre d'occurrences de 7
print(len(L), min(L), max(L), sum(L))   # longueur, extrêmes, somme
iRemarqueAttention : `remove` et `del` ne font pas la même chose

L.remove(v) supprime par valeur (la première occurrence de v) ; del L[i] supprime par indice. Après toute suppression ou insertion, les indices des éléments suivants sont décalés — source classique d'erreurs quand on modifie une liste en la parcourant.

Exemple 2.4Tranches

L[a:b] extrait la sous-liste des indices à (borne de fin exclue, comme range) :


L = [10, 11, 12, 13, 14, 15]
print(L[1:4])    # [11, 12, 13]
print(L[:3])     # [10, 11, 12]  (début omis = depuis le début)
print(L[3:])     # [13, 14, 15]  (fin omise = jusqu'au bout)

La borne de fin exclue se retient beaucoup mieux si l'on place les indices entre les cases plutôt que dessus :

2.4 Parcourir une liste

Méthode : Deux parcours équivalents

  • Itérer sur les éléments : for x in L: — on reçoit chaque valeur ; c'est le parcours à privilégier quand on n'a pas besoin des positions.
  • Itérer sur les indices : for i in range(len(L)): — on accède à L[i], et l'on peut modifier la liste ou comparer des éléments voisins (L[i] et L[i+1]).

Les deux parcours ne donnent pas la même chose à la variable de boucle, et c'est tout ce qui les sépare :

Exemple 2.5Les quatre motifs de parcours à connaître

def moyenne(L):
    # AGRÉGER : accumuler une somme sur les éléments
    total = 0
    for x in L:
        total += x
    return total / len(L)

def maximum(L):
    # SÉLECTIONNER : garder le meilleur élément rencontré
    m = L[0]
    for x in L:
        if x > m:
            m = x
    return m

def compte_positifs(L):
    # COMPTER : incrémenter sous condition
    c = 0
    for x in L:
        if x > 0:
            c += 1
    return c

def positions_de(L, v):
    # LOCALISER : parcours par indices pour renvoyer des positions
    P = []
    for i in range(len(L)):
        if L[i] == v:
            P.append(i)
    return P

notes = [12, 8, 15, 10, 15]
print(moyenne(notes), maximum(notes))     # 12.0 15
print(compte_positifs([-2, 5, 0, 3]))     # 2
print(positions_de(notes, 15))            # [2, 4]

(Les fonctions sum, max, min de Python font le même travail — mais savoir les réécrire est exigible, et c'est le prototype des fonctions statistiques du chapitre 4.)

Exemple 2.6Comparer des voisins : une suite est-elle croissante ?

def est_croissante(L):
    # Parcours par indices : on compare chaque terme au suivant
    for i in range(len(L) - 1):
        if L[i] > L[i + 1]:
            return False       # un contre-exemple suffit !
    return True

La logique du chapitre 1 du manuel affleure : pour réfuter « pour tout , », on exhibe un contre-exemple (return False immédiat) ; pour l'affirmer, il faut avoir tout vérifié (le return True n'arrive qu'après la boucle).

2.5 Listes et mathématiques : premiers ponts

Exemple 2.7Tableau de valeurs d'une fonction

def tableau_valeurs(f, a, b, n):
    # n + 1 valeurs de f régulièrement espacées sur [a, b]
    pas = (b - a) / n
    X = [a + k * pas for k in range(n + 1)]
    Y = [f(x) for x in X]
    return X, Y

X, Y = tableau_valeurs(lambda x: x**2 - 2*x, 0, 3, 6)
# X = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
# Y = [0.0, -0.75, -1.0, -0.75, 0.0, 1.25, 3.0]

Deux compréhensions suffisent : l'une pour les abscisses, l'autre pour les images. On lit sur Y le minimum atteint en — le sommet de la parabole. Les chapitres suivants systématisent ces allers-retours entre listes et notions mathématiques.

2.6 Exercices d'entraînement

Difficulté : ★ facile   ★ moyen   ★ plus difficile.

Génération

Exercice

Écrire en compréhension : (a) la liste des cubes de à ; (b) la liste des entiers de à multiples de ; (c) la liste des valeurs de pour de à .

Solution

(a) [n**3 for n in range(11)]   (b) [n for n in range(1, 51) if n % 7 == 0]   (c) [1/n for n in range(1, 11)].

Exercice

Traduire chaque ensemble en une liste Python en compréhension :

Solution

A = [2*k + 1 for k in range(10)] (les dix premiers impairs).
B = [n for n in range(100) if n % 4 == 0 and n % 6 != 0] — la condition combine un « et » et une négation, comme la proposition mathématique.

Exercice

Générer par ajouts successifs la liste des premiers termes de la suite de Fibonacci, puis en extraire (par compréhension) ceux qui sont pairs.

Solution

F = [1, 1]
for i in range(13):
    F.append(F[-1] + F[-2])
pairs = [x for x in F if x % 2 == 0]
# F : [1, 1, 2, 3, 5, 8, ..., 610] ; pairs : [2, 8, 34, 144, 610]

(Un terme sur trois est pair — le repérer, c'est déjà conjecturer.)

Manipulation et indices

Exercice

On pose L = [4, 7, 1, 7, 9]. Donner, sans machine, le contenu de L après chacune des instructions successives : L.append(2) ; L.remove(7) ; del L[0] ; L[2] = 5. Que vaut alors L[-2] ?

Solution

[4, 7, 1, 7, 9, 2] → [4, 1, 7, 9, 2] (première occurrence de retirée) → [1, 7, 9, 2] → [1, 7, 5, 2]. Et L[-2] vaut 5.

Exercice

Écrire une fonction inverse(L) qui renvoie une nouvelle liste contenant les éléments de L en ordre inverse, sans utiliser reversed ni L[::-1].

Solution

def inverse(L):
    R = []
    for i in range(len(L) - 1, -1, -1):   # indices de len-1 à 0
        R.append(L[i])
    return R

Variante par valeurs : for x in L: R.insert(0, x).

Parcours

Exercice

Écrire une fonction deuxieme_max(L) renvoyant le deuxième plus grand élément d'une liste d'au moins deux nombres distincts.

Solution

def deuxieme_max(L):
    m1, m2 = max(L[0], L[1]), min(L[0], L[1])
    for x in L[2:]:
        if x > m1:
            m2, m1 = m1, x     # x devient le max, l'ancien max recule
        elif x > m2:
            m2 = x
    return m2

Un seul parcours suffit : on entretient les deux meilleurs rencontrés.

Exercice

Écrire une fonction ecart_maximal(L) renvoyant le plus grand écart entre deux éléments consécutifs. L'appliquer de tête à [3, 8, 5, 11, 10].

Solution

def ecart_maximal(L):
    m = 0
    for i in range(len(L) - 1):
        ecart = abs(L[i + 1] - L[i])
        if ecart > m:
            m = ecart
    return m

Écarts : — le maximum est .

Exercice

Écrire une fonction diviseurs(n) (liste en compréhension), puis est_parfait(n) qui teste si est égal à la somme de ses diviseurs stricts (hors ). Vérifier que et sont parfaits, puis chercher le suivant par une boucle.

Solution

def diviseurs(n):
    return [d for d in range(1, n + 1) if n % d == 0]

def est_parfait(n):
    return sum(diviseurs(n)) - n == n     # somme des diviseurs stricts

parfaits = [n for n in range(2, 10000) if est_parfait(n)]
# [6, 28, 496, 8128]

✓ et ✓ ; le suivant est . Trois fonctions en cascade : la modularité du chapitre 5 pointe déjà.

Exercice

Que renvoie mystere([12, 8, 15, 10, 15]) ? Décrire en une phrase ce que fait cette fonction.


def mystere(L):
    R = []
    for x in L:
        if x not in R:
            R.append(x)
    return R
Solution

Elle renvoie [12, 8, 15, 10] : la liste sans doublons, dans l'ordre de première apparition — c'est le passage de la liste (avec répétitions) à l'ensemble de ses valeurs, en conservant un ordre.

Continuer sur Adloun : animation, QCM, fiches, exercices