Algo Parcours Suite

Identification

Infoforall

5 - Différents algorithmes de parcours


Parcourir un tableau élément par élément nécessite un algorithme dont le coût est linéaire.

Nous allons voir aujourd'hui d'autres algorithmes ayant le même coût mais permettant de faire d'autres choses.

Et aujourd'hui, nous allons voir comment fonctonne un tableur. Quelqu'un l'a bien programmé non ?

Image d'un tableur
Un tableur ?

Et en fin d'activité, nous parlerons un peu d'informatique quantique, informatique qui va rebattre les cartes en terme de coût justement.

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Prérequis ✎ : Savoir utiliser while, for, if et les tableaux

Evaluation ✎ : questions 02-03-07-13-14-15-20-21.

Documents de cours : open document ou pdf

Le cours : open document ou pdf

1 - Rappel : recherche d'un élément

Rappel important pour comprendre les codes de cette activité

Sur la première ligne de la fonction, on donne des indications sur les types attendus des entrées et de la sortie.

def rechercher(t:list, x:'Element') -> int

Ccette documentation n'accepte que les types natifs, comme list.

Si vous voulez fournir une autre indication, il faut la fournir sous forme de string, comme 'Element'.

✔ 01° Réaliser les tâches demandées :

  1. Placez en mémoire la fonction proposée ci-dessous : elle implémente l'algorithme de recherche linéaire en utilisant un while (tiré de Algo 3).
  2. Vérifiez que les deux affichages de tests proposés correspondent bien aux résultats attendus.
  3. Rajoutez deux autres exemples d'utilisation et vérifiez que les résultats soient bons.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
def rechercher(t:list, x:'Element') -> int: """Renvoie l'indice de la première occurrence de x dans t, -1 si non présent""" i = 0 while i < len(t) and t[i] != x: i = i + 1 if i < len(t): reponse = i else: reponse = -1 return reponse # PROGRAMME DE TEST test = [10, 20, 30, 40, 50] res1 = rechercher(test, 20) print(res1) res2 = rechercher(test, 60) print(res2)

⚙ 02° Lancer le programme en mémoire et tester pour vérifier que cette nouvelle implémentation utilisant un for (vue dans Algo 3) répond exactement comme la précédente.

Question :

Expliquer pourquoi la fonction ne renvoie pas systématiquement -1 alors que la dernière instruction de la fonction (L7) est return -1.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
def rechercher(t:list, x:'Element') -> int: """Renvoie l'indice de la première occurrence de x dans t, -1 si non présent""" for i in range(len(t)): if t[i] == x: return i return -1 # Si on arrive ici, c'est qu'on a jamais rencontré la ligne précédente ! # PROGRAMME DE TEST test = [10, 20, 30, 40, 50] res1 = rechercher(test, 20) print(res1) res2 = rechercher(test, 60) print(res2)

...CORRECTION...

Tout simplement car on sort définitivement de la fonction lorsqu'on rencontre un return. Si on rencontre celui de la ligne 6, on ne rencontrera donc pas celui de la ligne 7.

Passons à un algorithme légérement différent : on voudrait obtenir la dernière position de l'élément s'il apparaît plusieurs fois dans le tableau. Pour cela, nous voulons partir de la dernière case et ensuite remonter jusqu'à la première.

1.1 Algorithme de la recherche linéaire (dernière occurrence)

En rouge, les modifications par rapport à l'algorithme de la première occurrence.

Description
  • NOM : RECHERCHE LINEAIRE de la dernière occurrence
  • ENTREES :
    • t : un tableau contenant n éléments.
    • x : l'élément qu'on recherche dans le tableau
  • SORTIE : un entier valant
    • soit l'indice où se trouve la dernière (et peut-être) seule occurrence de l'élément cherché.
    • soit une réponse (VIDE ou -1) signalant l'absence de l'élément x.
Pseudo-code
Recherche linéaire

Avec une réponse -1 en cas de valeur non trouvée.

RECHERCHE LINEAIRE(t:tableau, x:élément) -> entier

    in - 1

    TANT QUE i 0 ET que t[i] est différent de x

      ii - 1

    Fin TANT QUE

    SI i0

      reponsei

    SINON

      reponse-1

    Fin Si

    RENVOYER reponse

⚙ 03-A° Quelle est la boucle associée naturellement au problème de la dernière occurrence ?

  1. Une boucle bornée (implémentée facilement avec for en Python)
  2. Une boucle non bornée (implémentée facilement avec while en Python)

...CORRECTION...

C'est encore une boucle while puisqu'on peut s'arrêter dès qu'on aura trouver la bonne case. La différence c'est que cette fois, on part de la dernière case et on remonte vers la première.

⚙ 03-B° En vous aidant au besoin du code proposé question 1 pour la recherche de la première occurrence, compléter le code interne de la fonction ci-dessous pour qu'elle corresponde à la recherche de la dernière occurrence. CONSIGNE : utilisez la version while.

1 2 3 4 5 6 7 8 9 10 11 12 13
def rechercher(t:list, x:'Element') -> int: """Renvoie l'indice de la DERNIERE occurrence de x dans t, -1 si non présent""" return -1 # Si on arrive ici, c'est qu'on a jamais rencontré l'élément, en partant de la fin ! # PROGRAMME DE TEST test = [10, 20, 30, 10, 50] res1 = rechercher(test, 10) print(res1) res2 = rechercher(test, 60) print(res2)

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
def rechercher(t:list, x:'Element') -> int: """Renvoie l'indice de la DERNIERE occurrence de x dans t, -1 si non présent""" i = len(t) - 1 while i >= 0 and t[i] != x: i = i - 1 if i >= 0: reponse = i else: reponse = -1 return reponse # PROGRAMME DE TEST test = [10, 20, 30, 10, 50] res1 = rechercher(test, 10) print(res1) res2 = rechercher(test, 60) print(res2)

Nous allons maintenant implémenter cette recherche de la dernière occurrence en utilisant une boucle for associée à un return

⚙ 04-A° On rappelle qu'en Python, on peut transmettre 3 valeurs lorsqu'on utilise la fonction range() :

  1. La valeur de départ :
  2. La borne finale exclue, c'est à dire la première valeur qui sera interdite, la valeur "presque finale" ;
  3. Le pas, c'est à dire le gain ou la perte à chaque tour.

Question : expliquer les valeurs prises par i lorsqu'on utilise la boucle for ci-dessous (dans le contexte d'une exploration de tableau) :

for i in range(len(t)-1, -1, -1):

Si le petit rappel ci-dessous ne vous permet pas de remettre vos souvenirs à jour, allez voir le bout de cours situé ci-dessous.

...CORRECTION...

for i in range(len(t)-1, -1, -1):

  1. La valeur de départ : len(t)-1
  2. Cela veut donc dire qu'on va commencer par prendre l'indice de la dernière case du tableau.

  3. La borne finale exclue : -1
  4. Cela veut donc dire qu'on ne pourra pas atteindre -1, on va s'arrêter juste avant.

  5. Le pas : -1
  6. Cela veut donc dire qu'on diminue i de 1 à chaque tour de boucle.

On part de la case de droite et on perd 1 à chaque tour. Puisqu'on décroit et qu'on ne peut pas atteindre -1, cela veut dire qu'on va s'arrêter en 0 !

Bilan : on va obtenir les indices des cases, de la plus dernière à la première (0).

(Rappel si besoin) Boucle POUR : utilisation de range()

Visualisation de range sous forme d'un tableau

L'évaluation de range(5) ne renvoie pas réellement un simple tableau mais on peut représenter la réponse sous forme d'un tableau à l'aide de la fonction native list().

>>> list(range(5)) [0, 1, 2, 3, 4]

Cela va nous permettre de donner du sens à cette syntaxe.

(Rappel si besoin)..1 Un seul argument : borne finale exclue

Lorsqu'on utilise range(10), l'interpréteur Python comprendra :

  • qu'on veut commencer à 0 ;
  • que la borne finale exclue (la valeur "presque" finale qu'on ne pourra pas atteindre) est 10 ;
  • qu'on incrémente la variable de boucle de +1 à chaque tour de boucle.

Visualisons cela dans la console. Attention, il faudra appuyer deux fois sur ENTREE (représentée ici par ) pour valider la boucle dans la console.

>>> list(range(10)) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> for k in range(10): print(k) 0 1 2 3 4 5 6 7 8 9
(Rappel si besoin)..2 Deux arguments envoyés : valeur initiale, finale exclue

Lorsqu'on utilise range(3, 10), l'interpréteur Python comprendra :

  • qu'on veut commencer à 3 ;
  • que la borne finale exclue (la valeur "presque" finale qu'on ne pourra pas atteindre) 10 ;
  • qu'on incrémente la variable de boucle de +1 à chaque tour de boucle.

Visualisons cela dans la console.

>>> list(range(3, 10)) [3, 4, 5, 6, 7, 8, 9] >>> for k in range(3, 10): print(k) 3 4 5 6 7 8 9
(Rappel si besoin)..3 Trois arguments envoyés : initiale, finale exclue, pas

Lorsqu'on utilise range(3, 10, 2), l'interpréteur Python comprendra :

  • qu'on veut commencer à 3 ;
  • que la borne finale exclue (la valeur "presque" finale qu'on ne pourra pas atteindre) est 10 ;
  • qu'on incrémente la variable de boucle de +2 à chaque tour de boucle (on dira que le pas est de +2).

Visualisons cela dans la console.

>>> list(range(3, 10, 2)) [3, 5, 7, 9] >>> for k in range(3, 10, 2): print(k) 3 5 7 9
On peut fournir un pas négatif. Ca complique la compréhension de la vraie valeur finale : avec range(10, 1, -3), on indique qu'on part de 10 et qu'on décroit jusqu'à 2 puisque la première valeur interdite est 1.
>>> list(range(10, 1, -3)) [10, 7, 4] >>> for k in range(10, 1, -3): print(k) 10 7 4

⚙ 04-B° Compléter la fonction rechercher() ci-dessous pour qu'elle implémente avec un FOR la recherche de la position de la dernière occurrence.

1 2 3 4 5 6 7 8 9 10 11 12 13
def rechercher(t:list, x:'Element') -> int: """Renvoie l'indice de la DERNIERE occurrence de x dans t, -1 si non présent""" return -1 # PROGRAMME DE TEST test = [10, 20, 30, 10, 50] res1 = rechercher(test, 10) print(res1) res2 = rechercher(test, 60) print(res2)

Au lancement du programme, vous devriez voir ceci à l'écran si votre fonction fait correctement son travail :

0 -1

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
def rechercher(t:list, x:'Element') -> int: """Renvoie l'indice de la DERNIERE occurrence de x dans t, -1 si non présent""" reponse = -1 for i in range(len(t)-1, -1, -1): if t[i] == x: reponse = i return reponse # PROGRAMME DE TEST test = [10, 20, 30, 10, 50] res1 = rechercher(test, 10) print(res1) res2 = rechercher(test, 60) print(res2)

⚙ 05-A° Si on regarde le fonctionnement de nos fonctions, on voit que :

  • Dans le meilleur des cas, on ne doit fouiller qu'une seule case car la valeur cherchée se trouve dans la première case qu'on fouille.
  • Dans le pire des cas, on doit fouiller toutes les cases.

Question

Quels sont les coûts respectifs du meilleur et du pire des cas ?

...CORRECTION...

Dans le meilleur des cas, le coût est CONSTANT car on ne lit qu'une case.

Dans le pire des cas, le coût est LINEAIRE car on lit toutes les cases avant de répondre.

⚙ 05-B° Quel est alors le coût de cet algorithme ?

  1. Constant
  2. Logarithmique
  3. Linéaire
  4. Quadratique
  5. Exponentiel

...CORRECTION...

On prend le coût le moins performant : le coût linéaire.

⚙ 05-C° Laquelle de ces deux formules correspond au coût linéaire de la recherche linéaire :

  • 𝞗(n) (qu'on peut traduire approximativement par "toujours exactement linéaire")
  • 𝓞(n) (qu'on peut traduire approximativement par "linéaire ou mieux")

...CORRECTION...

La recherche séquentielle est en 𝓞(n) puisqu'elle est linéaire ou mieux.

2 - Recherche de valeur maximale ou minimale

Nous n'avons pas le choix : il va falloir lire les éléments un par un, jusqu'au bout.

Il s'agit donc bien d'un algorithme linéaire mais puisqu'on va nécessairement au bout à chaque fois, on va certainement pouvoir écrire 𝞗(n).

✔ 06° Utiliser l'animation ci-dessous pour visualiser le déroulement de la recherche du maximum.

Quel est le premier maximum détecté lors de la recherche ?

Reste-il le maximum tout au long de la recherche ?

Quel est le dernier maximum détecté ? Reste-il le maximum jusqu'au bout cette fois ?

Indice i 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19
Case t[i] 60 89 15 56 82 1 86 97 24 86 75 6 14 69 98 31 37 89 91 56

Le principe est de lire les éléments un par un, et d'écraser les anciennes valeurs temporaires du maximum si on en découvre une plus grande.

2.1 Algorithme du maximum (variante while)

Exemple
t = [13, 18, 89, 1024, 45, -12, 18]  ⇒   RECHERCHER_MAXIMUM   ⇒ 
et 1024
longueur = 7
En français
  1. On initialise le max_temporaire avec le contenu de la première case du tableau ;
  2. L'indice i pointe sur 1, la prochaine case à lire ;
  3. On parcourt les cases une à une TANT QUE l'indice i est bien un indice valide pour le tableau ;
    • Si la case actuelle contient une valeur plus grande que le max_temporaire, on stocke cette valeur dans max_temporaire ;
    • On passe à la case suivante en incrémentant i.
  4. On renvoie la valeur stockée dans max_temporaire.
Description
  • NOM : MAXIMUM
  • ENTREE : t : un tableau NON VIDE contenant n éléments.
  • SORTIE : un élément qui correspond au plus grande élément présent dans le tableau (ou sa première occurrence).
Pseudo-code
Recherche du maximum

MAXIMUM(t:tableau NON VIDE) -> élément

    max_temporairet[0]

    i ← 1

    TANT QUE i < n

      SI t[i] > max_temporaire

        max_temporairet[i]

      Fin du SI

      ii + 1

    Fin du TANT QUE

    RENVOYER max_temporaire

⚙ 07-A° Réaliser l'implémentation de cet algortihme (de la variante while) via une fonction Python maximum() dont voici le prototype :

maximum(t:'tableau NON VIDE') -> 'un élément du tableau'

On vous donne ci-dessous le prototype et quelques affichages à tester.

1 2 3 4 5 6 7 8 9 10 11 12 13 14
def maximum(t:'list NON VIDE') -> 'élément': """Renvoie la valeur maximale lue dans le tableau non vide""" pass # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = maximum(test) print(m1) test = [10, 20, 3000, 10, 50, 40] m2 = maximum(test) print(m2)

Au lancement du programme, vous devriez alors voir ceci à l'écran :

50 3000

...CORRECTION...

1 2 3 4 5 6 7 8 9 10
def maximum(t:'list NON VIDE') -> 'élément': """Renvoie la valeur maximale lue dans le tableau non vide""" max_temporaire = t[0] i = 1 while i < len(t): if t[i] > max_temporaire: max_temporaire = t[i] i = i + 1 return max_temporaire

⚙ 07-B° Cette version de l'algorithme nécessite d'examiner le contenu de toutes les cases : est-il vraiment judicieux d'utiliser une boucle non bornée pour la recherche du maximum ?

...CORRECTION...

Puisqu'on sait qu'on va devoir lire tous les éléments pour trouver le maximum, il serait plus judicieux d'utiliser une boucle bornée.

2.2 Algorithme du maximum (variante for)

En français
  1. On initialise le max_temporaire avec le contenu de la première case du tableau.
  2. POUR CHAQUE autre indice de case possible dans le tableau
    • Si la case actuelle contient une valeur plus grande que le max_temporaire, on stocke cette valeur dans max_temporaire.
  3. On renvoie la valeur stockée dans max_temporaire.
Pseudo-code
Recherche du maximum

MAXIMUM(t:tableau NON VIDE) -> élément

    max_temporairet[0]

    POUR i variant de 1 à (n -1)

      SI t[i] > maxi

        max_temporairet[i]

      Fin du SI

    Fin du POUR

    RENVOYER max_temporaire

POINT A SURVEILLER

Notez bien que

  • dans les algorithmes, les boucles FOR sont données avec valeur finale incluse.
  • en Python, la valeur finale d'un range est exclue, c'est la valeur "presque finale".

Je n'y peux rien. C'est à surveiller lors de l'implémentation.

⚙ 08-A° Réaliser l'implémentation de cet algortihme (de la variante for) via une fonction Python maximum() dont voici le prototype :

maximum(t:'tableau NON VIDE') -> 'un élément du tableau'

On vous donne ci-dessous le prototype et quelques affichages à tester.

1 2 3 4 5 6 7 8 9 10 11 12 13 14
def maximum(t:'list NON VIDE') -> 'élément': """Renvoie la valeur maximale lue dans le tableau non vide""" pass # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = maximum(test) print(m1) test = [10, 20, 3000, 10, 50, 40] m2 = maximum(test) print(m2)

Au lancement du programme, vous devriez alors voir ceci à l'écran :

50 3000

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
def maximum(t:'list NON VIDE') -> 'élément': """Renvoie la valeur maximale lue dans le tableau non vide""" max_temporaire = t[0] for i in range( 1, len(t) ): if t[i] > max_temporaire: max_temporaire = t[i] return max_temporaire # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = maximum(test) print(m1) test = [10, 20, 3000, 10, 50, 40] m2 = maximum(test) print(m2)

⚙ 08-B° Trois questions liées à la fonction corrigée ci-dessous :

  1. Ligne 5 : quelle va être la première valeur de la variable de boucle i du POUR ?
  2. Ligne 5 : pourquoi noter len(t) dans cette fonction Python alors qu'on a POUR i variant de 1 à (longueur -1) dans l'algorithme ?
  3. Quelle ligne poserait problème si l'utilisateur n'avait pas respecté la précondition NON VIDE et avait envoyé un tableau vide ?
1 2 3 4 5 6 7 8
def maximum(t:'Tableau NON VIDE') -> 'Elément': """Renvoie la valeur maximale lue dans le tableau NON VIDE""" max_temporaire = t[0] for i in range( 1, len(t) ): if t[i] > max_temporaire: max_temporaire = t[i] return max_temporaire

...CORRECTION...

  1. La variable de boucle i commence à 1. Par défaut, si on ne précise pas la valeur de départ, l'interpréteur Python place 0. Mais, ici, on a placé la valeur 1.
  2. Dans l'algorithme, on précise que la dernière valeur doit être longueur-1, la borne est incluse. En Python, la borne est exclue. On donne donc la valeur "presque" finale qui est en réalité la première valeur interdite. On indique donc longeur pour dire qu'on ne peut pas aller jusqu'à longueur et qu'on va donc s'arrêter en longueur - 1.
  3. C'est la ligne 4 avec max_temporaire = t[0] qui posera problème avec un tableau vide, puisque l'élément 0 n'existe pas dans ce cas. On va se retrouver avec un IndexError.

⚙ 08-C° Pour information, une lecture comme t[i] est une opération à coût constant.

Justifiez que le coût de cette fonction est linéaire en répondant mentalement à ces questions :

    Opération avant la boucle

  1. L4 : Quel est le coût de la ligne 4 ?
  2. La boucle

  3. L5 : Combien de tours de boucle la ligne 5 provoque-t-elle (on notera n le nombre de cases du tableau) ?
  4. L6 : Quel est le coût de la ligne 6 ?
  5. L7 : Quel est le coût de la ligne 7 ?
  6. L6-7 : Quel est le coût des lignes 6-7 ?
  7. L5-6-7 : En déduire le coût de la boucle L5-L6-L7.
  8. Opération après la boucle

  9. L8 : Quel est le coût de la ligne 8 ?
  10. Conclusion

  11. L4-(5-6-7)-8 : Conclure sur le coût de la fonction.
1 2 3 4 5 6 7 8
def maximum(t:'Tableau NON VIDE') -> 'Elément': """Renvoie la valeur maximale lue dans le tableau NON VIDE""" max_temporaire = t[0] for i in range( 1, len(t) ): if t[i] > max_temporaire: max_temporaire = t[i] return max_temporaire

...CORRECTION...

    Ligne 4

    max_temporaire = t[0]

  1. Cette ligne contient un accès à une case de tableau (constant) et une affectation (constante).
  2. On a donc un coût constant pour cette ligne : "constant + constant = constant". En notation asymptotique : 𝞗(1).

    Ligne 5

  3. On boucle en faisant varier i de 1 à n-1. On va donc réaliser (n-1) tours.
  4. Ligne 6

    if t[i] > max_temporaire:

  5. On accède à une case de tableau (coût constant), à une variable (coût constant) et on fait une comparaison (coût constant). Cette ligne est donc à coût constant : "constant + constant + constant = constant". En notation asymptotique : 𝞗(1+1+1) = 𝞗(1).
  6. Ligne 7

    max_temporaire = t[i]

  7. On accède à une case de tableau (coût constant) et on réalise une affectation (coût constant). Cette ligne est donc à coût constant : "constant + constant = constant". En notation asymptotique : 𝞗(1+1) = 𝞗(1).
  8. Le coût des lignes 6-7 est donc constant. "constant + constant = constant". En notation asymptotique : 𝞗(1+1) = 𝞗(1).

  9. Lignes 5-6-7

  10. On réalise donc (n-1) fois un bloc à coût constant. On obtient donc un coût linéaire.
  11. Si on veut l'écrire avec une notation asymptotique : 𝞗((n-1)*1) = 𝞗(n*1) = 𝞗(n) puisqu'on élimine les facteurs lorsqu'on cherche à connaître uniquement la forme de l'évolution lorsque n devient très grand.

    Ligne 8

    return max_temporaire

  12. On accède à une variable (coût constant) et on renvoie ce qu'on vient d'évaluer (coût constant). Cette ligne est donc à coût constant : "constant + constant = constant". En notation asymptotique : 𝞗(1+1) = 𝞗(1).
  13. Au total

  14. On additionne les coûts des 3 phases, et on obtient un coût linéaire puisque "constant + linéaire + constant = linéaire". En notation asymptotique : 𝞗(1+n+1) = 𝞗(n).

⚙ 08-D° Lire cette justification beaucoup moins détaillée qui correspond à la réponse attendue sur votre copie si on vous demande le coûut de la fonction.

Vous devez être capable d'écrire quelque chose de similaire.

  • La ligne 4 est à coût constant.
  • Les instructions du corps de la boucle (L6-L7) sont à coût constant et on la réalise (n-1) fois. On en déduit que le coût de la boucle est en ((n-1)*1) et donc linéaire.
  • La ligne 8 est à coût constant.
  • Au total, la fonction est donc à coût linéaire.

⚙ 09-A° Réaliser la fonction indice_du_max() pour qu'elle renvoie maintenant plutôt la position du maximum dans le tableau (à savoir son indice).

indice_du_max(t:'tableau NON VIDE') -> int

1 2 3 4 5 6 7 8 9 10 11 12 13 14
def indice_du_max(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur maximale lue dans le tableau NON VIDE""" pass # Programme de test test = [10, 20, 30, 10, 50, 40] i1 = indice_du_max(test) print(i1) test = [10, 20, 3000, 10, 50, 40] i2 = indice_du_max(test) print(i2)

Au lancement du programme, vous devriez alors voir ceci à l'écran :

4 2

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
def indice_du_max(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur maximale lue dans le tableau NON VIDE""" i_du_max = 0 for i in range( 1, len(t) ): if t[i] > t[i_du_max]: i_du_max = i return i_du_max # Programme de test test = [10, 20, 30, 10, 50, 40] i1 = indice_du_max(test) print(i1) test = [10, 20, 3000, 10, 50, 40] i2 = indice_du_max(test) print(i2)

⚙ 09-B° Justifiez que le coût de cette fonction est linéaire.

1 2 3 4 5 6 7 8
def indice_du_max(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur maximale lue dans le tableau NON VIDE""" i_du_max = 0 for i in range( 1, len(t) ): if t[i] > t[i_du_max]: i_du_max = i return i_du_max

...CORRECTION...

  • La ligne 4 est à coût constant.
  • Les instructions du corps de la boucle L6-L7 sont à coût constant et on la réalise (n-1) fois. On en déduit que le coût de la boucle est en ((n-1)*1) et donc linéaire.
  • La ligne 8 est à coût constant.
  • Au total, la fonction est donc à coût linéaire.

⚙ 10° Répondre aux questions liées à ces deux appels :

>>> indice_du_max([20, 500, 10, 40]) ??? >>> indice_du_max([20, 50, 100, 50, 100]) ???
  1. Que vont renvoyer les appels suivants (si la fonction est bien en mémoire !) ?
  2. Quelle est la légère modification à faire en ligne 6 pour renvoyer la dernière occurrence du maximum rencontré plutôt que la première ?
1 2 3 4 5 6 7 8
def indice_du_max(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur maximale lue dans le tableau NON VIDE""" i_du_max = 0 for i in range( 1, len(t) ): if t[i] > t[i_du_max]: i_du_max = i return i_du_max

...CORRECTION...

Les appels renvoient 1 et 2.

Si on veut plutôt obtenir l'indice du dernier maximum détécté, il suffit de remplacer l'opérateur SUPERIEUR par l'opérateur SUPERIEUR OU EGAL : ainsi, l'indice du maximum est modifié même si on tente sur une valeur identique au maximum temporaire.

if t[i] >= t[i_du_max]:

⚙ 11-A° Fournir une fonction indice_du_min() qui renvoie la position du minimum des valeurs (en cas d'égalité, on prendra le cas du premier indice rencontré).

indice_du_min(t:'tableau NON VIDE') -> int

...CORRECTION...

1 2 3 4 5 6 7 8
def indice_du_min(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur minimale lue dans le tableau NON VIDE""" i_du_min = 0 for i in range( 1, len(t) ): if t[i] < t[i_du_min]: i_du_min = i return i_du_min

⚙ 11-B° Justifiez mentalement du coût de cette fonction.

...CORRECTION...

Le coût est encore linéaire et d'ailleurs l'explication est exactement la même.

  • La ligne 4 est à coût constant.
  • Les instructions du corps de la boucle L6-L7 sont à coût constant et on la réalise (n-1) fois. On en déduit que le coût de la boucle est en ((n-1)*1) et donc linéaire.
  • La ligne 8 est à coût constant.
  • Au total, la fonction est donc à coût linéaire.

⚙ 12-A° Modifier la ligne 13 de la fonction minimum() pour qu'elle renvoie la valeur du minimum des valeurs. Pour ceal, elle devra faire appel à la fonction indice_du_min(). Contrainte : vous n'avez pas le droit de rajouter de ligne. On veut juste modifier cette ligne 13.

minimum(t:'tableau NON VIDE') -> 'élément'

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def indice_du_min(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur minimale lue dans le tableau NON VIDE""" i_du_min = 0 for i in range( 1, len(t) ): if t[i] < t[i_du_min]: i_du_min = i return i_du_min def minimum(t:'tableau NON VIDE') -> int: """Renvoie la valeur minimale dans le tableau""" return 0 # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = minimum(test) print(m1) test = [10, 20, 3000, -10, 50, 40] m2 = minimum(test) print(m2)

Au lancement du programme, vous devriez alors voir ceci à l'écran :

10 -10

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def indice_du_min(t:'tableau NON VIDE') -> int: """Renvoie l'indice de la valeur minimale lue dans le tableau NON VIDE""" i_du_min = 0 for i in range( 1, len(t) ): if t[i] < t[i_du_min]: i_du_min = i return i_du_min def minimum(t:'tableau NON VIDE') -> int: """Renvoie la valeur minimale dans le tableau""" return t[ indice_du_min(t) ] # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = minimum(test) print(m1) test = [10, 20, 3000, -10, 50, 40] m2 = minimum(test) print(m2)

⚙ 12-B° Quelqu'un dit que "puisque cette fonction ne contient qu'une ligne de code, son coût est constant". Que pouvez-vous dire de son affirmation ? Justifiez votre réponse.

...CORRECTION...

Elle est fausse.

La ligne 13 fait appel à la fonction indice_du_min() qui est à coût linéaire.

Ensuite on utilise la réponse pour accéder à une case du tableau (à coût constant) puis on renvoie la réponse évaluée (à coût constant).

On obtient des étapes à coût linéaire, constant, constant.

Au total, le coût est donc linéaire.

On pourrait écrire cela ainsi en utilisant les notations asymptotiques : 𝞗(n+1+1) = 𝞗(n).

3 - Somme et valeur moyenne

Dernières fonctions courantes à savoir réaliser : la somme et la moyenne des valeurs contenues dans un tableau.

On va créer une variable-mémoire s, dans laquelle on place la somme progressive des éléments du tableau.

Connaissant la longueur du tableau, on va alors renvoyer s / longueur.

On voit donc que la fonction moyenne() va avoir besoin de faire un appel à la fonction sommme().

⚙ 13-A° Ecrire la fonction sommme() qui renvoie la somme des éléments présents dans un tableau d'entiers.

somme(t:list) -> int|float

Remarque : la barre verticale signifie OU : la fonction va donc renvoyer un entier ou un flottant.

Le tableau pourra être vide et, dans ce cas, il faut renvoyer 0.

1 2 3 4 5 6 7 8 9 10 11 12 13 14
def somme(t:list) -> 'int|float': """Renvoie la somme des éléments présents dans le tableau""" return 0 # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = somme(test) print(m1) test = [10, 20, 30, -10, 50, 40] m2 = somme(test) print(m2)

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
def somme(t:list) -> 'int|float': """Renvoie la somme des éléments présents dans le tableau""" s = 0 for i in range(len(t)): s = s + t[i] return s # Programme de test test = [10, 20, 30, 10, 50, 40] m1 = somme(test) print(m1) test = [10, 20, 30, -10, 50, 40] m2 = somme(test) print(m2)

⚙ 13-B° Justifiez mentalement le coût linéaire de cette fonction sommme().

...CORRECTION...

On retrouve encore les mêmes arguments que sur les fonctions précédentes.

Les lignes 4 et 7 sont à coût constant.

La boucle des lignes 5-6 est à coût linéaire car on réalise n fois une ligne 6 à coût constant.

Au total, on garde donc le coût linéaire.

⚙ 13-C° Cette fonction sommme() est à coût linéaire, mais doit-on noter 𝞗(n) ou 𝓞(n) ?

...CORRECTION...

Il faut lire toutes les cases pour répondre correctement. On doit donc écrire 𝞗(n) pour signifier que le coût est toujours linéaire, il n'y a pas de meilleur ou de pire des cas.

⚙ 14-A° Sous votre fonction somme(), écrire la fonction moyenne() qui renvoie la moyenne des éléments présents dans un tableau d'entiers NON VIDE. Fournir également la documentation de la fonction.

moyenne(t:'list NON VIDE') -> float

Le tableau doit être NON VIDE et contenir des nombres.

Une fois la fonction réalisée, dire pourquoi on a dû rajouter que le tableau devrait être NON VIDE.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def somme(t:list) -> 'int|float': """Renvoie la somme des éléments présents dans le tableau""" s = 0 for i in range(len(t)): s = s + t[i] return s def moyenne(t:'list NON VIDE') -> float: """Renvoie la moyenne des éléments présents dans le tableau""" return 0.0 # Programme de test test = [10, 20] m1 = moyenne(test) print(m1) test = [10, 20, 30] m2 = moyenne(test) print(m2)

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def somme(t:list) -> 'int|float': """Renvoie la somme des éléments présents dans le tableau""" s = 0 for i in range(len(t)): s = s + t[i] return s def moyenne(t:'list NON VIDE') -> float: """Renvoie la moyenne des éléments présents dans le tableau""" return somme(t) / len(t) # Programme de test test = [10, 20] m1 = moyenne(test) print(m1) test = [10, 20, 30] m2 = moyenne(test) print(m2)

Non vide car sinon on aura une division par 0 puisqu'on divise par le nombre de cases.

⚙ 14-B° Quelqu'un dit que "puisque cette fonction ne contient qu'une ligne de code, son coût est constant". Que pouvez-vous dire de son affirmation ? Justifiez votre réponse.

...CORRECTION...

C'est faux.

On décompose la ligne en 3 étapes :

  1. Appel à somme() : coût linéaire.
  2. Appel à len() ; la documentation Python précise que cet appel est à coût constant.
  3. Réaliser une division : coût constant par rapport au nombre de cases du tableau.

On a donc un coût total linéaire.

En conclusion, un petit rappel général sur les coûts et leurs significations.

✎ 15° Ces algorithmes (maximum, minimum, somme et moyenne) ont donc besoin de lire les éléments un par un jusqu'au bout.

Puisque leur coût est linéaire, lorsque n double, le nombre d'opérations à effectuer double aussi.

Avec les coûts ci-dessous, comment décrire l'évolution du nombre d'opérations lorsque n double ?

  1. à coût constant (en 𝞗(1))
  2. à coût logarithmique (en 𝞗(log2(n)) ou 𝞗(lg(n)))
  3. à coût quadratique (en 𝞗(n2))
  4. à coût cubique (en 𝞗(n3))

...CORRECTION...

  1. à coût constant (en 𝞗(1))
  2. Si n double, le nombre d'opérations ne bouge pas.

  3. à coût logarithmique (en 𝞗(log2 (n)) ou 𝞗(lg(n)))
  4. Si n double, le nombre d'opérations est simplement augmenté de 1.

  5. à coût quadratique (en 𝞗(n2))
  6. Si n double, le nombre d'opérations est multiplié par 22, soit 4.

  7. à coût cubique (en 𝞗(n3))
  8. Si n double, le nombre d'opérations est multiplié par 23, soit 8.

Coût des fonctions usuelles

Les fonctionnalités vues aujourd'hui sont très courantes dans la plupart des algorithmes.

Elles sont donc déjà implémentées en Python via des fonctions natives.

Nous allons maintenant les décrire mais attention, la plupart n'ont pas un coût constant même si leur utilisation ne prend qu'une seule ligne !

  • Recherche de la présence avec le mot-clé in à coût linéaire pour un tableau.
  • >>> t = [5, 15, 10, 20] >>> 10 in t True >>> t = [5, 15, 10, 20, 10, 40, 20] >>> 10 in t True >>> 25 in t False
  • Recherche de la position de la première occurence d'un élément EXISTANT avec la méthode index() à coût linéaire.
  • >>> t = [5, 15, 10, 20] >>> t.index(10) 2 >>> t = [5, 15, 10, 20, 10, 40, 20] >>> t.index(10) 2 >>> t.index(25) ValueError: 25 is not in list
  • Recherche de la valeur maximum avec la fonction max() à coût linéaire.
  • >>> t = [5, 15, 10, 20] >>> max(t) 20 >>> t = [5, 15, 10, 20, 10, 40, 20] >>> max(t) 40
  • Recherche de la valeur minimumm avec la fonction min() à coût linéaire.
  • >>> t = [5, 15, 10, 20] >>> min(t) 5 >>> t = [5, 15, 10, 20, 10, 40, 20] >>> min(t) 5
  • Recherche de la somme des éléments avec la fonction sum() à coût linéaire.
  • >>> t = [5, 15, 10, 20] >>> sum(t) 50 >>> t = [5, 15, 10, 20, 10, 40, 20] >>> sum(t) 120
  • Recherche du nombre d'éléments avec la fonction len() à coût constant.
  • >>> t = [5, 15, 10, 20] >>> len(t) 4 >>> t = [5, 15, 10, 20, 10, 40, 20] >>> len(t) 7
  • Recherche de la moyenne des éléments en utilisant sum() et len(), donc à coût linéaire.
  • >>> t = [5, 15, 10, 20] >>> sum(t)/len(t) 8.75 >>> t = [5, 15, 10, 20, 10, 40, 20] >>> min(t)/len(t) 17.142857142857142

4 - Mini-projet : application concrète

Nous allons travailler sur la liste des prénoms déclarés à l'état-civil de 2004 à 2025 à Paris. La liste elle-même est présente à cette adresse si vous voulez aller vérifier : LISTE en OPEN DATA

Nous allons travailler plus tard sur l'OPEN DATA et la possibilité de récupérer facilement ces données dans Python à partir d'un fichier texte.

Pour l'instant, vous allez simplement travailler à partir d'un script Python ne contenant qu'un très loooooooooooooong string multiligne contenant un prénom par ligne. Voici les premiers prénoms qui apparaissent dans cette liste :

1 2 3 4 5
string_prenoms ="""Marianne Abdou Myrtille Anis Ingrid"""

Le caractère de séparation des prénoms est donc le passage à la ligne ('\n' en Python).

Comment obtenir un tableau à partir de ce string ? En utilisant la méthode des strings split().

Voici un rappel de son fonctionnement en pratique :

>>> string_prenoms = """Marianne Abdou Myrtille Anis Ingrid""" >>> string_prenoms 'Mariannee\nAbdou\nMyrtille\nAnis\nIngrid' >>> prenoms = string_prenoms.split('\n') >>> prenoms ['Marianne', 'Abdou', 'Myrtille', 'Anis', 'Ingrid']

⚙ 16° Créer un dossier tp_prenoms dans lequel vous allez placer les deux fichiers qu'il faudra télécharger (avec un clic-droit à l'aide des liens ci-dessous) :

📁 tp_prenoms

📄 prenoms_paris.py

📄 question_finale_algo5.py

Une fois téléchargés et placés au bon endroit :

  1. Vérifier en l'ouvrant avec Thonny que prenoms_paris.py ne contient bien qu'une variable nommée string_prenoms qui est un très long string contenant un prénom par ligne.
  2. Ouvrir le script question_finale_algo5.py pour vérifier qu'il se compose de trois parties :
  3. 1 2 3 4 5 ... 100 101 102
    # 1 - Importation from prenoms_paris import string_prenoms # 2 - Déclaration des fonctions # 3 - PROGRAMME prenoms = string_prenoms.split('\n') del string_prenoms

    Lire ces explications pour comprendre ce que fait le programme pour l'instant :

    • Ligne 3 : on importe la variable string_prenoms depuis le module Python nommé prenoms_paris
    • Ligne 101 : on crée un tableau nommé prenoms en séparant (avec split('\n')) les différents prénoms du string string_prenoms en utilisant le passage à la ligne comme élément séparateur.
    • Ligne 102 : nouveauté, on demande de supprimer de la mémoire le string (qui occupe pas mal de place) maintenant que les données sur les prénoms sont dans le tableau prenoms.

✎ 17 (mini-projet-envoi par ENT)° Utiliser les deux fonctions proposées prenoms_commencant_par() et prenoms_finissant_par() pour vérifier qu'elles fonctionnent ET que vous comprennez vraiment comment elles fonctionnent.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# 1 - Importation from prenoms_paris import string_prenoms # 2 - Déclaration des fonctions def prenoms_commencant_par(c:str, prenoms:list): '''Affiche les prénoms commençant par le caractère c fourni et leurs nombres de lettres''' for prenom in prenoms: if prenom[0] == c: # En Python, l'indice 0 veut dire le premier caractère du string print(f"{prenom} qui comporte {len(prenom)} caractères") def prenoms_finissant_par(c:str, prenoms:list): '''Affiche les prénoms finissant par la lettre fournie et leurs nombres de lettres''' for prenom in prenoms: if prenom[-1] == c: # En Python, l'indice -1 veut dire le dernier caractère du string print(f"{prenom} qui comporte {len(prenom)} caractères")

Vous devriez obtenir des résultats de ce type :

>>> prenoms_commencant_par('X', prenoms) Xavier qui comporte 6 caractères Xavier qui comporte 6 caractères ...
>>> prenoms_finissant_par('X', prenoms) >>>prenoms_finissant_par('x', prenoms) Alix qui comporte 4 caractères Max qui comporte 3 caractères Felix qui comporte 5 caractères Félix qui comporte 5 caractères ...

Réaliser ENSUITE une à une et progressivement les autres fonctions demandées :

  • nombre()
  • nombre_prenom()
  • nombre_prenoms_commencant_par()
  • nombre_prenoms_finissant_par()
  • nombre_prenoms_longs()
  • nombre_prenoms_courts()
  • prenoms_de_x_caracteres()
  • longueur_maximale()
  • prenoms_les_plus_longs()

✎ 18 (mini-projet-envoi par ENT)° Créer maintenant une seconde version du projet. On ne veut pas que les fonctions affichant une liste de réponses renvoient plutôt un tableau contenant les réponses. Le plus simple est de travailler en utilisant des déclarations par compréhension. Si vous ne voyez pas, pensez à me demander un peu d'aide.

Conclusion en classe entière

Vous venez de voir plusieurs fonctions dont le coût est lnéaire et donc qui sont classés dans la catégorie FACILE.>/

En réalité, la classification FACILE / DIFFICILE est par la notion de "temps raisonnable". Oui, mais ce temps réel d'exécution est lié à l'ordinateur CLASSIQUE sur lequel il tourne.

Les coûts sont différents lorsqu'on utilise des algorithmes destinés à un ordinateur QUANTIQUE.

PAGE du CEA

Site Tout est Quantique

5 - FAQ

On peut avoir de plus gros fichiers de prénoms ?

Sans problème avec l'OpenData : Les prénoms en France de 1900 à 2019

Le fichier est au format CSV que vous avez peut-être découvert en seconde et qui sera étudié en 1er de toutes manières.

Dans les prochaines activités Algorithmique, nous verrons comment créer un algorithme plus performant qu'un algorithme linéaire mais nécessitant des données triées.

Si vous avez fait l'activité OpenData, vous savez ce que nous allons faire maintenant : des calculs et de l'évaluation non pas d'un tableau de valeurs mais d'un tableau d'enregistrements !

Activité publiée le 13 03 2020
Dernière modification : 08 03 2022
Auteur : ows. h.