Connexion élèves

Choisir le(s) module(s) à installer :

Algorithmes utilisant des parcours de tableau

Recherche d'une valeur dans un tableau

Nous allons nous intéresser à un algorithme simple : celui de la recherche d'un élément de valeur donnée dans un tableau.

Un tableau au sens algorithmique est implémenté en Python avec un type construit appelé liste Python, que vous avez déjà étudié en détails ici.

En particulier, vous connaissez donc la syntaxe du test d'appartenance d'une valeur à un tableau :


if valeur in tableau:
		.................		
			

Mais on ne sait pas ce que Python fait derrière cette syntaxe pour trouver l'élément dans le tableau.

Algorithme "naïf"

La méthode "naïve", c'est à dire qui est la plus simple, pour trouver un élément dans un tableau est la suivante :


fonction recherche_seq:
	pour chaque élément successif du tableau :
		si l'élément est la valeur recherchée :
			alors renvoyer VRAI
		fin si
	fin pour
	renvoyer FAUX	
				

→ on parcourt donc le tableau élément après élément; si on trouve la valeur, on sort du parcours et de la fonction en renvoyant VRAI; si on arrive à la fin du parcours et que l'on n'a pas trouvé la valeur, on sort de la fonction en renvoyant FAUX.

Cette façon de procéder est appelée algorithme de recherche séquentielle.

  1. Traduisez l'algorithme en codant une fonction recherche_seq.

    Cette fonction prendra en argument un tableau et une valeur à chercher, et renverra True ou False selon que la valeur est présente ou non dans le tableau.

    Vous testerez votre recherche sur un tableau de 50 valeurs entières entre 1 et 100 générées aléatoirement.

    from random import randint def recherche_seq(tab, valeur): pass

    Lien vers les RÉPONSES

  2. Modifier la fonction de façon à ce qu'elle renvoit :

    • le (ou les) indice(s) dans le tableau de l' (ou des) élément(s) contenant la valeur recherchée dans le cas où celle-ci s'y trouve
    • la valeur -1 dans le cas contraire.

    Réaliser les mêmes tests que ci-dessus.

    from random import randint def recherche_seq_indice(tab, valeur): pass

Comparaison avec l'algorithme utilisé par Python

Est-ce l'algorithme utilisé par Python "en interne" pour tester l'appartenance d'une valeur dans un tableau ? Impossible de le savoir directement...mais nous allons essayer de comparer son temps d'exécution avec celui de notre algorithme "naïf".

Pour cela, nous allons utiliser le module timeit qui permet de comparer le temps d'exécution ( ou de plusieurs exécutions ) d'un script Python :


t = timeit(stmt = "code_à_éxécuter", globals=globals(), number = nombre_de_répétitions)			
			

Compléter le code ci-dessus avec le code des deux fonctions et un tableau de 100 valeurs aléatoires. Lancer le script plusieurs fois de suite, et comparer les valeurs de durée obtenues à chaque fois.

from random import randint from timeit import timeit def recherche_seq(tableau, valeur) : # la fonction de recherche séquentielle pass def recherche_python(tableau, valeur) : # la fonction utilisant le test d'appartenance Python pass # Programme principal tab = ... t1 = timeit(stmt = 'recherche_seq(tab, 15)', globals=globals(), number = 1000) print(t1) t2 = timeit(stmt = 'recherche_python(tab, 15)', globals=globals(), number = 1000) print(t2)

Que peut-on conclure ?

Lien vers les RÉPONSES

Conclusion : complexité temporelle des algorithmes

Coût en temps d'un algorithme

A conditions d'exécution identiques ( même ordinateur, même taille de données à traiter,...) tous les algorithmes ne se valent donc pas : certains sont plus efficaces que d'autre, c'est à dire qu'ils résoudront plus rapidement le problème pour lequel ils sont prévus.

On dit que leur coût en temps (ou coût temporel) est différent : un algorithme efficace aura un coût en temps plus faible qu'un autre algorithme moins efficace.

Dans le cas de notre fonction de recherche séquentielle, l'algorithme a un coût en temps plus grand que celui utilisé en interne par Python, il est donc moins efficace que ce dernier.

( Remarque : on peut aussi comparer les algorithmes en fonction de leur coût en espace, c'est à dire la plus ou moins grande quantité de mémoire dont ils ont besoin pour fonctionner; cependant nous ne nous y intéresserons pas dans ce chapitre et les suivants.)

Essayons d'analyser le coût temporel de cet algorithme, c'est à dire le nombre d'opérations élémentaires à effectuer en fonction du nombre n de données à traiter. Par exemple ici, le nombre d'opérations élémentaires à faire est le nombre de comparaisons à réaliser pour déterminer si un élément du tableau est égal à la valeur recherchée.

Nous ne pouvons pas prévoir quand cet algorithme s’arrête, mais nous pouvons dire que:

Généralement, on s’intéresse au coût au temps d'un algorithme dans le pire des cas; ici, on dira donc que le coût en temps de l'algorithme de recherche séquentielle est de n opérations élémentaires pour n données à traiter.

Complexité temporelle d'un algorithme

La complexité temporelle d'un algorithme correspond à l'évaluation du coût en temps d'un algorithme quand la quantité de données qu'il traite augmente.
Elle dépend souvent de la quantité de données notée n.

Généralement, on s'intéresse là aussi à la complexité temporelle dans le pire des cas.

Dans notre cas, le coût en temps de la recherche dans un tableau augmentera proportionnellement avec la taille du tableau : si le tableau contient 30 000 noms, le pire cas demandera 30 000 étapes; avec 60 000 noms, il y aura 60 000 étapes, etc....: le coût en temps est donc proportionnel au nombre n de données à traiter.

On dira que la complexité d'une recherche séquentielle dans un tableau est "en O(n)", ce que l'on appelle une complexité linéaire.

La notation "O(n)" ( "grand O de n" ou "O de n") est une notation mathématique qui résume la définition précédente et nous évite de dire à chaque fois : "dans le pire des cas", "proportionnel à", ...

Attention, l'étude de la complexité d'un algorithme prend du sens quand n devient très grand; pour n = 10 ou n = 20, cette notion n'a en effet pas beaucoup d'intérêt la plupart du temps car on voit alors peu de différence entre les différents algorithmes !

Quelques exemples de complexité

  • Si cette recherche ne dépendait pas de la taille n du tableau, on dirait qu'elle est en O(1) ( "en temps constant" )
  • Si cette recherche était proportionnelle à n⨯n, on dirait qu'elle est en O(n2) ( "quadratique" )
  • Si un algorithme demande par exemple 2⨯n opérations ou 12⨯n opérations, sa complexité sera tout de même en O(n)
  • Si dans un algorithme apparaissent plusieurs étapes de complexités différentes, c'est la plus grande d'entre elles qui donne la complexité globale de l'algorithme.

Lien avec la durée d'exécution d'un programme

Mais nous ne parlons ici que de la complexité temporelle d'un ALGORITHME !
Si nous voulons savoir quelle sera la durée d'exécution du programme c'est une autre question :

La complexité temporelle d'un algorithme nous donne des indications sur le coût en temps d'un algorithme, MAIS si la complexité est théorique et ne dépend que de l'algorithme, le temps d'exécution lui est une notion pratique et dépend :

  • de la machine sur laquelle on va exécuter le programme
  • des autres programmes qui s'exécutent sur la machine
  • du système d'exploitation de la machine
  • des choix que le programmeur a fait pour coder son algorithme
  • et bien d'autres choses encore...

En conclusion la complexité temporelle N'EST PAS le temps d'exécution d'un programme, elle ne permet de comparer que l'efficacité d'ALGORITHMES entre eux.

Vous trouverez sur cette page un peu plus de détails sur cette notion de complexité des algorithmes.

Extremum ( maximum ou minimum ), moyenne

L'algorithme de recherche d'une valeur dans un tableau est basé sur le parcours du tableau et la comparaison de chaque élément à la valeur recherchée.
Sur cette base nous pouvons imaginer des traitement différents des éléments du tableau.

Tous les algorithmes suivants étant basé sur le parcours intégral d'un tableau, leur complexité temporelle est donc O(n) dans tous les cas.

Recherche du maximum

L'algorithme de recherche d'un maximum est le suivant :


fonction maximum:
	maxi ← premier élément du tableau
	
	pour chaque élément successif du tableau:
		si valeur de l'élément > maxi
			maxi ← valeur de l'élément
		fin si
	fin pour
	
	renvoyer maximum		
				

Vous devez coder la fonction maximum_tableau qui renvoie la valeur maximale dans un tableau.
Vous n'oublierez pas les annotations et la docstring
Tester le bon fonctionnement de votre fonction à l'aide du jeu de tests ci-dessous :

  • maximum_tableau([1, 10, 8, 17, 0, 3, 6, 12, 14, 18, 15, 4, 7, 16, 13, 2, 9, 19, 5, 11]) == 19
  • maximum_tableau([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) == 1
  • maximum_tableau([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) == 19
  • maximum_tableau([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0]) == 19
  • maximum_tableau([48, 89, 75, 12, 31, 80, 86, 52, 52, 75, 26, 94, 48, 70, 76, 9, 6, 64, 71, 31]) == 94
def maximum_tableau(tab): pass

Lien vers les RÉPONSES

Recherche du minimum

  1. écrire l'algorithme permettant de déterminer le minimum dans un tableau
  2. écrire une fonction minimum_tableau renvoyant le minimum d'un tableau
  3. compléter la fonction avec les tests unitaires sur les tableaux du 2.1.
def minimum_tableau(tab): pass

Lien vers les RÉPONSES

Calcul d'une moyenne

L'algorithme de calcul d'une moyenne est le suivant :


fonction moyenne:
	somme ← 0
	
	pour chaque élément successif du tableau:
		ajouter valeur de l'élément à somme
	fin pour
		
	moyenne ← somme / n ( n = nombre d'éléments du tableau )
	
	renvoyer moyenne		
			
  1. écrire une fonction moyenne_tableau renvoyant la moyenne des éléments d'un tableau.
    Vous n'oublierez pas les annotations ( attention au type du résultat renvoyé ! ) et la docstring
  2. compléter la fonction avec quelques tests unitaires bien choisis
  3. utiliser ensuite la fonction avec les tableaux du 2.1.
def moyenne_tableau(tab): pass

Lien vers les RÉPONSES

Applications

Le mot le plus long

Compléter la fonction mot_le_plus_long ci-dessous qui prend en paramètre un tableau non vide tab de mots et renvoie un tuple constitué :

  • du mot le plus long,
  • et de la longueur de ce mot le plus long.

En cas d'égalité entre plusieurs mots ayant la longueur maximale, on renverra celui ayant le plus petit indice.

def mot_le_plus_long(tab): longueur_max = ... # la taille du premier élément du tableau mot_max = ... # le premier élément du tableau for ... in ... : # pour chaque mot dans le tableau if ... > ... : # si le mot est plus long que le plus grand actuel longueur_max = ... # alors on stocke sa longueur mot_max = ... # et le mot lui même return ..., ... # renvoi des résultats ​

Tester votre fonction en utilisant le jeu de tests ci-dessous :


mot_le_plus_long(['chaton', 'licorne', 'ratatouille', 'bip']) == ('ratatouille', 11)
mot_le_plus_long(['chatons', 'licorne', 'or', 'car', 'etoile']) == ('chatons', 7)  # l'ordre des mots 
mot_le_plus_long(['licorne', 'chatons', 'or', 'car', 'etoile']) == ('licorne', 7)  # influe sur le résultat
			

Lien vers les RÉPONSES

Face au Soleil...

On dispose d'un tableau contenant des valeurs entières correspondant à la hauteur de buildings placés les uns à côté des autres.

Le Soleil est situé à gauche des buildings, soit "du côté du début du tableau" :

Buildings face au Soleil

Les hauteurs des buildings ci-dessus seraient codées par le tableau : buildings = [4, 2, 5, 4, 6]

Le but est de compter le nombre de buildings au sommet desquels on verra effectivement le Soleil couchant; pour cela, la hauteur d'un building doit être strictement supérieure à celle des buildings situés à sa gauche : ainsi ci-dessus, seuls le premier, le troisième et le dernier building verront le Soleil couchant, soient les éléments du tableau aux indices 0, 2 et 4.

  1. écrire une fonction building_sunset, qui :
    • prend comme paramètres un tableau d'entier modélisant les hauteurs des buildings
    • renvoie le nombre de buildings au sommet desquels on voit le Soleil couchant
  2. écrire une deuxième version de la fonction, qui renvoie cette fois les indices des buildings dans le tableau de hauteurs.
  3. tester les fonctions avec un tableau de valeurs entière aléatoires.
def building_sunset(buildings): pass def building_sunset_v2(buildings): pass b1 = [4, 2, 5, 4, 6] # doit renvoyer 3 ou [0, 2, 4]

Lien vers les RÉPONSES

Plus longue répétition d'une valeur

Gelées

Tous différents

Suivi par un GPS de sport

Introduction

Lors de la pratique de nombreux sports on peut utiliser un périphérique GPS pour suivre nos performances. Ces montres relèvent le position du sportif, et le temps tout au long de l'activité sportive.
On peut ainsi retrouver son parcours sur une carte, la durée écoulée pour chaque kilomètre, la vitesse instantanée, le kilomètre le plus rapide lors de l'activité etc...
montre GPS

Données disponibles

Les donnés brutes extraites d'une montre GPS se présentent sous cette forme pour chaque point enregistré :

			
<Trackpoint>
	<Time>2021-10-17T09:58:15Z</Time>
   <Position>
   	<LatitudeDegrees>45.5668804</LatitudeDegrees>
      <LongitudeDegrees>5.9177179</LongitudeDegrees>
    </Position>
    <AltitudeMeters>273.1518555</AltitudeMeters>
    <DistanceMeters>0.1047208</DistanceMeters>
    <SensorState>Absent</SensorState>
</Trackpoint>
 				

Nous allons simplifier cela et vous travaillerez à partir d'un tableau python contenant la durée de chaque tranche de 100 m effectuée :


[49.49, 32.37, 33.02, 33.55, 42.14, 34.6, 42.38, 32.34, 41.6, 37.03, 41.22, 37.9]			
			

Recherche du meilleur 100 m

Vous devez dans cette partie coder une fonction qui renvoie le temps du meilleur 100 m.
Cette fonction prendra en argument un tableau contenant les durées de toutes les tranches de 100 m.

Pour simplifier votre travail vous trouverez ci-dessous un code qui génère un tableau à traiter pour un parcours de 10 km.

from random import randint def meilleur_100m(L: list)->float: pass parcours = [randint(3000,5000)/100 for i in range (1000)]

Lien vers les RÉPONSES

Recherche du meilleur kilomètre

Rechercher le meilleur 100 m c'est facile, mais sur une course de 10 km cela a peu d'intérêt.
Nous allons donc tenter de rechercher le meilleur km dans le tableau précédent.

record

Mais sur une course de 10 km il n'y a pas que 10 fois 1 km à vérifier...comme vous pouvez le constater sur le schéma ci-dessous :

somme glissante

Vous devez dans cette partie coder une fonction qui renvoie le temps du meilleur km.
Cette fonction prendra en argument un tableau contenant les durées de toutes les tranches de 100 m.

from random import randint def meilleur_km(L: list)->float: pass parcours = [randint(3000,5000)/100 for i in range (1000)]

Lien vers les RÉPONSES

Preuve d'algorithme : Terminaison et Correction

Nous allons dans cette partie commencer à nous poser une question que nous reverrons en algorithmique cette année : la question de la preuve d'un algorithme.

Pourquoi une preuve ?

Vous connaissez le théorème ci-contre et vous savez très bien dessiner un triangle rectangle quelconque et vérifier que ce théorème est correct.
Mais vous savez aussi que ce théorème a été démontré et qu'au delà de tous les tests que l'on pourrait faire, il a été vérifié théoriquement.

Théorème de Pythagore

Et bien c'est la même chose pour les algorithmes; on peut :

Mais il est aussi nécessaire de les démontrer.

Comment structurer le raisonnement ?

Nous n'allons pas aujourd’hui démontrer un algorithme mais uniquement voir les deux choses qu'il faudrait démontrer...

Terminaison

La première chose à démontrer c'est sa terminaison, c'est à dire le fait qu'il va s'arrêter, en dehors de toute autre considération.

En effet un algorithme, la plupart du temps, effectue un grand nombre d'actions et utilise donc une boucle. Il est important de savoir si l'algorithme va s'arrêter ( sans chercher à savoir s'il remplit bien son rôle ).

Par exemple dans l'algorithme de recherche de maximum que nous venons de voir :


mettre 0 dans maximum

pour tous les éléments du tableau
	si élément > maximum
		mettre élément dans maximum

renvoyer maximum		
				

On peut constater que :

Comme le tableau a nécessairement une longueur finie ( contrairement aux mathématiques, rien ne peut être infini en informatique, sauf certaines boucles mal codées...) l'algorithme va nécessairement se terminer.
Nous venons de prouver la terminaison de l'algorithme ( sans, encore une fois, avoir prouvé que celui ci nous donnait bien le maximum du tableau ).

Correction et invariant de boucle

La deuxième chose à démontrer pour un algorithme c'est sa correction, le fait qu'il est correct, qu'il remplit bien le rôle pour lequel il a été conçu.
Par exemple :

Et cette démonstration va s'appuyer sur une notion importante : l'invariant de boucle.
En effet un algorithme, la plupart du temps, effectue un grand nombre d'actions et utilise donc une boucle.
Pour appuyer la démonstration il faut donc trouver une proposition vraie tout au long du déroulement de l'algorithme.

Par exemple dans l'algorithme de recherche que nous venons de voir :


pour tout les éléments du tableau
	si l'élément est l'élément recherché
		renvoyer Vrai
renvoyer Faux	
			

Un invariant de boucle pourrait être :


"l'élément cherché est placé entre l'élément courant et la fin du tableau" 
			

Ces notions de démonstration et d'invariant de boucle ne sont pas faciles a aborder, car, la plupart du temps, la compréhension de l'algorithme semble nous rendre inutile sa démonstration... Mais sur des algorithmes plus complexes c'est une démarche mathématique importante.

Conclusion

Quand on a démontré que :

On a alors réalisé une preuve mathématique de l'algorithme. On est sur de son bon fonctionnement dans n'importe quelle situation.
Il reste alors à le coder et un "bon" algorithme peut être "mal" codé, il faut donc bien faire attention, dans la phase d'implémentation, à respecter l'algorithme tel qu'il a été écrit.

QCM

Voici quelques questions pour tester si vous avez bien compris les notions de complexité et de preuve d'un algorithme.