Aller au contenu

Exercices - Algorithmique


Écrire les fonctions suivantes en Python (parcours séquentiel) :

a. recherche_dernier(tab, val) : renvoie l'indice de la dernière occurrence de val, ou -1.

b. contient_doublon(tab) : renvoie True si le tableau contient au moins un élément en double.

c. deux_plus_grands(tab) : renvoie les deux plus grandes valeurs d'un tableau d'au moins 2 éléments (sous forme de tuple).

def recherche_dernier(tab, val):
  pass

def contient_doublon(tab):
  pass

def deux_plus_grands(tab):
  pass
Solution

a.

def recherche_dernier(tab, val):
dernier_indice = -1
for i in range(len(tab)):
if tab[i] == val:
dernier_indice = i
return dernier_indice

b.

def contient_doublon(tab):
for i in range(len(tab)):
for j in range(i + 1, len(tab)):
if tab[i] == tab[j]:
return True
return False

c.

def deux_plus_grands(tab):
assert len(tab) >= 2
if tab[0] >= tab[1]:
max1, max2 = tab[0], tab[1]
else:
max1, max2 = tab[1], tab[0]
for i in range(2, len(tab)):
if tab[i] > max1:
max2 = max1
max1 = tab[i]
elif tab[i] > max2:
max2 = tab[i]
return (max1, max2)

a. Quelle est la complexité de la fonction contient_doublon de l'exercice 1b ? Justifier.

b. Proposer une version plus efficace en utilisant un ensemble (set).

Solution

a. La complexité est en O(n²) car on a deux boucles imbriquées : pour chaque élément (n itérations), on compare avec tous les suivants (au maximum n-1 itérations).

b.

def contient_doublon_rapide(tab):
vus = set()
for element in tab:
if element in vus:
return True
vus.add(element)
return False

Cette version est en O(n) car la recherche dans un set est en O(1) en moyenne.


Dérouler l'algorithme de recherche dichotomique sur le tableau trié [3, 7, 11, 15, 22, 28, 35, 42, 50] pour les valeurs :

a. Recherche de 28

b. Recherche de 10

Solution

a. Recherche de 28 :

Étapegauchedroitemilieutab[milieu]Action
10842228 > 22 → gauche = 5
25863528 < 35 → droite = 5
35552828 == 28 → trouvé à l'indice 5

b. Recherche de 10 :

Étapegauchedroitemilieutab[milieu]Action
10842210 < 22 → droite = 3
2031710 > 7 → gauche = 2
32321110 < 11 → droite = 1
421--gauche > droite → non trouvé (-1)

a. Combien d'étapes au maximum faut-il pour chercher un élément dans un tableau trié de 1 000 éléments par dichotomie ?

b. Et pour 1 000 000 d'éléments ?

c. Si un algorithme en O(n) met 1 seconde pour n = 1 000 000, combien de temps met un algorithme en O(log₂ n) ?

Solution

a. ⌈log⁡2(1000)⌉=10\lceil \log_2(1000) \rceil = 10 étapes au maximum (car 210=1024>10002^{10} = 1024 > 1000).

b. ⌈log⁡2(1000000)⌉=20\lceil \log_2(1000000) \rceil = 20 étapes au maximum (car 220=1048576>10000002^{20} = 1048576 > 1000000).

c. L'algorithme en O(n) fait 1 000 000 opérations en 1 seconde. L'algorithme en O(log₂ n) fait seulement 20 opérations. Si chaque opération prend le même temps : 201000000×1=0.00002\frac{20}{1000000} \times 1 = 0.00002 secondes, soit 20 microsecondes.


Dérouler le tri par sélection sur le tableau [8, 3, 6, 1, 5]. À chaque étape, indiquer l'échange effectué et l'état du tableau.

Solution
ÉtapeRecherche min dansMin (indice)ÉchangeTableau après
1[8, 3, 6, 1, 5]1 (indice 3)8 ↔ 1[1, 3, 6, 8, 5]
2[3, 6, 8, 5]3 (indice 1)pas d'échange[1, 3, 6, 8, 5]
3[6, 8, 5]5 (indice 4)6 ↔ 5[1, 3, 5, 8, 6]
4[8, 6]6 (indice 4)8 ↔ 6[1, 3, 5, 6, 8]

Résultat final : [1, 3, 5, 6, 8]


Dérouler le tri par insertion sur le tableau [7, 2, 5, 1, 4]. À chaque étape, indiquer la clé, les décalages et l'état du tableau.

Solution
ÉtapeCléComparaisons et décalagesTableau après
122 < 7, décaler 7[2, 7, 5, 1, 4]
255 < 7, décaler 7 ; 5 > 2, insérer[2, 5, 7, 1, 4]
311 < 7, décaler 7 ; 1 < 5, décaler 5 ; 1 < 2, décaler 2[1, 2, 5, 7, 4]
444 < 7, décaler 7 ; 4 < 5, décaler 5 ; 4 > 2, insérer[1, 2, 4, 5, 7]

Résultat final : [1, 2, 4, 5, 7]


a. Compter le nombre de comparaisons effectuées par le tri par sélection et le tri par insertion pour trier le tableau [1, 2, 3, 4, 5] (déjà trié).

b. Même question pour le tableau [5, 4, 3, 2, 1] (trié à l'envers).

c. Quel algorithme est le plus avantageux pour un tableau presque trié ? Justifier.

Solution

a. Tableau [1, 2, 3, 4, 5] (déjà trié) :

  • Sélection : 4 + 3 + 2 + 1 = 10 comparaisons (toujours le même nombre)
  • Insertion : 1 + 1 + 1 + 1 = 4 comparaisons (chaque clé est déjà à sa place)

b. Tableau [5, 4, 3, 2, 1] (trié à l'envers) :

  • Sélection : 4 + 3 + 2 + 1 = 10 comparaisons
  • Insertion : 1 + 2 + 3 + 4 = 10 comparaisons (chaque clé doit être insérée tout au début)

c. Le tri par insertion est plus avantageux pour un tableau presque trié : sa complexité est alors proche de O(n), tandis que le tri par sélection fait toujours O(n²) comparaisons.


a. Dérouler l'algorithme glouton de rendu de monnaie pour rendre 87 centimes avec les pièces [1, 2, 5, 10, 20, 50, 100, 200].

b. Avec le système de pièces [1, 3, 4], dérouler l'algorithme glouton pour rendre 6. Le résultat est-il optimal ?

c. Pour le système [1, 3, 4] et la somme 6, trouver la solution optimale.

Solution

a. 87 = 50 + 20 + 10 + 5 + 2 → 5 pièces [50, 20, 10, 5, 2]. C'est optimal.

b. Glouton : 6 = 4 + 1 + 1 → 3 pièces [4, 1, 1]. Ce n'est pas optimal.

c. Solution optimale : 6 = 3 + 3 → 2 pièces [3, 3]. L'algorithme glouton échoue ici car en prenant la plus grande pièce possible (4), il s'empêche d'atteindre la solution optimale.


On dispose des objets suivants et d'un sac de capacité 10 kg :

ObjetPoids (kg)Valeur (€)Ratio valeur/poids
A36020
B410025
C512024
D23015

a. Appliquer l'algorithme glouton (meilleur ratio d'abord). Quels objets sont pris ? Quelle valeur totale ?

b. Trouver une meilleure solution par essai.

Solution

a. Tri par ratio décroissant : B (25), C (24), A (20), D (15).

  • B : poids 4, valeur 100. Poids total : 4.
  • C : poids 5, valeur 120. Poids total : 9.
  • A : poids 3. Poids total serait 12 > 10.
  • D : poids 2. Poids total serait 11 > 10.

Glouton : B + C = 220 € (poids 9 kg).

b. Essayons B + A + D : poids 4 + 3 + 2 = 9 kg, valeur 100 + 60 + 30 = 190 €. Moins bien.

Essayons C + A + D : poids 5 + 3 + 2 = 10 kg, valeur 120 + 60 + 30 = 210 €. Moins bien.

Ici le glouton donne bien la meilleure solution parmi les combinaisons possibles (220 €).


On dispose des données suivantes représentant des fruits selon leur poids (g) et leur diamètre (cm) :

PoidsDiamètreFruit
1507Pomme
1707.5Pomme
1306.5Pomme
604Mandarine
704.5Mandarine
503.5Mandarine
2008Poire
1807Poire

Un nouveau fruit a un poids de 140 g et un diamètre de 6.8 cm.

a. Calculer la distance euclidienne entre ce nouveau fruit et chacun des 8 fruits connus.

b. Quel est le résultat de KNN avec k = 3 ?

c. Et avec k = 5 ?

Solution

a. Nouveau point : (140, 6.8)

FruitPointDistance
Pomme 1(150, 7)(150−140)2+(7−6.8)2=100+0.04≈10.0\sqrt{(150-140)^2 + (7-6.8)^2} = \sqrt{100 + 0.04} \approx 10.0
Pomme 2(170, 7.5)900+0.49≈30.0\sqrt{900 + 0.49} \approx 30.0
Pomme 3(130, 6.5)100+0.09≈10.0\sqrt{100 + 0.09} \approx 10.0
Mandarine 1(60, 4)6400+7.84≈80.0\sqrt{6400 + 7.84} \approx 80.0
Mandarine 2(70, 4.5)4900+5.29≈70.0\sqrt{4900 + 5.29} \approx 70.0
Mandarine 3(50, 3.5)8100+10.89≈90.1\sqrt{8100 + 10.89} \approx 90.1
Poire 1(200, 8)3600+1.44≈60.0\sqrt{3600 + 1.44} \approx 60.0
Poire 2(180, 7)1600+0.04≈40.0\sqrt{1600 + 0.04} \approx 40.0

Note : les distances sont dominées par le poids (échelle bien plus grande), d'où l'intérêt de la normalisation.

b. k = 3 : les 3 plus proches sont Pomme 1 (10.0), Pomme 3 (10.0), Pomme 2 (30.0). Classe majoritaire : Pomme (3 Pommes / 0 autres).

c. k = 5 : les 5 plus proches sont Pomme 1, Pomme 3, Pomme 2, Poire 2, Poire 1. Classe majoritaire : Pomme (3 Pommes, 2 Poires).




Activité 1 - Mesurer la complexité expérimentalement

Section intitulée « Activité 1 - Mesurer la complexité expérimentalement »

a. Écrire les fonctions recherche_sequentielle et recherche_dichotomique.

b. Mesurer le temps d'exécution de chaque algorithme pour des tableaux de tailles 1 000, 10 000, 100 000, 1 000 000. Utiliser le module time.

c. Reporter les résultats dans un tableau. Observer que le temps de la recherche séquentielle est proportionnel à n, et que celui de la dichotomie est quasi-constant.

d. Faire la même mesure pour le tri par sélection et le tri par insertion sur des tableaux de tailles 100, 1 000, 5 000, 10 000. Vérifier que quand n est multiplié par 10, le temps est multiplié par environ 100.

Correction
import time
import random
def recherche_seq(tab, val):
for i in range(len(tab)):
if tab[i] == val:
return i
return -1
def recherche_dicho(tab, val):
g, d = 0, len(tab) - 1
while g <= d:
m = (g + d) // 2
if tab[m] == val:
return m
elif tab[m] < val:
g = m + 1
else:
d = m - 1
return -1
def tri_selection(tab):
n = len(tab)
for i in range(n - 1):
i_min = i
for j in range(i + 1, n):
if tab[j] < tab[i_min]:
i_min = j
tab[i], tab[i_min] = tab[i_min], tab[i]
def tri_insertion(tab):
for i in range(1, len(tab)):
cle = tab[i]
j = i - 1
while j >= 0 and tab[j] > cle:
tab[j + 1] = tab[j]
j -= 1
tab[j + 1] = cle
# Mesure recherche
print("=== Recherche ===")
for n in [1000, 10000, 100000, 1000000]:
tab = list(range(n))
val = -1 # pire cas : élément absent
debut = time.time()
recherche_seq(tab, val)
t_seq = time.time() - debut
debut = time.time()
recherche_dicho(tab, val)
t_dicho = time.time() - debut
print(f"n={n:>8} séquentielle: {t_seq:.6f}s dichotomie: {t_dicho:.6f}s")
# Mesure tri
print("\n=== Tri ===")
for n in [100, 1000, 5000, 10000]:
tab1 = [random.randint(0, 10000) for _ in range(n)]
tab2 = tab1.copy()
debut = time.time()
tri_selection(tab1)
t_sel = time.time() - debut
debut = time.time()
tri_insertion(tab2)
t_ins = time.time() - debut
print(f"n={n:>6} sélection: {t_sel:.4f}s insertion: {t_ins:.4f}s")

a. Créer un jeu de données avec 3 classes de points (par exemple des coordonnées (x, y) avec les classes "A", "B", "C").

b. Implémenter la fonction knn(donnees, point, k).

c. Tester avec k = 1, k = 3 et k = 7 pour un même point. Observer comment le résultat change.

d. Créer un tableau montrant les résultats de classification pour 5 points de test avec chaque valeur de k.

Correction
import math
import random
def distance(p1, p2):
return math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)
def knn(donnees, point, k):
distances = []
for coords, classe in donnees:
d = distance(point, coords)
distances.append((d, classe))
distances.sort(key=lambda x: x[0])
compteur = {}
for _, classe in distances[:k]:
compteur[classe] = compteur.get(classe, 0) + 1
return max(compteur, key=compteur.get)
# Jeu de données : 3 groupes
donnees = [
((1, 1), "A"), ((2, 1), "A"), ((1, 2), "A"), ((2, 2), "A"),
((5, 5), "B"), ((6, 5), "B"), ((5, 6), "B"), ((6, 6), "B"),
((1, 5), "C"), ((2, 6), "C"), ((1, 6), "C"), ((2, 5), "C"),
]
# Points de test
tests = [(3, 3), (1, 4), (4, 5), (2, 1.5), (4, 2)]
print(f"{'Point':>10} | k=1 | k=3 | k=7")
print("-" * 40)
for point in tests:
r1 = knn(donnees, point, 1)
r3 = knn(donnees, point, 3)
r7 = knn(donnees, point, 7)
print(f"{str(point):>10} | {r1} | {r3} | {r7}")

a. Implémenter l'algorithme glouton de rendu de monnaie.

b. Tester avec le système euro [1, 2, 5, 10, 20, 50, 100, 200] pour les sommes 63, 126, 289. Vérifier que le résultat est optimal.

c. Tester avec le système [1, 3, 4] pour les sommes 6, 9, 12. Identifier les cas où le glouton échoue.

d. Pour les cas où le glouton échoue, trouver la solution optimale à la main.

Correction
def rendu_glouton(somme, pieces):
pieces_triees = sorted(pieces, reverse=True)
resultat = []
for p in pieces_triees:
while somme >= p:
resultat.append(p)
somme -= p
return resultat
# Tests avec le système euro
print("=== Système euro ===")
for s in [63, 126, 289]:
r = rendu_glouton(s, [1, 2, 5, 10, 20, 50, 100, 200])
print(f"{s} = {r} ({len(r)} pièces)")
# Tests avec le système [1, 3, 4]
print("\n=== Système [1, 3, 4] ===")
for s in [6, 9, 12]:
r = rendu_glouton(s, [1, 3, 4])
print(f"{s} = {r} ({len(r)} pièces)")

Résultats :

  • Système euro : toujours optimal
  • [1, 3, 4] :
    • 6 : glouton = [4, 1, 1] (3 pièces), optimal = [3, 3] (2 pièces)
    • 9 : glouton = [4, 4, 1] (3 pièces), optimal = [3, 3, 3] (3 pièces) - même nombre
    • 12 : glouton = [4, 4, 4] (3 pièces), optimal = [4, 4, 4] ou [3, 3, 3, 3] - glouton optimal

Le jeu de données Iris contient 150 fleurs de 3 espèces (Setosa, Versicolor, Virginica) caractérisées par 4 mesures : longueur et largeur des sépales, longueur et largeur des pétales.

a. Créer un jeu de données simplifié avec au moins 5 fleurs par espèce, en utilisant les mesures de pétales (longueur, largeur) :

iris = [
((1.4, 0.2), "Setosa"), ((1.3, 0.2), "Setosa"),
((1.5, 0.2), "Setosa"), ((1.4, 0.3), "Setosa"),
((1.7, 0.4), "Setosa"),
((4.7, 1.4), "Versicolor"), ((4.5, 1.5), "Versicolor"),
((4.9, 1.5), "Versicolor"), ((4.0, 1.3), "Versicolor"),
((4.6, 1.5), "Versicolor"),
((6.0, 2.5), "Virginica"), ((5.1, 1.9), "Virginica"),
((5.9, 2.1), "Virginica"), ((5.6, 1.8), "Virginica"),
((5.8, 2.2), "Virginica"),
]

b. Implémenter les fonctions distance(p1, p2) et knn(donnees, point, k).

c. Classifier les fleurs suivantes et vérifier les résultats :

  • (1.6, 0.3) → devrait être Setosa
  • (4.3, 1.4) → devrait être Versicolor
  • (5.5, 2.0) → devrait être Virginica

d. Diviser le jeu de données en un ensemble d'entraînement (80 %) et un ensemble de test (20 %).

e. Évaluer le taux de bonne classification pour k = 1, 3, 5.

f. Afficher une matrice de confusion (combien de Setosa sont classées comme Setosa, combien comme Versicolor, etc.).

Correction
import math
import random
def distance(p1, p2):
return math.sqrt(sum((a - b)**2 for a, b in zip(p1, p2)))
def knn(donnees, point, k):
distances = []
for coords, classe in donnees:
d = distance(point, coords)
distances.append((d, classe))
distances.sort(key=lambda x: x[0])
compteur = {}
for _, classe in distances[:k]:
compteur[classe] = compteur.get(classe, 0) + 1
return max(compteur, key=compteur.get)
iris = [
((1.4, 0.2), "Setosa"), ((1.3, 0.2), "Setosa"),
((1.5, 0.2), "Setosa"), ((1.4, 0.3), "Setosa"),
((1.7, 0.4), "Setosa"),
((4.7, 1.4), "Versicolor"), ((4.5, 1.5), "Versicolor"),
((4.9, 1.5), "Versicolor"), ((4.0, 1.3), "Versicolor"),
((4.6, 1.5), "Versicolor"),
((6.0, 2.5), "Virginica"), ((5.1, 1.9), "Virginica"),
((5.9, 2.1), "Virginica"), ((5.6, 1.8), "Virginica"),
((5.8, 2.2), "Virginica"),
]
# Partie C - Classification
print("=== Classifications ===")
tests = [((1.6, 0.3), "Setosa"), ((4.3, 1.4), "Versicolor"), ((5.5, 2.0), "Virginica")]
for point, attendu in tests:
resultat = knn(iris, point, 3)
ok = "✓" if resultat == attendu else "✗"
print(f" {point} → {resultat} (attendu: {attendu}) {ok}")
# Partie D-E - Évaluation
print("\n=== Évaluation ===")
random.seed(42)
donnees = iris.copy()
random.shuffle(donnees)
n_test = max(1, len(donnees) // 5)
test = donnees[:n_test]
train = donnees[n_test:]
for k in [1, 3, 5]:
correct = 0
for point, vrai_classe in test:
pred = knn(train, point, k)
if pred == vrai_classe:
correct += 1
taux = correct / len(test) * 100
print(f" k={k} : {correct}/{len(test)} correct ({taux:.0f}%)")
# Partie F - Matrice de confusion
print("\n=== Matrice de confusion (k=3) ===")
classes = ["Setosa", "Versicolor", "Virginica"]
matrice = {v: {p: 0 for p in classes} for v in classes}
for point, vrai in test:
pred = knn(train, point, 3)
matrice[vrai][pred] += 1
print(f"{'':>15}", end="")
for c in classes:
print(f"{c:>12}", end="")
print()
for vrai in classes:
print(f"{vrai:>15}", end="")
for pred in classes:
print(f"{matrice[vrai][pred]:>12}", end="")
print()