Vous êtes sur la page 1sur 46

Cours d’informatique – 2021/2022

PCSI

Vésale Nicolas
2021 – 2022 Page 3/46

Table des matières

1 Programmation élémentaire 5
1.1 Utilisation de l’éditeur de texte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.1 Généralités. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.2 Rédaction de fonctions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.3 Instructions conditionnelles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Boucles simples. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.1 Boucles for. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.2 Boucles while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3 Utilisation de listes, de chaînes de caractères et de dictionnaires. . . . . . . . . . . . . . . . . . . . 9
1.3.1 Utilisation de listes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.2 Chaînes de caractères. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.3 Utilisation de dictionnaires. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4 Boucles imbriquées, complexité linéaire et quadratique. . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.4.1 Boucles imbriquées. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.4.2 Complexité linéaire et quadratique. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.5 Récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.5.1 Fonctions récursives : principe et premiers exemples . . . . . . . . . . . . . . . . . . . . . . 17
1.5.2 Récursif et listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.6 Algorithmes classiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6.1 Algorithmes gloutons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6.2 Algorithmes « diviser pour régner ». . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.7 Utilisation de modules. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7.1 Numpy et matplotlib.pyplot. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7.2 Utilisation de tableaux. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.7.3 Manipulation d’images. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

2 Représentation des nombres. 29


2.1 Représentation des entiers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.2 Représentation des réels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

3 Méthodes de programmation et analyse des algorithmes 33


3.1 Méthodes de programmation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.1.1 Spécification des variables d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.1.2 Variables locales et globales, effet de bord . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2 Terminaison et correction des algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.2.1 Mise en place de jeux de tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.2.2 Terminaison des algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.2.3 Correction des algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

4 Graphes 41
4.1 Vocabulaire, modes de définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2 Chemins entre deux sommets. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.3 Files, parcours en largeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.4 Algorithme de Djikstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44

Vésale Nicolas VH
2021 – 2022 Page 5/46

Chapitre 1

Programmation élémentaire

1.1 Utilisation de l’éditeur de texte.


1.1.1 Généralités.
Sur Pyzo, l’éditeur de texte est situé en à gauche. On peut y taper des instructions :

1+1

Que l’on peut exécuter en appuyant simultanément sur les touches Ctrl+Entrée. Le résultat (2) s’affiche alors
dans la console, située en haut à droite sur Pyzo.
Un des intérêts de l’éditeur de texte est que l’on peut taper plusieurs instructions avant de les exécuter :

x=2
x+=2
x

l’exécution de ces trois lignes effectuera successivement les trois opérations.


Souvent, on cherchera à garder les instructions que l’on a tapées dans l’éditeur de texte sans les exécuter à chaque
fois. Pour ceci, on peut séparer ses instructions par ## :

1 x=2
2 x+=2
3 x
4 ##
5 x**=2
6 x

Dans cet exemple, appuyer sur Ctrl+Entrée exécutera les trois premières lignes si notre curseur y est et les lignes
5 et 6 si notre curseur y est. Il vous est fortement recommandé de séparer au moins les différentes questions des
T P par des ##. Un dernier avantage de l’éditeur de texte est que l’on peut/doit sauvegarder son travail au cours
et à la fin d’un séance : Fichier-Enregistrer (sous).

1.1.2 Rédaction de fonctions.


On peut, grâce à l’éditeur de texte, définir des fonctions sur Pyzo. Il faut cependant faire très attention à la
syntaxe de rédaction :

def f(x):
res=2*x+1
return res

Vésale Nicolas VH
Page 6/46 2020 – 2021

Cet exemple définit la fonction f , qui prend pour valeur x et qui rend 2 x + 1. Plus généralement, pour définir une
fonction nomfonction qui prend pour paramètres a,b,c,... et qui rend un résultat res, la syntaxe à utiliser est :
def nomfonction(a,b,c,...):
...
...
return res
n’oubliez pas les deux points à la fin de la première ligne et l’indentation de toutes les ligne suivantes !
Voyons un deuxième exemple. Définissons la fonction qui prend pour valeur x et qui rend True si x est solution
de l’équation 2 x + 1 = 0 et False sinon :

def solution(x):
res=(f(x)==0)
return res

1.1.3 Instructions conditionnelles.


Bien souvent, il sera utile de rendre des résultats différents suivant une ou plusieurs conditions logiques. Par
exemple, si l’on cherche à définir la fonction valeur absolue, on a :

x si x est positif
|x| =
−x si x est négatif
En Pyzo, la syntaxe à utiliser est alors (n’oubliez pas les indentations !) :
if condition:
premier cas
else:
deuxième cas
Par exemple, pour la fonction valeur absolue :

def valabs(x):
if x>=0:
res=x
else:
res=-x
return res

On peut également traiter le cas de la fonction qui rend le maximum de deux réels x et y :

def maxi(x,y):
if x>y:
res=x
else:
res=y
return res

Enfin, il est possible, si il y a plus que deux cas, d’utiliser un elif, comme dans l’exemple suivant :

def g(x):
if x>=0:
res=x**2
elif x>=-1:
res=-x
else:
res=1
return res

Sauriez-vous tracer le graphe de la fonction g ?

VH Vésale Nicolas
2021 – 2022 Page 7/46

1.2 Boucles simples.


1.2.1 Boucles for.
On utilise une boucle for pour faire prendre successivement à une variable donnée une suite d’entiers. Par exemple :

for i in range(10):
print(i)

affiche successivement les entiers naturels compris entre 0 et 9 (notez la bizarrerie). Plus généralement :
range(n): # Fait prendre pour valeurs à i les entiers compris entre 0 et n − 1
for i in range(a,b): # Fait prendre pour valeurs à i les entiers compris entre a et b − 1
range(a,b,c): # i prend pour valeurs les entiers a, a + c, a + 2 c, ... jusqu’à b − 1
Notez en particulier que l’entier c dans la dernière syntaxe peut être négatif, ce qui peut être très pratique. Enfin,
si l’on a oublié quels sont les valeurs qui sont prises par i dans une telle syntaxe, on pour toujours taper dans la
console :
>>> list(range(20,5,-5))
[20, 15, 10]
Voyons quelques exemples d’utilisation :
1. On cherche à définir une fonction u qui rend le n-ième terme de la suite définie par :
u2n
u0 = 1 ∀n ∈ N, un+1 = +1
4

def u(n):
u=1 #initialisation
for i in range(n):
u=u**2/4+1 #relation de recurrence
return u

#on montrera en mathématiques que un tend vers 2


print(u(100))
n
X
2. On cherche à définir une fonction S qui prend pour paramètre n et qui rend Sn = k.
k=0

def S(n):
s=0 #initialisation de la somme à 0
for i in range(n+1): #attention ici
s=s+i #on ajoute un par un tous les entiers entre 0 et n
return s

print(S(100)==100*101//2)

3. Définissons une fonction Fibo qui prend pour paramètre n et qui rend Fn , le n-ième terme de la suite de
Fibonacci définie par :
F0 = F1 = 1 et ∀n ∈ N, Fn+2 = Fn+1 + Fn .

def Fibo(n):
fn,fnp=1,1 #initialisations. On va calculer f_n et f_(n+1) simultanément
for i in range(n):
fn,fnp=fnp,fn+fnp
#car le terme suivant pour fn est f_(n+1)
#et celui pour f_(n+1) est f_(n+2)=f_(n+1)+f_n
return(fn)
print(Fibo(10))

Vésale Nicolas VH
Page 8/46 2020 – 2021

1.2.2 Boucles while.


On utilise une boucle while pour demander à l’ordinateur de continuer à effectuer des opérations jusqu’à ce qu’une
condition est remplie. Par exemple :

i=0
while i<10:
print(i)
i=i+1

affiche les entiers naturels strictement inférieurs à 10. Avec un code similaire, on peut transformer toutes les boucles
for en boucles while mais attention !

while True:
print(’Ah’)

Les boucles while peuvent ne pas s’arrêter... on parle de problème de terminaison de l’algorithme. Quand un tel
problème vous arrivera, vous pourrez forcer un arrêt en appuyant simultanément sur Ctrl+i. Pour cette raison, si
l’on peut choisir entre une boucle for et une boucle while, on préférera toujours la boucle for. La sytaxe générale
d’une telle boucle est :
while condition:
...
Une dernière remarque ; pour trouver la condition, il est souvent utile de réfléchir « négativement », c’est-à-dire
de chercher ce qui fera s’arrêter le fonction et pas ce qui la fait continuer.
1. Déterminons la plus grande puissance de 2 inférieure ou égale à 10000 :

p=2
while p<10000:
p**=2
print(p)

2. Un mathématicien nous souffle que la suite de Héron, définie par :


 
1 2
u0 = 2, ∀n ∈ N, un+1 = un +
2 un

converge vers 2. Un informaticien peu √ rigoureux suggère qu’on peut donc utiliser l’idée suivante pour
calculer une bonne approximation de 2 : quand deux termes consécutifs sont√« suffisamment » proches
(par ex. |un+1 − un | 6 10−6 ) alors un est lui-même « suffisamment » proche de 2 :

def approx():
u=2
up=1/2*(u+2/u) #initialisations
while abs(u-up)>10**(-6): #condition d’arrêt
u,up=up,1/2*(up+2/up) #récurrence
return(u)

print(approx(),2**0.5)

3. La suite de Siracuse est définie par une valeur initiale s0 = a ∈ N∗ et la relation de récurrence :
(
3 sn + 1 si sn est impair
sn+1 =
sn /2 si sn est pair

Définissons une fonction Sira qui prend pour paramètre a et qui affiche toutes les valeurs prises par sn
jusqu’à ce que sn prenne la valeur 1.

VH Vésale Nicolas
2021 – 2022 Page 9/46

def Sira(a):
sn=a #initialisation
while sn!=1:
if sn%2==0: #si sn est pair
sn=sn//2
else: #si sn est impair
sn=3*sn+1
print(sn) #on affiche sn
Sira(27)

Le problème de terminaison est particulièrement criant pour cette fonction : personne ne sait si elle termine
pour toutes les valeurs de a !

1.3 Utilisation de listes, de chaînes de caractères et de dictionnaires.


1.3.1 Utilisation de listes.
Les listes sont un moyen pratique pour enregistrer de grandes quantités de données. Par exemple, si :

l=[1,10,5,7,9]

la liste l contient les valeurs 1, 10, 5, 7 et 9. Il est bien souvent utile de créer une liste qui ne contient aucun
élément. Pour ceci, on tape :

lvide=[]

• Obtenir des données d’une liste déjà créée.


1. Pour accéder à un élément d’une liste, on l’appelle de la façon suivante :
>>> l[1]
10
Notez le décalage : comme pour les boucles for, le premier terme de la liste n’est pas l[1] mais l[0] ! Il est
souvent utile d’appeler le dernier élément d’une liste. Pour ceci, on peut taper :
>>> l[-1]
9
Attention ! Bien souvent sans s’en rendre compte, on essayera d’accéder à un élément « hors des bornes ». Il
faut savoir alors reconnaître le message d’erreur :
>>> l[5]
Traceback (most recent call last):
File "<console>", line 1, in <module>
IndexError: list index out of range
2. On peut également accéder à une sous-liste d’une liste avec la syntaxe l[début,fin] avec, comme pour les
range un « décalage » sur le début et la possibilité d’utiliser des entiers négatifs.
>>>l[1:4] >>> l[-4:-1]
[10, 5, 7] [10, 5, 7]
3. Enfin, il est souvent utile d’obtenir la longueur d’une liste lst, ce qui se fait avec la commande len(lst).
>>> len(l)
5
En particulier, la syntaxe suivante permet de parcourir tous les éléments d’une liste l :

for i in range(len(l)):
print(l[i])

Vésale Nicolas VH
Page 10/46 2020 – 2021

Exemple(s) 1 :
1.1 Créons une fonction sum qui prend pour paramètre une liste l composée d’entiers naturels et qui rend
la somme des éléments de cette liste :

def sum(l):
s=0 #initialisation de la somme à 0
for i in range(len(l)): #on parcourt les éléments de l
s=s+l[i] #et on les ajoute un par un
return s

print(sum([1,2,3]))

1.2 Créons une fonction max qui prend pour paramètre une liste l composée d’entiers naturels et qui rend
le maximum de cette liste :

def max(l):
maxtemp=l[0]#initialisation d’un max temporaire
for i in range(len(l)): #on parcourt les éléments de l
#et on teste si ils sont plus grands que le max temporaire
if l[i]>maxtemp:
#si oui, ils deviennent le max temporaire
maxtemp=l[i]
#le dernier max temporaire est le max de toute la liste
return maxtemp

print(max(l))

• Créer et modifier des listes.


1. Pour ajouter un élément a à la fin d’une liste lst, on peut utiliser la syntaxe lst.append(a). Par exemple :

l=[]
l.append(3)
l.append(2)

donne à l la valeur [3,2].

Exemple(s) 2 :
2.1 Pour créer la liste l=[0,2,...,500] on peut taper :

l=[]
for i in range(251):
l.append(2*i)

2. L’opération inverse lst.pop() permet de retirer à une liste son denrier élément. Par exemple, après :

l=[3,2,4]
l.pop()

la liste l a pour valeur [3,2].


3. Enfin, on peut concaténer des suites :

l1=[1,2,4]
l2=[5,7,9]
l=l1+l2

Donne à l la valeur [1, 2, 4, 5, 7, 9] . Notez que pour des raisons de rapidité, il faut toujours privilégier
append à une concaténation si l’on a la choix.

VH Vésale Nicolas
2021 – 2022 Page 11/46

1.3.2 Chaînes de caractères.


Une chaîne de caractères se crée de la façon suivante :

s=’Hello World!’

Il est alors possible de faire sur une telle chaîne toutes les opérations déjà vues sur les listes, à l’exception de
.append() et .pop(). Voici quelques exemples :

s=’Hello World!’

#affiche « H »
print(s[0])

#affiche « Hello World! » caractère par caractère.


for i in range(len(s)):
print(s[i])

#transforme s en ’Hello PCSI1!’


s=s[:6]+’PCSI1!’
print(s)

1.3.3 Utilisation de dictionnaires.


Un dictionnaire est une liste dont on peut choisir la numérotation des valeurs (on parle de clés). Par exemple :

d={2:10,5:11,7:9}

alors d[2] rend 10, d[5] rend 11 mais d[1] rend une erreur. Plus généralement, on peut créer un dictionnaire par
la syntaxe :

d={clé_1:valeur_1,...,clé_n:valeur_n}
• Opérations sur les dictionnaires.
1. On peut vérifier si une clé appartient au dictionnaire d par clé in d. Par exemple :
>>> 2 in d >>> 1 in d
True False
2. On peut obtenir la longueur d’un dictionnaire d par len(d).
>>> len(d)
3
3. On peut ajouter ou modifier un couple clé/valeur au dictionnaire d par d[clé]=valeur. Par exemple, si :

d[3]=7
d[2]=d[2]+1

alors :
>>> d
{2: 11, 5: 11, 7: 11, 3: 7}
4. On peut de même enlever le couple clé/valeur du dictionnaire d en tapant d.pop(clé).

Exemple(s) 3 :
3.1 Un professeur cherche un moyen efficace d’enregistrer les notes suivantes :
Alice 10 15
Bob 13

Vésale Nicolas VH
Page 12/46 2020 – 2021

notes={’Alice’:[10,15],’Bob’:[13]}

Bob vient d’obtenir une nouvelle note : 12. Pour l’enregistrer, on peut taper :

notes[’Bob’].append(12)

Il a oublié les notes de Bob ! Pour les retrouver, il peut taper :

print(notes[’Bob’])

Une nouvelle étudiante intègre la classe : Carole. Sa première note est un 8. Pour l’enregistrer, on
exécute :
notes[’Carole’]=[8]

3.2 Créons une fonction compte qui prend pour paramètre une liste l et qui rend un dictionnaire d dont les
clés sont les éléments de l et si i est un élément de l, d[i] rend le nombre d’occurrences de i dans l.

def compte(l):
d={}#initialisation du dictionnaire
for i in range(len(l)):
#dans le cas où l[i] est une clé de dictionnaire,
if l[i] in d:
#on ajoute 1 à la valeur d[l[i]]
d[l[i]]=d[l[i]]+1
else:
#sinon, on ajoute la clé d[l[i]]
#et on lui donnne pour valeur 1
d[l[i]]=1
return(d)

print(compte([1,2,1,1,2,3]))

• Parcourir les éléments d’un dictionnaire.


1. Méthode keys(). Si d est un dictionnaire, list(d.keys()) est la liste des clés du dictionnaire d.
2. Méthode items(). Si d est un dictionnaire, list(d.items()) est si d=[clé1:valeur1,...,clén:valeurn] la
liste [(clé1:valeur1),...,(clén:valeurn)].

Exemple(s) 4 :
4.1 Réutilisons le dictionnaire notes de l’exemple précédent. Pour faire l’appel, le professeur a besoin d’af-
ficher tous les noms des étudiants de la classe :
noms=list(notes.keys())
for i in range(len(noms)):
print(noms[i])

4.2 Il cherche enfin à faire la moyenne des notes de la classe :

noms=list(notes.keys())
#on initialise deux variable: la somme des notes et leur nombre
somme,nbnotes=0,0
for i in range(len(noms)):
#pour chaque étudiant, on parcourt la liste de ses notes
for j in range(len(notes[noms[i]])):
somme+=notes[noms[i]][j]
nbnotes+=1
print(somme/nbnotes)

VH Vésale Nicolas
2021 – 2022 Page 13/46

1.4 Boucles imbriquées, complexité linéaire et quadratique.


1.4.1 Boucles imbriquées.

Exemple(s) 5 :
5.1 On considère une liste L d’entiers ou de réels dont on cherche les deux valeurs les plus proches. L’idée
naturelle pour ce faire est d’utiliser deux pointeurs i et j qui parcourent toute la liste :
L=[l1,l2,...,li,...,lj,...ln]
↑ ↑
i j

et qui permettent de déterminer l’écart entre toutes deux valeurs de la liste L. Il suffit alors de déterminer
les valeurs qui correspondent au minimum de ces écarts.

L=[66,54,23,42,1]

def rechercheproches(L):
#initialisation de deux variables
mindst=abs(L[0]-L[1]) #écart minimal
res=L[0],L[1] #valeurs concernées
for i in range(len(L)):
#attention à l’indentation ici, on veut
#pour chaque valeur de i parcourir toutes les valeurs de j
for j in range(len(L)):
#i!=j car on veut deux valeurs différentes
if i!=j and abs(L[i]-L[j])<mindst:
res=L[i],L[j]
mindst=abs(L[i]-L[j])
return res

print(rechercheproches(L))

5.2 On considère une liste L composée de réels. On cherche à trier la liste L dans le sens croissant. Une
première idée est d’utiliser un tri à bulles dont le principe est le suivant :
(a) On parcourt la liste L = [l1 , l2 , ...ln ] de gauche à droite en intervertissant les paires successives
li , li+1 non ordonnées.
À la fin de ce premier parcours, l’élément le plus grand de la liste est le dernier.
(b) On recommence avec la sous-liste [l1 , l2 , ...ln−1 ] et ceci jusqu’à ce qu’il n’y ai plus qu’un seul élément
dans la liste.
Le nom de ce tri vient du fait que les valeurs les plus grandes semblent remonter la liste au cours de
l’algorithme, telles des bulles dans l’eau. Traitons un exemple :
[ 66, 54, 23, 42, 1]
←−−−→
[54, 66, 23, 42, 1]
←−−−→
[54, 23, 66, 42, 1]
←−−−→
[54, 23, 42, 66, 1]
←−−→
Fin de la première étape : la « bulle » 66 est remontée jusqu’à la fin de la liste :
[54, 23, 42, 1,66]
C’est l’élément le plus grand de la liste. On recommence maintenant avec les les autres éléments.

Vésale Nicolas VH
Page 14/46 2020 – 2021

[54, 23, 42, 1, 66]


←−−−→
[23, 54, 42, 1, 66]
←−−−→
[23, 42, 54, 1, 66]
←−−→
Fin de la deuxième étape, deux « bulles » sont montées :
[23, 42 , 1, 54, 66]
On continue avec les trois autres.
[23, 42, 1, 54, 66]
←−−→
Fin de la troisième étape :
[23, 1 , 42, 54, 66]
On termine avec les deux premiers de la liste.
[23, 1, 42, 54, 66]
←−−→
La liste est triée !
[1, 23 , 42, 54, 66]
En Python, cet algorithme s’écrit :

L0=[66,54,23,42,1]

def triabulles(L):
#le pointeur i indique la fin de la liste encore à trier
for i in range(len(L),0,-1):
#le pointeur j parcourt le liste encore à trier.
#attention à s’arrêter un avant la fin: on appelle L[j+1]
for j in range(0,i-1):
#on teste si les éléments successifs sont dans le bon ordre
if L[j+1]<L[j]:
#si ce n’est pas le cas, on les intervertit
(L[j],L[j+1])=(L[j+1],L[j])
#on rend la liste triée
return L

print(triabulles(L0))

5.3 Être capable de trier une liste est souvent très pratique pour résoudre un problème. Par exemple, si l’on
reprend l’exemple 5.1, une nouvelle façon de procéder est de trier la liste puis de juste chercher l’écart
le plus petit entre deux éléments successifs de la liste triée.

def rechercheproches2(L):
Lt=triabulles(L)
dmin=Lt[1]-Lt[0]
res=Lt[1],Lt[0]
for i in range(len(Lt)-1):
if Lt[i+1]-Lt[i]<dmin:
dmin=Lt[i+1]-Lt[i]
res=Lt[i+1],Lt[i]
return res

print(rechercheproches2([66, 54, 23, 42, 1]))

VH Vésale Nicolas
2021 – 2022 Page 15/46

1.4.2 Complexité linéaire et quadratique.


Si l’on cherche à calculer la variance d’une liste L composée d’entiers ou de réels, on peut :

Exemple(s) 6 :
6.1 Créer une fonction moyenne :

def moyenne(L):
s=0
for i in range(len(L)):
s+=L[i]
return s/len(L)

6.2 Puis l’utiliser pour calculer une variance des deux façons suivantes :

(a) def var1(L): (b) def var2(L):


m=moyenne(L) v=0
v=0 for i in range(len(L)):
for i in range(len(L)): v+=(L[i]-moyenne(L))**2
v+=(L[i]-m)**2 return(v/len(L))
return(v/len(L))

Cependant, exécuter var1(list(range(123,1234567))) donne un résultat rapidement alors que ce


n’est pas le cas de var2(list(range(123,1234567))).

Le problème dans la deuxième fonction précédente vient de sa complexité temporelle. Pour chacune des
n=len(L) valeurs de i dans la boucle for, elle appelle la fonction moyenne, qui lance une boucle for qui ef-
fectue n opérations. Elle effectue donc n2 opérations, ce qui est significativement plus que la première fonction,
qui en effectue n. Comme on ne sait pas et on ne peut pas savoir la durée d’une opération individuelle, on utilise
les notations suivantes : on dit qu’une quantité un qui dépend d’un entier naturel n est :
1. O(1) si elle est bornée (on parle de temps constant)
2. O(n) si elle est majorée par C n où C est une constante réelle (on parle de temps linéaire en n)
3. O(n2 ) si elle est majorée par C n2 où C est une constante réelle (on parle de temps quadratique en n)
Revoyons quelques exemples de fonctions du cours pour en déterminer la complexité temporelle :

Exemple(s) 7 :
7.1 La fonction valeur absolue :

def valabs(x):
if x>=0:
res=x
else:
res=-x
return res

Complexité en O(1).
7.2 La fonction moyenne :

def moyenne(L):
s=0
for i in range(len(L)):
s+=L[i]
return s/len(L)

Complexité en O(len(L)).

Vésale Nicolas VH
Page 16/46 2020 – 2021

7.3 Une somme double :

def S(n):
s=0
for i in range(1,n+1):
for j in range(1,n+1):
s+=1/(i+j)
return s

Complexité en O(n2 ).
7.4 Le tri à bulles :

def triabulles(L):
for i in range(len(L),0,-1):
for j in range(0,i-1):
if L[j+1]<L[j]:
(L[j],L[j+1])=(L[j+1],L[j])
return L

Le nombre d’opérations effectué est proportionnel à (si n =len(L)) :


n−1
X n (n − 1)
(i − 1) = − n = 0(n2 ).
i=0
2

Complexité en O(len(L)2 ).

Revenons enfin sur les complexités des opérations que nous avons vues sur les listes, les chaînes de caractères et
les listes.

Foncitons et méthodes sur les listes


Opération Exemple Complexité
Création d’une liste vide l=[] O(1)
Accès direct l[0] O(1)
Longueur len(l) O(1)
Concaténation l1+l2 O(n1 + n2)
Ajout en fin de liste l.append(1) O(1)
Suppression en fin de liste l.pop()+ O(1)
Extraction de tranche l[1:10] O(n), où n est la longueur de la tranche.

Fonctions et méthodes sur les chaînes de caractères


Opération Exemple Complexité
Création s = ’Ma chaîne’ O(n)
Accès direct s[0] O(1)
Longueur len(s) O(1)
Concaténation s1+s2 O(n1 + n2)
Extraction de tranche s[1:10] O(n), où n est la longueur de la tranche.

Fonctions et méthodes sur les dictionnaires


Opération Exemple Complexité
Création d = {cle : valeur} O(1)
Test d’appartenance d’une clé cle in d O(1)
Ajout d’un couple clé/valeur d[cle] = valeur O(1)
Valeur correspondant à une clé d[cle] O(1)
Ensemble des clés list(d.keys()) O(n)

VH Vésale Nicolas
2021 – 2022 Page 17/46

1.5 Récursivité
1.5.1 Fonctions récursives : principe et premiers exemples
Une fonction récursive est une fonction qui peut s’appeler elle-même. Par exemple :

def fact(n):
#valeur initiale
if n==0:
return 1
#formule de récurrence
else:
return n*fact(n-1)

Pour exécuter une telle fonction, l’ordinateur crée une pile contenant les différents appels de la fonction comme
variables. Par exemple, si l’on cherche à exécuter fact(2), la pile sera successivement :

[fact(2)] puis [fact(2),fact(1)] et enfin [fact(2),fact(1),fact(0)]

et une fois ceci fait, il utilise les liens entre ces variables pour les calculer. Ici :

fact(0)=1 puis fact(1)=1*fact(0)=1 et enfin fact(2)=2*fact(1)=2*1=2

Le problème de cette méthode est que la pile peut devenir trop grande (on parle de dépassement de pile). Dans
Python, la taille maximale d’une pile est d’environ 1000. On repère un tel problème par le message d’erreur :

RecursionError: maximum recursion depth exceeded in comparison

Dans ce cas, il est donc clair que la fonction itérative est meilleure :

def facti(n):
res=1
for i in range(1,n+1):
res*=i
return res

Une application du principe de récursivité est la méthode d’exponentiation rapide. L’idée est de calculer le
plus vite possible (au sens avec le moins d’opérations possibles) une très grande puissance. Traitons un exemple.
si l’on cherche à calculer 214 , on peut soit écrire :

214 = 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2

ce qui nécessite 13 opérations, et qui se traduit par la fonction itérative suivante

def expi(a,n):
res=1
for i in range(n):
res*=a
return res

mais il est aussi possible d’écrire :

214 = (27 )2 = (2 × (23 )2 )2 = (2 × (2 × 22 )2 )2

ce qui ne nécessite que 5 opérations ! La méthode utilisée ici se traduit par la fonction récursive :

Vésale Nicolas VH
Page 18/46 2020 – 2021

def expr(a,n):
if n==0:
return 1
#dans le cas où n est pair
elif n%2==0:
#a^n=(a^(n/2))^2 donc
p=expr(a,n//2)
return p*p
#si n est impair
else:
#a^n=a*(a^((n-1)/2)) donc
p=expr(a,(n-1)//2)
return a*p*p

Un mot sur la complexité. Dans le cas de l’algorithme « naïf », calculer an nécessite n − 1 = O(n) opérations.
Dans le cas de l’algorithme d’exponentiation rapide, on peut montrer par récurrence sur n qu’il est de complexité
O(log2 (n)).

1.5.2 Récursif et listes


On peut également utiliser la programmation récursive avec des listes. Par exemple, si l’on cherche à calculer le
maximum d’une liste, on peut procéder de façon itérative (paragraphe 1.3.1) ou utiliser que :
1. Initialisation : max([a]) = a
2. Récursivité : on pose m = max([a1 , a2 , ..., an−1 ]) alors :
(a) si an > m : max([a1 , a2 , ..., an ]) = an
(b) sinon max([a1 , a2 , ..., an ]) = m

def maxr(L):
#on initialise
if len(L)==1:
return L[0]
#pour la partie récursive
else:
a=L.pop()
m=maxr(L)
#pourquoi a,m=L[-1],maxr(L[:-1])
#est-il moins bien?
if m>a:
return m
else:
return a

Notez l’utilisation de L.pop(), qui est de complexité O(1), contrairement à l’extraction de tranche.
Un exemple classique d’algorithme récursif est la recherche dichotomique d’un élément x dans une liste L, que
l’on suppose triée. Le principe est le suivant :
1. Initialisation : si L=[] alors x n’est pas dans L.
2. Récursivité : on pose m=len(L)//2,
(a) si x vaut mil=L[m], alors x appartient à L,
(b) si x>L[m] on cherche dans la moitié droite de L,
(c) si x<L[m] on cherche dans la moitié gauche de L.

VH Vésale Nicolas
2021 – 2022 Page 19/46

def dicor(L,x):
if len(L)==0:
return False
m=len(L)//2
if x==L[m]:
return True
elif x>L[m]:
return(dicor(L[m+1:],x))
else:
return(dicor(L[:m],x))

Notez qu’à cause de l’extraction de tranches, la complexité de cette fonction est loin d’être optimale. On peut
résoudre ce problème en gardant une fonction récursive, mais profitons-en pour voir comment l’on peut transformer
une fonction récursive en une fonction itérative. L’idée est d’introduire deux pointeurs :

def dichoit(L,x):
#on introduit deux pointeurs pour indiquer là où l’on cherche:
deb,fin=0,len(L)-1
#on continue à chercher tant que l’on ne tombe pas sur une liste vide
while fin-deb>=0:
m=(fin+deb)//2
if x==L[m]:
return True
elif x>L[m]:
deb=m+1
else:
fin=m-1
return False

Concernant la complexité, comme chaque étape de la boucle while divise par deux la longueur de la liste dans
laquelle nous devons chercher : O(log2 (len(L))).

1.6 Algorithmes classiques


1.6.1 Algorithmes gloutons
L’exemple typique d’algorithme glouton est le problème de rendu de monnaie. Imaginons qu’on souhaite rendre
une somme de 354 euros à un client. On peut pour ceci lui rendre :

200 reste à rendre 354 − 200 = 154. Puis 100 reste 154 − 100 = 54. Puis 50 reste 54 − 50 = 4. Et enfin 2 et 2.

Cet algorithme est dit glouton car il cherche à chaque étape, parmi les possibilités pour rendre la monnaie, la plus
grande possible.
Pour implémenter cet algorithme, on se donne une liste L triée contenant tous les montants des « pièces » utiles
(dans notre cas, L=[1,2,5,10,20,50,100,200,500]). Et n, un entier qui représente la somme que l’on doit rendre.
L’algorithme doit rendre une liste R, qui est composée des « pièces » que l’on utilisera pour rendre la somme n.
L’idée la plus simple (mais la moins efficace) pour cet algorithme est récursive :

1. si n<L[0], R=[],
2. sinon, on cherche la plus grande « pièce » pmax que l’on peut rendre, on l’ajoute au résultat et on recommence
avec n-pmax.

Vésale Nicolas VH
Page 20/46 2020 – 2021

def rendurec(n,pieces):
if n<pieces[0]:
return []
else:
#recherche de la plus grande pièce que l’on peut rendre
pmax=pieces[0]
for i in range(len(pieces)):
if pieces[i]<=n:
pmax=pieces[i]

#par un appel récursif, on appelle le rendu de n-pmax


R=rendurec(n-pmax,pieces)
#et on lui ajoute pmax
R.append(pmax)
return R

On peut cependant faire beaucoup mieux : on n’a pas exploité que la liste des pièces est triée (ce qui peut nous «
économiser » la boucle for à chaque appel).
L’idée est d’introduire un pointeur rg qui désigne le rang dans la liste de pièce de la dernière pièce rendue. On
peut alors améliorer notre fonction en récursif :

def rendurec2(n,rg,pieces):
#on s’arrête maintenant quand notre pointeur « dépasse » la dernière piéce
if rg==-1:
return []
else:
#si la pièce en cours est trop grande
if pieces[rg]>n:
#on passe à la pièce suivante
return rendurec2(n,rg-1,pieces)
else:
#sinon, on rend cette pièce et on recommence avec la somme restante
R=rendurec2(n-pieces[rg],rg,pieces)
R.append(pieces[rg])
return R

print(rendurec2(354,len(piecesfr)-1,piecesfr))

ou même la transformer en fonction itérative :

def renduit(n,pieces):
R=[]
rg=len(pieces)-1
while rg >=0:
if n-pieces[rg]>=0:
n=n-pieces[rg]
R.append(pieces[rg])
else:
rg-=1
return(R)

Un mot sur l’algorithme en tant que tel. On peut démontrer que dans les cas des pièces européennes, on réussira
toujours ainsi à rendre la somme avec un nombre minimal de pièces. Mais avec d’autres types de pièces on pourra :
1. Ne pas réussir à rendre la somme alors que c’est possible : si n=4 et les pièces sont [2,3] il rendra [3] alors
que [2,2] est possible.
2. Rendre un nombre non optimal de pièces. Par exemple, pour rendre 6 avec pour pièces [1,3,4], l’algorithme
glouton rendra [1,1,4] alors que [3,3] est possible.

VH Vésale Nicolas
2021 – 2022 Page 21/46

1.6.2 Algorithmes « diviser pour régner ».


Un exemple typique d’un tel algorithme est le tri par partition-fusion. Donc l’idée récursive est la suivante :
1. Les listes de longueur un sont déjà triées.
2. Pour trier une liste de longueur quelconque, on peut :
(a) partition : découper la liste en deux moitiés de liste,
(b) appel récursif : trier chacune de ces sous-listes,
(c) fusion : fusionner ces deux sous-listes triées en une liste triée.
Traitons un exemple. on cherche à trier la liste :
L = [7, 4, 1, 6, 3, 8, 2, 5]
on partitionne :
[7,4,1,6,3,8,2,5]

[7,4,1,6] [3,8,2,5]

[7,4] [1,6] [3,8] [2,5]

[7] [4] [1] [6] [3] [8] [2] [5]

puis on fusionne
[7] [4] [1] [6] [3] [8] [2] [5]

[4,7] [1,6] [3,8] [2,5]

[1,4,6,7] [2,3,5,8]

[1,2,3,4,5,6,7,8]

Concernant la partie fusion, une façon efficace de mettre en place cette partie de l’algorithme est d’utiliser un
pointeur par sous-liste triée et de créer la liste fusionnée en ajoutant à chaque étape le plus petit élément des deux
indiqués par les pointeurs jusqu’à ce qu’il n’y ait plus d’éléments dans les listes. Algorithmiquement, ceci nous
donne la fonction :

def fusion(L1,L2):
#L1 et L2 sont des listes triées
p1,p2,l1,l2=0,0,len(L1),len(L2)
res=[]
while p1+p2<l1+l2:
#tests pour ne pas dépasser le dernier élément des listes
if p2==l2:
res.append(L1[p1])
p1+=1
elif p1==l1:
res.append(L2[p2])
p2+=1
elif L1[p1]<=L2[p2]:#<= pour que le tri soit stable
res.append(L1[p1])
p1+=1
else:
res.append(L2[p2])
p2+=1
return res

Vésale Nicolas VH
Page 22/46 2020 – 2021

La complexité de cette fonction est en O(len(L1) + len(L2)). On peut maintenant créer la fonction de tri par
partition-fusion :

def tripf(L):
if len(L)==1:
return L
else:
mil=len(L)//2
L1=tripf(L[:mil])
L2=tripf(L[mil:])
return fusion(L1,L2)

Pour déterminer la complexité de cette fonction, le plus parlant est de revenir à la vision en arbre de l’algorithme.
Comme la taille de listes considérée est divisée par 2 à chaque niveau de l’arbre, le nombre de lignes de celui-ci
est en O(log2 (n)) = O(ln(n)) où n = len(L). De plus, la complexité des fusions étant proportionnelle au nombre
d’éléments des listes considérées, celles-ci sont de complexité O(n) à chaque ligne. La complexité de ce tri est donc
en O(n ln(n)) . On peut démontrer que c’est la meilleure complexité temporelle théorique possible pour un tri.
Le principal reproche que l’on peut faire à ce tri est que si la liste est presque triée (ce qui est souvent le cas en
pratique) il n’en tiendra aucun compte. L’algorithme de tri de Python est une variante du tri par partition-fusion
qui résout ce problème.
Un autre reproche que l’on peut faire à cet algorithme est qu’il peut être gourmand en mémoire. Contrairement au
tri à bulles qui se contentait de modifier la liste initiale (on dit qu’un tel tri est en place) le tri par partition-fusion
nécessite la création d’une grande quantité de listes auxiliaires.
Terminons par un dernier avantage de ce tri : il est stable. Si deux éléments sont la même valeur mais sont classés
dans un certain ordre, ce tri les laissera dans le même ordre. C’est particulièrement utile dans le cas où l’on trie
une liste suivant différents critères successivement.

1.7 Utilisation de modules.


Un module est une collection de fonctions (mais pas seulement) déjà écrites que l’on peut charger dans Python.
Pour ajouter de telles fonctions à notre environnement de travail, on peut utiliser différentes syntaxes. La plus
simple est :

import module

mais on préférera souvent renommer ce module. Pour ceci, on pourra écrire :

import module as mdl

on pourra ensuite appeler les fonctions fct du module mdl en tapant mdl.fct. Enfin, on peut décider de n’ajouter
qu’une fonction fct du module module. pour ceci, on peut utiliser :

from module import fct

et il suffira alors d’appeler fct (sans module. devant !) pour l’utiliser.

1.7.1 Numpy et matplotlib.pyplot.


Le module numpy contient la plupart des fonctions « mathématiques ». L’usage est de le renommer np lorsqu’on
le charge :

import numpy as np

on peut alors par exemple calculer np.cos(np.pi). La pluspart des noms de fonctions de numpy sont transparents
mais l’aide et la touche Tab sont vos amis !
Le sous-module matplotlib.pyplot permet quand à lui de tracer des graphes. Il est d’usage de le renommer plt.

import matplotlib.pyplot as plt

VH Vésale Nicolas
2021 – 2022 Page 23/46

Tracer un graphe avec plt revient à tracer une succession de segments. Pour ceci, on lui donne une liste de
coordonnées x = [x1 , x2 , . . . , xn ] et y = [x1 , x2 , . . . , xn ] de points P1 = (x1 y1 ), P2 = (x2 , y2 ), . . . , Pn = (xn , yn ) et on
entre plt.plot(x,y). Notez qu’alors rien ne s’affiche ! C’est très pratique car ceci permet de superposer les graphes
avant de les afficher. Pour afficher le graphe, il faut alors taper plt.show() (n’oubliez pas les parenthèses...).

#un triangle équilatéral


x=[-1,1,0,-1]
y=[0,0,np.sqrt(3),0]
plt.plot(x,y)
plt.show()

Pour tracer le graphe d’une fonction sur le segment [a, b], c’est la même idée : on crée une liste x = [x0 , x1 , . . . , xn−1 ]
avec n points équirépartis sur le segment :

b−a
∀i ∈ J0, n − 1K, xk = a + k
n−1
et une autre liste y = [f(x0 ), f(x1 ), . . . , f(xn−1 )] composée des images de ces points. L’idée est qu’avec suffisamment
de points, ce qu’on trace ressemblera assez au graphe de la fonction f . Pour faire ceci, le vous conseille pour la
liste x d’utiliser une fonction de numpy :

x=np.linspace(a,b,n)

et pour y d’utiliser une liste en compréhension (retenez bien la syntaxe qui peut être utile dans d’autres
circonstances) :

y=[f(t) for t in x]

Par exemple, si l’on souhaite tracer le graphe des fonction sinus et cosinus sur [−3 π, 3 π] :

x=np.linspace(-3*np.pi,3*np.pi,1000)
y1=[np.cos(t) for t in x]
y2=[np.sin(t) for t in x]
plt.plot(x,y1)
plt.plot(x,y2)
plt.show()

Bien entendu, il est possible d’ajouter de nombreux paramètres esthétiques aux graphes. Là encore, n’hésitez pas
à utiliser l’aide !
Mais pourquoi se limiter à des fonctions ? Traçons un arbre de Noël par récursivité en partant de la transformation
suivante, que l’on répétera sur chaque segment obtenu :

−−→ −−→
où BC = 5/6 AB, BD = BE = 1/2 BE et les angles entre BC et BD et BC et BE valent tous π/6.

Vésale Nicolas VH
Page 24/46 2020 – 2021

#rend le point C, où le vecteur AC est la rotation de AB d’angle th


#puis son homothétie de rapport p
def roth(th,p,A,B):
C=[A[0]+p*(np.cos(th)*(B[0]-A[0])-np.sin(th)*(B[1]-A[1])),
A[1]+p*(np.cos(th)*(B[1]-A[1])+np.sin(th)*(B[0]-A[0]))]
return C

def arbre(n,pts):
if n>0:
for i in range(len(pts)//2):
#charche tous les segments [A,B] déjà calculés et les trace
A=pts[2*i]
B=pts[2*i+1]
#pas optimal de retracer plusieurs fois mais effet «neige»
plt.plot([A[0],B[0]],[A[1],B[1]],color=’g’,linewidth=.5)
#calcule les points C,D et E
#et ajoute les segments [B,C], [B,D] et [B,E] aux segments
C=[-5/6*A[0]+11/6*B[0],-5/6*A[1]+11/6*B[1]]
D=roth(-np.pi/6,1/2,B,C)
E=roth(np.pi/6,1/2,B,C)
pts=pts+[B,C,B,D,B,E]
arbre(n-1,pts)

arbre(7,[[0,0],[0,1]])

#axes proportionnels et on centre le dessin


plt.axis(’square’)
plt.xlim(-2,2)

plt.show()

Joyeuses fêtes !

VH Vésale Nicolas
2021 – 2022 Page 25/46

1.7.2 Utilisation de tableaux.


En Python, les listes peuvent avoir n’importe quelle longueur. Pour cette raison, lorsqu’on exécute les instructions :

L1=[1,2,3]
L2=L1
L1[0]=2
alors appeler L2 affiche [2,2,3]. La commande L1=L2 est à comprendre comme « à partir de maintenant, les listes
L1 et L2 sont identiques ». On parle de copie superficielle. Cette façon permet de s’assurer que, quelque soit la
longueur de L1, l’exécution de L1=L2 reste en temps constant. On pourrait chercher à contourner le problème avec
une boucle for :
L1=[1,2,3]
L2=[x for x in L1] #ou encore L2=L1.copy()
L1[0]=2
afficher L2 rend [1,2,3] mais si l’on tape :

L1=[[1,2],[3,4]]
L2=L1.copy()
L1[0][0]=2
afficher L2 rend [[2,2],[3,4]]. La superficialité de la copie est passée des listes aux variables qui les composent...
le fait que le type des objets d’une liste peut être quelconque (donc en particulier peut être une liste) oblige Python
à procéder ainsi.

Plus généralement, la flexibilité qu’offre les listes peut être une source de problèmes. Pour les résoudre, on est
amenés à utiliser un objet moins flexible : les tableaux (array) du module numpy, qui contrairement aux listes
ont une longueur donnée et un même typage donné pour les objets qui les composent. Alors, si :

import numpy as np
t1=np.array([[1,2],[3,4]])
t2=np.copy(t1)
t1[0][0]=2
appeler t2 rend np.array([[1,2],[3,4]]). Le problème est résolu mais sa résolution crée d’autres surprises.
Comme les objets d’un tableau n’ont qu’un seul type si t1=np.array([1,1.2]) alors t1[0] rend 1.0. De même,
tenter d’exécuter t1=np.array([[1,2],[3]]) rendra un message d’erreur. Enfin, le fait que le nombre d’éléments
d’un tableau est fixé nous empêche d’utiliser une commande de type .append.
Pour terminer sur les comparaisons entre listes et tableaux, les deux spécificités des tableaux les rendent moins
gourmands en espace mémoire (un seul type à enregistrer) et il est plus rapide de leur appliquer des fonctions
mathématiques :

t1=np.array([[0,1,2,3,4]])
t2=np.cos(t1*np.pi/2)

(notez qu’on a déjà utilisé cette propriété quand on a tracé des graphes : np.linspace est un tableau !). Pour ces
raisons, on les utilisera prioritairement pour traiter de grandes quantités de données. Par exemple, si l’on cherche
expérimentalement à déterminer la distribution des valeurs obtenues lorsqu’on jette deux dés à 6 faces équilibrés :

#premier dé
jets1=np.random.choice([1,2,3,4,5,6],10000)
#deuxième
jets2=np.random.choice([1,2,3,4,5,6],10000)
#somme des deux dés
jets=jets1+jets2
#affchage de l’histogramme
plt.hist(jets,11,range=[2,12],density=True)
plt.show()

Vésale Nicolas VH
Page 26/46 2020 – 2021

1.7.3 Manipulation d’images.


Dans ce paragraphe, nous utiliserons les modules numpy et matplotlib.pyplot.
import numpy as np
import matplotlib.pyplot as plt
Pour charger une image dans Python, on peut utiliser le module matplotlib.image que l’on peut charger avec
import matplotlib.image as mpimage
Les deux seules fonctions que nous allons utiliser dans ce module sont :
mpimage.imread(chemin_image1) et mpimage.imsave(chemin_image2,tableau)
La première prend en paramètre un chemin en chaîne de caractères (par ex : ”C : //dossier/image.jpg”) (attention
au // après le C : !) de l’image à charger et rend un tableau numpy. La seconde prend pour paramètres un chemin en
chaîne de caractères (par ex : ”C : //dossier/imagemodifiee.jpg”) de l’image à enregistrer et un tableau numpy
et enregistre l’image au chemin demandé.
Une image en couleurs est constituée de petits carrés pixels disposés sur une grille et colorés. Pour cette raison,
si t est un tableau représentant une image t[y,x] représente la coloration du pixel en position (x, y) dans l’image
(linversion de y et x parraît pizzare mais pensez-y, c’est comme pour les matrices). Attention ! t[0,0] représente
le pixel en haut à gauche de l’image. on progresse ensuite vers la droite (en x) et vers le bas (en y).
Pour représenter la coloration d’un pixel, on utilisera le format RGB (red-green-blue) : un tableau à trois ou
quatre entrées : la première code la quantité de rouge, la seconde celle de vert, la troisième celle de bleu et la
quatrième éventuellement la transparence du pixel.
Enfin (et c’est souvent ici qu’on a des problèmes...) suivant le type d’image les entrées qui codent les pixels
changent :
type d’image type des entrées nombre d’entrées valeurs
.jpg ou .jpeg dtype=np.uint8 3 entiers entre 0 et 255
.png dtype=np.float32 4 réels entre 0 et 1
une image .jpg ou .jpeg sera donc moins lourde mais pourra représenter moins de couleurs. Attention, les entiers
de type uint8 sont calculés modulo 256. Par exemple :
>>> np.uint8(255)+np.uint8(1)
<console>:1: RuntimeWarning: overflow encountered in ubyte_scalars
0
Une dernière commande peut être utile lorsqu’on travaille avec des images : pour afficher l’image représentée par
le tableau t dans Python, on peut utiliser la commande : plt.imshow(t) suivie bien-sûr par plt.show().

Exemple(s) 8 :
8.1 Créons une image .jpg de 256 × 128 pixels, tous bleus.

#tout est important ici:


#(256,128): dimensions de l’image, 3: nombre d’entrées par pixel
#dtype=np.uint8 type des pixels pour les .jpg
bleu=np.zeros((128,256,3),dtype=np.uint8)
for x in range(256):
for y in range(128):
bleu[y,x]=np.array([0,0,255],dtype=np.uint8)
#ni rouge ni vert mais un maximum de bleu

#avant de l’enregistrer, on l’affiche dans Python pour vérifier:


plt.imshow(bleu)
plt.show()

Enregistrons-la dans le dossier dir :

dir=’le_chemin_de_votre_dossier’
mpimg.imsave(dir+’bleuNFT.jpg’,bleu)

VH Vésale Nicolas
2021 – 2022 Page 27/46

Dans la suite, par souci de simplicité, nous manipulerons des images en niveaux de gris. Chaque pixel est alors
codé par une seule valeur, de type unit8 : un entier entre 0 et 255. L’ouverture se fait comme précédemment.

#affichage d’une image en niveaux de gris dans Python


plt.imshow(img,cmap=’gray’)
plt.show()
#pour les enregistrer, c’est plus compliqué...
mpimg.PIL.Image.fromarray(img).save(dir+’nomfichier.jpg’)

Exemple(s) 9 :
9.1 Nous allons chercher à effectuer une homothétie de facteur k de l’image. L’idée naïve est de chercher,
pour chaque pixel du tableau représentant l’image agrandie, le pixel le plus proche lui correspondant
dans l’image de départ.

def zoom(img,k):
li,lj=int(k*len(img)),int(k*len(img[0]))
res=np.zeros((li,lj),dtype=np.uint8)
for i in range(li):
for j in range(lj):
res[i,j]=img[int(i/k),int(j/k)]
return res

9.2 Cette façon de procéder crée une image de relativement mauvaise qualité : chaque pixel de l’image de
départ apparaît environ k fois dans l’image modifiée. Pour régler ce problème, on peut chercher à lisser
l’image obtenue en prenant pour chaque pixel la moyenne de lui et de ses 8 voisins.

def moyvois(img,i,j):
res=0
for k in range(-1,2):
for l in range(-1,2):
res+=img[i+k,j+l]/9
return res

def lissage(img):
res=np.copy(img)
li,lj=len(img),len(img[0])
for i in range(1,li-1):
for j in range(1,lj-1):
res[i,j]=moyvois(img,i,j)
return res

L’idée que l’on vient d’utiliser permet bien d’autres choses. Théoriquement, on parle
 de convolution
 de
1/9 1/9 1/9
l’image par une matrice A (appelée noyau de convolution). Dans notre cas, A = 1/9 1/9 1/9 .
1/9 1/9 1/9

Vésale Nicolas VH
Page 28/46 2020 – 2021

VH Vésale Nicolas
2021 – 2022 Page 29/46

Chapitre 2

Représentation des nombres.

2.1 Représentation des entiers.


• Un entier naturel n ∈ N s’écrit de façon unique :

n = a0 + a1 b + · · · + ad bd

où ad , . . . , a1 , a0 sont des entiers naturels compris entre 0 et b − 1. On parle alors d’écriture en base b de l’entier
n. Par exemple,
61 = 1 + 0. 2 + 1. 22 + 1. 23 + 1. 24 + 1. 25
On note alors 61 = 1111012 (attention à lire les coefficients de la somme précédente de droite à gauche !) et on dit
que 111101 est l’écriture de 61 en base 2.
Trouver la valeur d’un entier dont on connaît la représentation en base b est un simple calcul de somme. Pour la
base 2, on peut même utiliser :
>>> 0b111101
61
Réciproquement, étant donné un entier n donné pour calculer l’écriture en base b de n, on utilise l’algorithme :
• Comme
n = a0 + b (a1 + · · · + ad bd−1 )
a0 est le reste de la division euclidienne de n par b.
• On remplace la variable n par (n − a0 )/b et alors, comme

n//b = (n − a0 )/b = a1 + b (a2 + · · · + ad bd−2 )

a1 est le reste de la division euclidienne de cet entier par b.


• On itère ce processus jusqu’à obtenir un entier nul.
En Python, les entiers sont enregistrés en mémoire en base 2. Pour des raisons de rapidité, on fixe souvent le
nombre de 0 et de 1 (on parle de bits) que l’on se donne pour enregistrer un entier naturel quelconque au moment
du choix du type. Par exemple, en uint8 on de donne 8 bits et en uint16 16 bits etc...
Entier Écriture en uint8 Écriture en uint16
61 00111101 0000000000111101
Si cette façon de faire est très efficace, elle entraîne qu’un « plus grand entier » ainsi représentable existe.
7
X 15
X
k 8
En uint8 : 2 = 2 − 1 = 255, et en uint16 : 2k = 216 − 1 = 65535.
k=0 k=0

Une fois cet entier atteint, l’ordinateur recommence en zéro pour l’entier suivant.
>>> np.uint16(65536)
0
En toute généralité, si l’on se donne n bits, on pourra ainsi représenter tous les entiers compris entre 0 et 2n − 1.

Vésale Nicolas VH
Page 30/46 2020 – 2021

• Pour coder les entiers relatifs (on dit signés en informatique), on utilise le plus souvent le principe du
complément à 2. Plus précisément, si l’on se donne n bits, on représentera les entiers relatifs compris entre −2n−1
et 2n−1 − 1. Notez qu’on « perd » une puissance de 2 par rapport aux entiers naturels. Coder un entier naturel se
fait comme précédemment. Pour un entier négatif k ∈ J−2n−1 , −1K, on code l’entier positif k + 2n ∈ J2n−1 , 2n − 1K.
Les entiers négatifs « suivent » donc les entiers positifs dans cette façon de les coder. Par exemple en int8: (vous
devinez donc que les autres types s’appellent int16, int32...)
Entier relatif 0 ··· 127 −128 ··· −1
Entier naturel codé 0 ··· 127 128 ··· 255
Écriture en base 2 00000000 ··· 01111111 10000000 ··· 11111111
On peut facilement vérifier les deux premières lignes avec :
for i in range(0,256):
print(np.int8(i))
Cette façon de faire a beaucoup d’avantages. Par exemple :
1. Le premier bit de l’écriture donne toujours le signe de l’entier. Un 0 indique un entier positif et un 1 un
entier négatif.
2. Pour obtenir l’écriture de −k, on peut écrire k − 1 en base 2 puis inverser tous les 0 et les 1 (d’où le nom de
complément à 2). Par exemple, comme 61 = 62 − 1 s’écrit sur 8 bits 00111101 alors −62 s’écrit 11000010.
Démonstration : On rappelle que −k est codé par l’entier 2n − k. Mais alors :

(k − 1) + (2n − k) = 2n − 1

et 2n −1 est représenté sur n bits par n fois 1. L’écriture en base 2 de k−1 et 2n −k sont donc bien complémentaires.


3. L’addition de les représentation en base 2 de tels entiers se fait de la même façon qu’avec des entiers : par
retenues, qu’ils soient négatifs ou positifs.
• Le type int de Python est au même temps plus simple et plus compliqué que ce que nous venons de voir. Le
logiciel utilise une représentation des entiers en multi-précision ce qui signifie qu’au lieu d’un tableau de taille
fixe, il représente les entiers par une liste de taille arbitraire. En particulier, il n’existe (théoriquement car l’espace
mémoire est quand même toujours limité) pas de plus grand entier naturel pour Python. Il en est de même pour
les entiers négatifs. Cependant, ce qu’on gagne ainsi peut poser des problèmes. Bien entendu, travailler avec des
listes plutôt qu’avec des tableaux est moins efficace. Mais plus grave, il n’est par exemple pas possible de prédire
le temps que va prendre la somme ou la multiplication de deux entiers...

2.2 Représentation des réels.


• Un réel x ∈ R peut toujours s’écrire :

x = ad bd + · · · + a1 b + · · · + a0 + b1 b−1 + b2 b−2 + · · ·

où ad , . . . , a1 , a0 , b1 , b2 , . . . sont des entiers naturels compris entre 0 et b−1. On note alors n = ad · · · a1 a0 .b1 b2 · · ·(b)
une écriture en base b de x. Par convention si les bi sont nuls à partir d’un certain rang, on ne les fera pas appa-
raître dans cette écriture.

Exemple(s) 10 :
10.1 Bien souvent, cette écriture est infinie, ce qui posera problème en informatique. C’est toujours le cas
pour des irrationnels comme π et parfois le cas pour des fractions. Par exemple, en base 10 :
1 1
= 0, 333 · · · mais = 0, 5
3 2
10.2 Voyons comment passer d’une écriture en base b à un réel et réciproquement.
(a) Le sens direct est un simple calcul de somme. Par exemple, si x = 0.11(2) . Alors :

x = 0 20 + 1 2−1 + 1 2−2 = 3/4 = 0.75

VH Vésale Nicolas
2021 – 2022 Page 31/46

(b) Réciproquement, si x est un réel, on commence en calculant l’écriture en base b de bxc (comme au
paragraphe précédent) puis on calcule l’écriture en base 2 de y = x − bxc. On a :
y = b1 b−1 + b2 b−2 + b3 b−3 + · · ·
donc
• Pour calculer b1 , on peut utiliser que : b1 = by bc
• On recommence alors avec le réel y b − b1 = b2 b−1 + b3 b−2 + · · · .
Par exemple, calculons l’écriture en base 2 de y = 0.25. On a :
y 0.25 0.5 0
b2 yc 0 1
Donc y = 0.01(2) .
Calculons de même l’écriture en base 2 de y = 0.1. On a :
y 0.1 0.2 0.4 0.8 0.6 0.2
b2 yc 0 0 0 1 1
Comme l’on retrouve 0, 2, l’écriture est infinie :
0.1 = 0.0001100110011 · · ·(2) .

• En informatique, on utilisera l’écriture scientifique pour encoder les réels. Vus connaissez déjà celle en base 10 :
h = 6.62607015 × 10−34 ou NA = 6.02214076 × 1023 .
En toute généralité, on peut écrire tout réel non nul x sous la forme :
x = ± m × 10k
où m ∈ [1, 10[ est appelé la mantisse de x et k ∈ Z. En informatique, on préférera l’écriture :

x = ± m × 2k
avec la mantisse m ∈ [1, 2[. Que l’on encode sur 64 bits avec la répartition suivante :
1. Un bit pour coder le signe,
2. 11 bits pour l’exposant k ∈ Z,
3. le reste c’est-à-dire 52 bits pour la mantisse m ∈ [1, 2[.
Concernant les 11 bits dédiés à l’exposant, ils permettent de coder 211 = 2048 entiers relatifs. Cependant, on ne
codera que les entiers compris entre −1022 et 1023, les deux possibilités restantes sont réservées pour des réels dit
« non standards » (zéro par exemple). Plus précisément, si k ∈ J−1022, 1023K, c’est l’entier n = k + 1023 (pas tout
à fait un complément à 2...) que l’on encode.
Concernant la mantisse, comme m ∈ [1, 2[ alors en base 2 :
m = 1, b1 b2 · · · b52 · · ·(2)
c’est donc les quantités b1 b2 · · · b52 que l’on encode.

Exemple(s) 11 :
11.1 On considère x = −2.4. Alors :
−2.4 = −1.2 × 21
Donc m = 1.2 et k = 1. Pour la mantisse, l’écriture en base 2 de 1.2 vaut :
y 0.2 0.4 0.8 0.6 0.2
b2 yc 0 0 1 1
1.2 = 1.0011 · · ·(2) . Donc les 52 premiers chiffres sont :

{z· · · 0011} .
00110011
|
13 fois

Pour l’exposant, on encode n = k + 1023 = 1024 = 210 = 1000000000(2) . On encode donc −2.4 par :

Vésale Nicolas VH
Page 32/46 2020 – 2021

1 0 0 1 1 ··· 0 0 1 1 1 0 ··· 0
11.2 Comme si −1022 6 k 6 1023 on a 1 6 n = k + 1023 6 2046 le entiers n = 0 et n = 211 − 1 = 2047 ne
sont jamais codés pour des réels « normalisés ». En particulier, on utilise n = 0 pour coder 0 de l’une
des deux façons suivantes :
0+ : 0 0 ··· 0 0 ··· 0 ou 0− : 1 0 ··· 0 0 ··· 0
(Hors programme) Dans tous les autres cas, ces deux exposants sont utilisés pour coder des réels dénor-
malisés, c’est-à-dire codés comme les autres mais avec une matisse m = 0, b1 b2 · · · b52 · · ·(2) . Comme
ils contiennent moins de chiffres significatifs que les réels standards, ils sont à éviter.

• Conséquences :
1. Le plus petit réel strictement positif normalisé codé ainsi vaut + 1.0 × 2−1022 . Attention cependant aux réels
« dénormalisés » :
>>> 2**(-1022),2**(-1022-52),2**(-1075)
(2.2250738585072014e-308, 5e-324, 0.0)
2. Le plus grand réel strictement positif normalisé vaut +m 21023 où m = 1.1 · · · 1(2) . Mais alors :

52
X 1 − 2−53
m= 2−k = = 2 − 2−52 .
1 − 2−1
k=0

Donc x = 21024 − 2971 .


3. En Python, l’écart entre deux réels consécutifs (et donc l’erreur numérique commise) n’est pas constant, il
dépend de la taille du réel. Plus précisément, on peut toujours écrire :
a
x = ±m 2k = ±
252−k
où a est un entier. L’écart entre deux réels consécutifs est donc de 2k−52 . Il dépend donc de l’exposant k !
Ceci a plusieurs conséquences :
(a) Il est malheureusement très difficile (pour ne pas dire pratiquement impossible) de faire des calculs
d’erreurs en informatique.
(b) Par ce qu’on vient de voir, l’erreur numérique commise sera très petite avec des réels très petits et très
grande avec des réels très grands.
(c) En particulier, il faut retenir que, dès qu’on utilise des réels en informatique, on fait une approximation.
Il est donc très fortement déconseillé faire des tests d’égalité entre les flottants.
>>> 1.==1.+2**(-53)
True
Pour éviter les problèmes liés à ces approximations, utilisez des inégalités plutôt que des égalités :

#ne s’arrête jamais... #s’arrête.


f=0 f=0
while f!=1: while f<1:
f=f+0.1 f=f+0.1

(d) L’addition des réels en Python n’est pas associative.


a,b,c=2**(54),-2**(54),1.
print((a+b)+c,a+(b+c))

>>>
1.0 0.0
Dans le premier cas, l’ajout de c se fait avec des réels d’exposant 0 (donc avec une précision d’environ
2−52 ) et dans le deuxième cas avec des réels d’exposant 54 (donc avec une précision d’environ 22 = 4...).

VH Vésale Nicolas
2021 – 2022 Page 33/46

Chapitre 3

Méthodes de programmation et analyse


des algorithmes

3.1 Méthodes de programmation


3.1.1 Spécification des variables d’une fonction

Définition 3.1.1 : 1. Donner la signature d’une fonction revient à :


(a) la nommer,
(b) préciser ses arguments et leurs types,
(c) préciser le type des valeurs renvoyées par la fonction.
2. Spécifier une fonction revient à donner sa signature et à lui ajouter :
(a) les pré-conditions que ses arguments doivent vérifier,
(b) les post-conditions portant sur les valeurs rendues par la fonction.

Remarque(s) 1 : 1. Bien souvent, la signature et la spécification de la fonction sont données par l’énoncé.
2. Il est toujours une bonne idée de se laisser guider par ces informations pour coder une fonction.

Exemple(s) 12 :
12.1 La signature de la fonction racine est :

(x : float) → (res : float)

une pré-condition est x>=0 et une post-condition res>=0.


12.2 Une fonction qui vérifie si le float x est dans la liste L admet pour signature :

(x : float, L : list) → (res : bool)

12.3 Il est souvent très utilise d’utiliser la signature et la spécification pour corriger un code. Par exemple, si
Max est une fonction qui rend la maximum de deux float, sa signature est :

(x : float, y : float) → (res : float)

La fonction :

Vésale Nicolas VH
Page 34/46 2020 – 2021

def Max(n,m):
if n>m:
return n
elif m>n:
return m
else:
return ’les deux réels sont égaux’

ne respecte pas la signature, elle ne peut donc pas être juste.


12.4 De même, ces informations peuvent nous permettre d’utiliser correctement une fonction. Si une fonction
Tri admet pour signature :
(L : list) → (L : list)
et comme pré-condition que L est composée de float ou de int. Alors exécuter
Tri([[1, 2, 3], [4, 5, 6]])
n’a pas de sens !

Dans un code, on peut préciser la signature et la spécification d’une fonction de deux façons différentes :
1. par des commentaires au début et à la fin de la fonction,
2. par la commande assert. Cette dernière vérifie une condition, laisse le programme continuer si elle est vraie
et l’arrête en renvoyant un message d’erreur si elle est fausse.

Exemple(s) 13 :
13.1 Reprenons l’exemple de la fonction racine. on peut progressivement améliorer le code par :

def racine(x): def racine(x): def racine(x):


return x**(1/2) #x est réel positif #x est réel positif
return x**(1/2) assert(x>=0)
return x**(1/2)

Avec la dernière fonction, on aura :


>>> racine(-1)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "<tmp 1>", line 3, in racine
assert(x>=0)
AssertionError
Notez qu’on peut encore améliorer le code en ajoutant une vérification du type de x :

def racine(x):
#x est un réel positif
assert(type(x)==float and x>=0)
return x**(1/2)

13.2 On peut aussi se servir de assert pour vérifier des post-conditions. Par exemple :

def reduction(prix, pourcentage):


prixred = prix * (1.0 - pourcentage)
assert (0 <= prixred <= prix)
#le prix après réduction ne peut être
#ni négatif ni supérieur au prix initial
return prixred

VH Vésale Nicolas
2021 – 2022 Page 35/46

3.1.2 Variables locales et globales, effet de bord


On peut classer les variables utilisées en Python en deux catégories :
1. les variables globales dont la valeur peut être utilisée par toutes les fonctions d’un environnement donné.
2. les variables locales : les autres.
Voyons quelques exemples :

g=3 #g est globale x=3 #x est globale


def f():
def f(): x=0
x=0#x est locale à f return x
for i in range(3):#i aussi
x+=i print(f())
return x #renvoie 0: dans f la variable locale
#est prioritaire
#appeler x ou i par par exemple print(x)
print(x) #renvoie 3: la variable locale x
#renvoie: NameError: name ’x’ is not defined #n’existe pas hors de f

La plupart des variables utilisées dans une fonction sont locales, mais on peut parfois choisir d’utiliser une variable
globale. Par exemple :

L=[1,3,5]#L est globale


def ajoute(k):
L.append(k)

ajoute(6)
print(L)

cependant, si on a le choix, il est toujours préférable d’utiliser des variables locales dans une fonction. Le principal
problème lié à l’utilisation d’une variable globale est un effet de bord : (pour les anglo-saxons : effet secondaire)
la modification d’une variable globale reste en mémoire et peut avoir des effet sur une toute autre fonction ou
instruction qui l’utilise. Une façon de vérifier quelles variables globales sont définies dans votre environnement et
leur état (et donc d’éviter certains effets de bord) est d’utiliser l’outil Workspace (ou espace de travail) de Pyzo :

Notez en particulier que les fonctions sont considérées comme des variables globales. Terminons par le plus ennuyant
des problèmes : l’instruction = définit au moment de son exécution le caractère global ou local d’une variable. Mais
ce n’est pas le cas de toutes les instructions. Par exemple,

x=[0] x=[0]
def f(z): def f(z):
z=z+[1] z.append(1)
f(x) # l’instruction = fait que z est locale f(x) # .append() garde la variable globale
print(x)#rend [0] print(x)#rend [0,1]

Vésale Nicolas VH
Page 36/46 2020 – 2021

3.2 Terminaison et correction des algorithmes


3.2.1 Mise en place de jeux de tests
La façon la plus naturelle de tester un algorithme est d’utiliser un jeu d’exemples. On parle alors de jeu de tests.
Dans chaque cas, il s’agit de préciser les valeurs des variables d’entrée et la valeur attendue en sortie par la fonction.
La difficulté de cette approche est de correctement choisir les tests effectués. Le principe général est que :

Tester un programme est la démarche de l’exécuter avec le volonté de trouver des erreurs.

Les principes généraux de la construction de « bons » jeux de tests à retenir sont les suivants :
1. il est bon de partitionner les domaines d’entrées possibles et de tester chacun de ces domaines,
2. il est souvent une bonne idée de tester les « cas limites ».
essayons d’appliquer ces principes sur un programme simple : max(a,b) qui rend le maximum des trois float ou
int en entrée (on suppose que le type de a et celui de b sont respectés par l’utilisateur)
1. Partition des domaines : deux possibilités de maximum donc deux domaines. On teste donc par exemple :
max(1,2)==2,max(2,1)==2
2. Cas limites : les deux entrées sont égales. On teste donc :
max(1,1)==1
Considérons maintenant la fonction du(a,b) qui prend pour paramètres deux float : a et b et qui rend True ssi
(a, b) est dans le disque unité.
1. Partition du domaine : on teste :
du(0,0)==True,du(1,1)==False
2. Cas limite :
du(1,0)==True,du(-1,0)==True,du(1/2,3**(1/2)/2)==True
Bien entendu, si l’une des variables est de taille variable, il sera assez difficile « à la main » de tester tous les cas
possible. Mais l’idée de cas limites reste valable dans ce cas : tester une liste vide ou si la fonction donne le bon
résultat si celui-ci porte sur le dernier élément d’une liste sont de bons tests.
Traitons enfin l’exemple de la fonction suivante 1

La fonction typetr(a,b,c) prend pour paramètre trois int et rend 2 si le triangle dont les côtés ont pour
longueurs a,b et c est équilatéral, 1 si il est isocèle et 0 sinon.

Une fois votre jeu de tests créé, répondez aux questions suivantes ce qui vous donnera une note sur 12 :
1. Avez-vous un test qui représente un « vrai » triangle ? (1,2,3) n’est pas un triangle valide par exemple.
2. Avez-vous un test qui représente un triangle équilatéral valide ?
3. Avez-vous un test qui représente un triangle isocèle valide ?
4. Avez-vous un test de triangle isocèle pour chacune des trois possibilités de côtés égaux ? par exemple (3,3,4),
(3,4,3) et (4,3,3)
5. Avez-vous un test avec l’une des trois valeurs qui vaut 0 ?
6. Avez-vous un test avec l’une des trois valeurs négatives ?
7. Avez-vous un test où la longueur d’un côté est la somme des deux autres ? (figure plate)
8. Avez-vous testé les trois permutations du test 7 ?
9. Avez-vous un test où la somme des longueurs de deux côtés est strictement inférieure à la troisième longueur ?
10. Avez-vous testé touts les permutations du test 9 ?
11. Avez-vous testé (0,0,0) ?
12. Chacun de vos tests est-il accompagné de la valeur de sortie attendue ?
Attention cependant, malgré tout, utiliser des exemples ne permet pas de vérifier en toute généralité qu’une fonction
fait bien ce qu’on attend qu’elle fasse. Par exemple :
1. Tiré de : « The Art of Software Testing », Glenford J. Myers.

VH Vésale Nicolas
2021 – 2022 Page 37/46

def egaux(a,b):
if a==5120 and b==834:
return True
else:
return a==b

l’erreur dans le code est immédiatement détectable à la lecture mais presque impossible à détecter par des tests.

3.2.2 Terminaison des algorithmes


Un algorithme peut ne jamais terminer :

def pbwhile(): def pbrec():


while True: print("pas fini")
print("pas fini") pbrec()

pour démontrer qu’un algorithme termine quand on l’exécute, on utilise des variants qui sont des suites d’entiers
positifs strictement décroissantes. Une telle suite ne peut contenir qu’un nombre fini d’éléments (au plus autant
que son premier terme) et assurent donc que le fonction s’arrête en un nombre fini d’étapes. Comme nous allons
le voir dans les exemples suivants, un variant de boucle peut aussi servir à déterminer la complexité de certaines
fonctions.

Exemple(s) 14 :
14.1 On considère la fonction de recherche naïve suivante :

def recherche(L,a):
#L est une list dans laquele on cherche
#l’indice de la dernière apparition de a
i=len(L)-1
while i>=0:
if L[i]==a:
return i
i-=1
#on rend -1 si l’on n’a pas trouvé a
return i

Dans ce cas, i est un variant de boucle : notons ik la valeur de i après k passages dans la boucle. Alors
i0 = len(L) − 1, ik+1 = ik − 1 et par définition, ik > 0 pour tout k. De plus, si l’on note n le nombre
de passages dans la boucle avant qu’elle s’arrête, comme ik est arithmétique de raison −1, pour tout k,
ik = len(L) − (k + 1) donc n = len(L) − 1. La fonction a donc pour complexité O(len(L)).
14.2 Parfois le variant de boucle n’est pas directement une variable. Dans la variation de la fonction précé-
dente :

def recherche2(L,a):
#L est une list dans laquele on cherche
#l’indice de la dernière apparition de a
i=0
while i<len(L):
if L[i]==a:
return i
i-=1
#on rend -1 si l’on n’a pas trouvé a
return i

le variant de boucle est j = len(L) − i.


14.3 On considère la fonction d’exponentiation rapide :

Vésale Nicolas VH
Page 38/46 2020 – 2021

def expr(a,n):
#n est un entier, n>=0 et a un int ou float
if n==0:
return 1
p=expr(a,n//2)
elif n%2==0:
return p*p
else:
return a*p*p

dans ce cas, c’est n qui est le variant de boucle. Notons nk sa valeur après k passages dans la boucle.
On a : n0 = n, nk > 0 et pour tout k, nk+1 = bnk /2c < nk (car nk 6= 0). Concernant la complexité, si
p = blog2 (n)c, on a :
p 6 log2 (n) 6 p + 1 donc 2p 6 n 6 2p+1
donc par une récurrence immédiate et croissance de la fonction partie entière, pour tout k 6 p :

2p−k 6 nk 6 2p−k+1 .

donc np+1 = 0 (ou np+2 = 0) (et aucun des précédents). La complexité de l’algorithme est donc

O(p) = O(blog2 (n)c) = O(ln(n)).

Montrer qu’un algorithme termine n’est pas toujours une question triviale. Par exemple, personne ne sait si
l’algorithme termine pour toutes valeurs de l’entier strictement positif n :

def Sira(n):
s=n
while s!=1:
if s%2==0:
s=s//2
else:
s=3*s+1

3.2.3 Correction des algorithmes


Commençons par un peu de vocabulaire :
1. Correction partielle d’un algorithme : un algorithme est partiellement correct si son résultat est correct
lorsque qu’il s’arrête.
2. Correction totale d’un algorithme : un algorithme est correct s’il termine et qu’il est partiellement
correct.
Utiliser des variants de boucle comme dans le paragraphe précédent permet de passer d’une correction partielle
à une correction totale d’un algorithme, mais pas de prouver une correction partielle. Pour ceci, on utilise des
invariants, qui sont des quantités qui restent constantes dans les boucles ou les appels récursifs.

Exemple(s) 15 :
15.1 Commençons par un exemple venu des mathématiques :

def euclide(a,b):
#a et b sont deux entiers positifs
while b!=0:
a,b=b,a%b
return a

Pour montrer la correction totale de cet algorithme, on avait exhibé le variant de boucle b et l’invariant
de boucle PGCD(a,b).

VH Vésale Nicolas
2021 – 2022 Page 39/46

15.2 On considère la fonction factorielle itérative :

def fact(n):
#n est un int positif
res=1
for i in range(1,n+1):
res*=i
return res

un invariant de boucle est, si l’on appelle resi la valeur de res après i passages dans la boucle : resi = i!.
En effet, on a res0 = 1 et resi+1 = i × resi et le résultat est vrai par une récurrence immédiate. En fin
de boucle, on rendra donc resn = n!, ce qui montre la correction partielle de l’algorithme. Une boucle
for ne posant pas de problème de terminaison, l’algorithme est correct.
15.3 On considère la fonction :

def m(L):
#L est une list de int ou float
max=L[0]
for i in range(1,len(L)):
if L[i]>max:
max=L[i]
return max

un invariant de boucle est, avec les notations habituelles : « maxi est le maximum de L[: i + 1] ». En
effet, max0 = L[0] est le maximum de L[: 1] = [L[0]] et si la propriété est vraie pour i, on a :
(
L[i + 1] si L[i + 1] > maxi
maxi+1 = = max(L[i + 1], maxi )
maxi sinon

donc comme maxi est le maximum de L[: i + 1], maxi+1 est la maximum de L[: i].
En fin de boucle, on rend maxlen(L)−1 qui est donc le maximum de L[: len(L)] = L, ce qui montre la cor-
rection partielle de l’algorithme. Une boucle for ne posant pas de problème de terminaison, l’algorithme
est correct.
15.4 On considère la fonction de recherche dichotomique dans une liste triée :

def dichoit(L,x):
#on cherche x dans la list triée L
d,f=0,len(L)-1
while f-d>=0:
m=(f+d)//2
if x==L[m]:
return m #(1)
elif x>L[m]:
d=m+1
else:
f=m-1
return -1 #(2)

Un variant de boucle est la quantité f-d, ce qui prouve que la fonction termine. Un invariant de boucle
est « si x est présent dans L à la position p alors di 6 p 6 fi ». La démonstration au cas pas cas de
cette affirmation se fait comme dans l’exemple précédent en utilisant que L est triée.
Concentrons-nous sur la correction de l’algorithme. S’il termine à cause du return (1), il rend clairement
le bon résultat. Sinon, il termine quand fi − di < 0 c’est-à-dire fi < di et alors par le variant de boucle :
« si x est présent dans L en position p alors di 6 p 6 fi » cette dernière affirmation étant incompatible
avec fi < di , x n’est pas présent dans L, ce qui achève de montrer la correction partielle si la fonction se
termine à cause du return (2). Une boucle for ne posant pas de problème de terminaison, l’algorithme
est correct.

Vésale Nicolas VH
Page 40/46 2020 – 2021

15.5 Considérons le tri par insertion :

def tri(L):
#L est une list de int ou float
for i in range(1,len(L)):
x=L[i]
j=i-1#(1)
while j>=0 and x<L[j]:#(2)
L[j+1]=L[j]#(a)
j-=1#(3)
L[j+1]=x#(b)
return L
Alors :
(a) la variable j donne un variant de boucle
Démonstration : Notons jk la valeur de j au début du k-ième passage dans la boucle while. Par (1), (2)
et (3), on a :
j0 = i − 1 ∈ J0, len(L) − 2K, jk > 0 et jk+1 = jk − 1
donc (jk ) est une suite strictement décroissante d’entiers naturels.


(b) la proposition « Si Li est la liste L après i passages dans la boucle for, Li [: i + 1] est triée » est un
invariant de boucle.
Démonstration :
i. Si i = 0, la liste L[: 1] = [L[0]] est triée.
ii. Supposons la propriété vraie pour i fixé. Alors pendant le i+1-ième passage dans la boucle, x = Li [i + 1]
et j = i. Comme j est un variant de boucle, la boucle while s’arrête. Il y a alors deux cas :
A. elle s’arrête car j = −1 et donc par (a) et (b),
Li+1 [: i + 2] = [x = Li [i + 1], Li [0], Li [1], . . . , Li [i]].
Mais comme dans ce cas, par (2), la condition x < Li [j] a toujours été fausse pour j ∈ J0, iK, cette
liste est triée par hypothèse de récurrence.
B. elle s’arrête car pour une certaine valeur de j ∈ J0, iK, x > L[j]. Alors, par (a) et (b) :
Li+1 [: i + 2] = [Li [0], . . . , Li [j], x = Li [i + 1], Li [j + 1], . . . , Li [i]].
et cette suite est triée par hypothèse de récurrence et Li [j] 6 x < Li [j + 1] comme la boucle ne
s’était pas arrêtée au passage précédent.


Déduisons-en la correction de cette fonction : tout d’abord, l’existence d’un variant de boucle montre sa
terminaison, il suffit donc de montrer sa correction partielle. De plus, la fonction s’arrête après len(L)-1
passages dans la boucle for donc par l’invariant de boucle, elle rend la liste
Llen(L)−1 = Llen(L)−1 [: len(L) − 1 + 1]
qui est triée. Parlons maintenant de complexité de cette fonction dans le pire et le meilleur des cas.
(a) La complexité dans le pire des cas est obtenue si à chaque étape la sortie de la boucle while se fait
lorsque j = −1. Ceci correspond, comme on l’a vu pendant la preuve de l’invariant de boucle au cas
où L[i + 1] est toujours plus petit que tous les éléments de Li [: i + 1] (qui sont les mêmes que ceux
de L[: i + 1]) donc au cas d’une liste triée « à l’envers ». Dans ce cas, par l’invariant de boucle,
la boucle while s’arrête en i étapes d’où une complexité dans le pire des cas en O(1 + 2 + · · · +
2
len(L) − 1) = O(len(L) ).
(b) La complexité dans le meilleur des cas est obtenue si à chaque étape, la sortie de la boucle while se
fait instantanément car x = L[i] > L[j] = L[i − 1]. Ceci correspond à une liste déjà triée. Dans ce
cas, la complexité est donc de O(len(L)).
Cette dernière complexité explique l’utilité de ce tri dans le cas de listes « presque triées ».
Terminons en remarquant que ce tri est en place et qu’il est stable car l’inégalité dans (2) est stricte.

VH Vésale Nicolas
2021 – 2022 Page 41/46

Chapitre 4

Graphes

4.1 Vocabulaire, modes de définition


Les deux figures suivantes sont des graphes :

La différence entre les deux est que le second est orienté.


1. Les trois entiers entourés ce des graphes sont appelés sommets (ou nœuds) du graphe.
2. Les traits qui relient les commets sont appelés arêtes pour les graphes non orientés et arcs pour les graphes
orientés. On peut associer à chacun de cas arcs ou arête un poids qui sont les réels positifs non entourés
dans ces graphes.
3. Un chemin est une suite d’arêtes ou arcs qui permet de relier un sommet d’un graphe à un autre. Un cycle
est un chemin qui relie un somment à lui-même.
4. Une arrête ou un arc qui relie un sommet à lui-même est appelé boucle.
Les graphes sont utilisés dans de nombreux domaines concrets. Citons par exemple :
1. les réseaux de transports. Taille typique (nombre d’arêtes/arcs nécessaires pour passer d’un sommet à un
autre) : 1.
2. l’étude des réseaux sociaux (on relie les « amis »). Taille typique 7.
3. graphe du web (qui relie une page à une autre si un lien hypertexte renvoie de l’une vers l’autre - base de
l’algorithme de recherche de Google). Taille typique : 19.
Informatiquement, on peut représenter les graphes sous forme de :
1. Listes d’adjacence (pour les graphes non pondérés) : on enregistre dans une liste pour chaque sommet la
liste des sommets « voisins » c’est-à-dire reliés au sommet considéré par une arête. On peut regrouper ces
informations dans une liste (ce qui est adapté dans les cas des graphes dont les sommets sont désignés par
des entiers), par exemple :

Vésale Nicolas VH
Page 42/46 2020 – 2021

g = [
[1,2], # voisins de 0
[1,3], # voisins de 1
[0,3], # voisins de 2
[1,2,3] # voisins de 4
]

mais aussi, dans les cas oú les sommets sont désignés autrement des dictionnaires dont les clés sont les noms
de sommets et les valeurs associées les listes de leurs voisins. Par exemple :

g = {
’A’:[’B’,’C’], # voisins de A
’B’:[’A’], # voisins de B
’C’:[’A’,’B’] # voisins de C
}

2. Les matrices d’adjacence (principalement pour les graphes pondérés) qui contiennent en case (i, j) le poids
d’une éventuelle arête ou d’un éventuel arc reliant le sommet i au sommet j. Dans le cas où i n’est pas relié
à j, on remplit la case par une quantité « infinie » 1 ou égale à 0 pour le cas des graphes non pondérés. Par
exemple, pour les deux graphes de l’introduction, les matrices d’adjacence sont :
   
∞ 1 3 ∞ 1 3
 1 ∞ 2  et ∞ ∞ 2 
3 2 ∞ ∞ ∞ ∞
informatiquement, on utilise le module numpy pour représenter ces matrices. Par exemple :

import numpy as np
m=10
#joue le rôle de l’infini,
#en pratique doit être strictement supérieur
#au nombre de sommet*maximum des poids
g=np.array([[m,1,3],
[1,m,2],

Notation : On note un graphe (S, A) où S est l’ensemble de ses sommets et A ⊂ S 2 est celui de ses arrêtes/arcs.

4.2 Chemins entre deux sommets.


On considère le graphe :

1. ce qui se comprend très bien si on pense en terme de distance

VH Vésale Nicolas
2021 – 2022 Page 43/46

de liste d’adjacence :

g = [
[9,2], # voisins de 0
[9,3], # voisins de 1
[0,3,5,6], # voisins de 2
[1,2,4], # voisins de 3
[3,5], # voisins de 4
[2,4], # voisins de 5
[2], # voisins de 6
[8], # voisins de 7
[7], # voisins de 8
[0, 1], # voisins de 9
]

Nous allons chercher à déterminer informatiquement s’il est possible de passer d’un sommet du graphe à un autre
via un chemin. L’idée de l’algorithme de parcours avec mémoire est de colorier le graphe :
1. on part d’un graphe dont on a colorié tous les sommets en blanc,
2. on colorie en gris tous les sommets atteignables et en noir le sommet où l’on est,
3. en se déplace sur un sommet gris si il y en a un,
4. on continue ainsi jusqu’à ce que l’on soit en le sommet à atteindre (et on rend alors True) ou qu’on n’ait
plus de sommet gris (et on rend alors False).

def parcoursmem(g,Si,Sb):
#création de la liste des couleurs des sommets
#0: blanc, 1:gris et 2:noir
c=[0 for i in range(len(g))]
#pile des sommets gris:
sg=[Si]
while len(sg)!=0:
#on prend le dernier sommet gris de sg et on le colorie en noir
S=sg.pop()
c[S]=2
if S==Sb:
return True
else:
#on ajoute les sommets atteignables encore blancs
for i in range(len(g[S])):
if c[g[S][i]]==0:
#on ajoute le sommet à la liste des
#sommets gris et on modifie sa couleur
sg.append(g[S][i])
c[g[S][i]]=1
return False

Si il est possible de joindre deux sommets entre eux, on dit qu’ils appartiennent à la même composante connexe.
Dans notre exemple il y a deux composantes connexes :

{0, 1, 2, 3, 4, 5, 6, 9} et {7, 8}.

4.3 Files, parcours en largeur


La façon dont on a implémenté notre algorithme de recherche dans le paragraphe précédent est appelé parcours
en profondeur. Elle est efficace si l’on cherche à savoir si deux sommets « éloignés » sont connectés mais très peu
efficace pour deux sommets « proches ». Pour créer un algorithme adapté à ce type de situation, on peut changer

Vésale Nicolas VH
Page 44/46 2020 – 2021

la façon dont on enregistre les sommets gris. Plutôt que d’ajouter les nouveaux à la fin de la liste des sommets
gris, on les ajoute au début, ainsi, les derniers sommets ajoutés sont traités en... dernier.
Malheureusement, effectuer de telles opérations sur les listes n’est pas efficace. En effet :

#ajouter UN élément au début de L


#est de complexité O(len(L))!
def ajoutedebut(a,L):
res=[a]
for i in range(len(L)):
res.append(L[i])
return res

On utilise donc des files, qui sont des sortes de listes auxquelles on peut ajouter efficacement des éléments à la fin
mais aussi au début. Voici les commandes à connaître sur cet objet.

from collections import deque


##
f=deque([1,2,3])#créer une file
#toutes les opérations qui suivent se font en O(1)
f.append(4)#ajouter un élément à la fin
f.appendleft(0)#ajouter un élément au début
f.pop()#enlever et renvoyer un élément à la fin
f.popleft()#enlever et renvoyer un élément au début

Nous pouvons maintenant modifier le code précédent pour effectuer un parcours en profondeur :

def parcoursprof(g,Si,Sb):
c=[0 for i in range(len(g))]
#file des sommets gris:
sg=deque([Si])
while len(sg)!=0:
S=sg.pop()
c[S]=2
if S==Sb:
return True
else:
for i in range(len(g[S])):
if c[g[S][i]]==0:
#on ajoute le sommet au début de la file des
#sommets gris et on modifie sa couleur
sg.appendleft(g[S][i])
c[g[S][i]]=1
return False

4.4 Algorithme de Djikstra


L’algorithme de parcours en profondeur permet de déterminer la distance minimale entre deux sommets pour les
graphes non pondérés. L’algorithme de Djikstra permet de faire la même chose pour les graphes pondérés. Son
principe est le suivant. Si l’on cherche à déterminer la distance minimale entre un sommet initial Si et un sommet
final Sf :
1. On associe à chaque sommet une couleur : blanc pour non exploré et noir pour exploré. Initialement, seul
Si est noir.
2. On associe à chaque sommet une distance au sommet initial que l’on initialise à 0 pour le sommet initial et
inf pour tous les autres sommets.
3. On parcourt alors le graphe de la façon suivante :

VH Vésale Nicolas
2021 – 2022 Page 45/46

(a) on se déplace à un sommet Sm dont la distance est minimale et finie si il en existe un (sinon l’algorithme
rend False)
(b) si Sm=Sf l’algorithme termine et rend la distance à Sf
(c) sinon, on actualise les distances des sommets connectés à Sm de la façon suivante : si un sommet S est
connecté à Sm et si la distance à Sm + la distance entre Sm et S est inférieure à la distance à S, cette
valeur est enregistrée comme nouvelle distance à S.
Traitons l’implémentation informatique de cet algorithme. On considère le graphe orienté :

Dont la matrice d’adjacence est :

#«infini», doit être plus grand que


#nb sommets*max(distances entre deux sommets)
inf=50*6

A= np.array([
[inf, 9, 20, 17, inf, inf, inf],
[inf, inf, inf, inf, 30, inf, inf],
[inf, inf, inf, inf, inf, 40, inf],
[inf, inf, inf, inf, inf, inf, 50],
[inf, inf, inf, inf, inf, 2, inf],
[inf, inf, inf, inf, inf, inf, 8],
[inf, inf, inf, inf, inf, inf, inf]])

Le passage technique de l’algorithme est le point 3.a. Celui-ci peut être fait de façon efficace grâce à une structure
de file de priorité. Cependant, pour gagner du temps, nous nous contenterons de la « mauvaise » fonction
suivante, qui prend pour paramètres c la liste des couleurs et d la liste des distances et qui rend -1 s’il ne reste
pas de sommets à explorer à distance finie et le numéro d’un sommet à distance minimale sinon.

def mini(d,c):
res=-1
min=inf
for i in range(len(d)):
if c[i]==0 and d[i]<min:
res=i
min=d[i]
return res

Vésale Nicolas VH
Page 46/46 2020 – 2021

Nous pouvons maintenant passer à l’algorithme de Djikstra à proprement parler :

def DjikstraV0(A,Si,Sf):
#A est un graphe pondéré
d=[inf for i in range(len(A[0]))]#distances
c=[0 for i in range(len(A[0]))]#couleurs
Sm=Si
c[Si]=1
d[Si]=0
while Sm!=Sf:
for i in range(len(A[Sm])):
if d[Sm]+A[Sm][i]<d[i]:#si la distance trouvée est plus courte
d[i]=d[Sm]+A[Sm][i]#mise à jour de la distance
Sm=mini(d,c)#recherche d’un sommet non exploré de distance minimale
if Sm==-1:#si on n’en a pas trouvé
return False#Si et Sf non connectés
c[Sm]=1#sinon, on change la couleur du sommet Sm
return d[Sf]

VH Vésale Nicolas

Vous aimerez peut-être aussi