P5.1 La boite à outils

  • Le tryptique CAC

  • La complexité

  • Construction de listes par ajout successifs

  • TP moyenne glissée

Premiers algorithmes

Utiliser un compteur, un accumulateur, ou un curseur

Dans de nombreux cas, on doit :

  • compter le nombre d'occurence d'une condition, dans ce cas, on utilisera un compteur
  • sommer (ou faire un produit) dans une boucle, dans ce cas, on utilisera un accumulateur
  • mémoriser la position d'un élément particulier, dans ce cas, on utilisera un curseur

Un compteur en action

Les compteur sont des variables qu'on incrémente quand on trouve un éléments correspondant à un critère. Parfois il n'y a pas vraiment de critère, on compte simplement tout les éléments ou toutes les occurrences, mais la notion de comptage est fréquente en algorithmique. Nous avons déjà vu des utilisations de compteurs.

Multiples de 3

Ecrire une fonction qui prend en argument une liste de nombres et renvoi le nombre de multiples de 3 dans cette liste

Exercice de code : Compte les multiples de 3
Ecrire la fonction compterMult3(lst). Une liste lst sera générée pour tester votre code.

L'algorithme en pseudo-code

Le pseudo-code est indépendant du langage, c'est une description des opérations a faire pour accomplir une tâche. Il est important de bien distinguer algorithme et code. Lorsque vous écrivez un pseudo-code vous vous libérez des questions de syntaxe pour vous concentrer uniquement sur l'algorithme.

pseudo code de l'algorithme de recherche des multiples de 3 dans une liste :

fonction compterMult3(lst) :
     n ← 0
     pour element dans lst :
          si element%3 =  0 :
                 n ← n+1
          fin de si
     fin de pour
     renvoyer n

Notion de complexité

L'algorithme contient une boucle sur les éléments de la liste lst.

  • Avant la boucle on a 1 affection
  • La boucle est exécutée N fois (N=longueur de la liste)
  • Dans la boucle il y a 1 opération (%), un test, une addition et une affectation. Au total 4 opération élémentaires
  • Après la boucle il n'y a rien

On a donc au total, 4N + 1 opérations élémentaires dans ce code. 4N+1 est une fonction linéaire, on dit qu'on a une complexité linéaire et on note C=O(N)

Exemple
Vérifions la complexité de compterMult3()

Vous observez que lorsque le nombre N d'éléments de la liste est multiplié par 10, le temps de calcul est multiplié aussi par un facteur du même ordre de grandeur. Nous observons bien ici une complexité linéaire.

Un accumulateur en action

Pour sommer ou multiplier (ou soustraire ou diviser) dans une boucle, on utilise une variable accumulateur. Nous avons déjà vu des utilisations d'accumulateurs.

Calcul de la moyenne
Exercice de code : Calcul de moyenne
Ecrire la fonction somme(lst). Une liste lst sera générée pour exécuter votre code.

Ici il n'est pas question de compter, mais de faire la somme. Nous avons déjà vu cet algorithme plusieurs fois.

pseudo code de l'algorithme de la moyenne :

fonction moyenne(lst) :
     somme ← 0
     pour element dans lst :
          somme ← somme + element
     fin de pour
     renvoyer somme/len(lst)

Notion de complexité

L'algorithme contient une boucle sur les éléments de la liste lst.

  • Avant la boucle on a 1 affection
  • La boucle est exécutée N fois (N=longueur de la liste)
  • Dans la boucle il y a une addition et une affectation. Au total 2 opération élémentaires
  • Après la boucle il n'y a rien

On a donc au total, 2N + 1 opérations élémentaires dans ce code. 2N+1 est une fonction linéaire, on dit ici aussi qu'on a une complexité linéaire et on note C=O(N)

Exemple
Vérifions la complexité de moyenne()

Vous observez là encore que lorsque le nombre N d'éléments de la liste est multiplié 10, le temps de calcul est multiplié aussi par un facteur du même ordre de grandeur. Nous observons donc encore bien ici une complexité linéaire.

Un curseur en action

Les curseurs sont des variables qui retiennent la position (l'index) d'un élément présentant une caractéristique particulière dans une liste. Un bon exemple est la recherche de la position du max des éléments d'une liste.

Indice du maximum
Exercice de code : Indice du maximum
Ecrire la fonction idxMax(lst). Une liste lst sera générée pour exécuter votre code.

Ici il n'est pas question de compter ou de sommer, mais de mémoriser la dernière position du max parmi les éléments déjà parcourus.

pseudo code de l'algorithme de recherche de l'indice du max des éléments d'une liste :

fonction idxMax(lst) :
     idxMax ← 0
     pour i allant de 1 à len(lst)-1 :
         si lst[i] > lst[idxMax] :
               idxMax ← i
         fin de si
     fin de pour
     renvoyer n
La boucle va de 1 à len(lst) -1 si les indices vont de 0 à len(lst)-1 comme en python. on écrit i in range( 1,len(lst) )

remarque : Dans d'autres langages les indices vont de 1 à len(lst), dans ce cas la boucle ira de 2 à len(lst).

Notion de complexité

L'algorithme contient une boucle sur les éléments de la liste lst (en partant du 2ème)

  • Avant la boucle on a 1 affection
  • La boucle est exécutée N-1 fois (N=longueur de la liste)
  • Dans la boucle il y a une comparaison et une affectation. Au total 2 opération élémentaires
  • Après la boucle il n'y a rien

On a donc au total, 2N + 1 opérations élémentaires dans ce code. On a de nouveau une complexité linéaire et on note C=O(N)

Exemple
Vérifions la complexité de idxMax()

Vous observez là encore que lorsque le nombre N d'éléments de la liste est multiplié 10, le temps de calcul est multiplié aussi par un facteur du même ordre de grandeur. Nous observons donc encore bien ici une complexité linéaire.

Variante

Il existe une variante (qu'on retrouve généralement dans tous les algorithme utilisant un curseur) à l'algorithme que nous venons de voir.

Si, au lieu de l'indice de la valeur maxi, on cherche la valeur elle même, l'algorithme est légèrement différent.
Ecrivez le pseudo-code modifié pour satisfaire cette recherche, puis codez le ici :

Exercice de code
Ecrire la fonction valMax(lst) qui renvoie la valeur max d'une liste
ATTENTION : vous ne pouvez pas utiliser range dans cet exercice !
pseudo code de l'algorithme de recherche du max des éléments d'une liste :

fonction valMax(lst) :
     valMax ← lst[0]
     pour i dans lst[1:] :
         si i > valMax :
               valMax ← i
         fin de si
     fin de pour
     renvoyer n

Exercices

sommes et moyenne des valeurs paires

Ecrire l'algorithme d'une fonction sommePaires(lst) qui renvoie la somme des valeurs paires d'une liste.

Exercice à choix multiple
Dans sommesPaires vous aurez besoin d'un compteur ?
Correct !
Exercice à choix multiple
Dans sommesPaires vous aurez besoin d'un curseur?
Correct !
Exercice à choix multiple
Dans sommesPaires vous aurez besoin d'un accumulateur ?
Correct !

Ecrire un second algorithme pour moyPaires qui renvoie la moyenne des valeurs paires d'une liste.

Exercice à choix multiple
Dans moyPaires vous aurez besoin d'un compteur ?
Correct !
Exercice à choix multiple
Dans moyPaires vous aurez besoin d'un curseur?
Correct !
Exercice à choix multiple
Dans moyPaires vous aurez besoin d'un accumulateur ?
Correct !

moyenne pondérée

N'oubliez pas que vous pouvez visualiser l'exécution de votre code pour vous aider à le mettre au point.

Exercice de code : moyenne coefficientée
Ecrire une fonction moyPondee(notes , coefs) qui renvoie la moyenne coefficientée.
Exercice à réponse courte
Combien y'a-t-il d'opération élémentaires avant la boucle ?
Correct !
Exercice à réponse courte
Combien y'a-t-il d'opération élémentaires dans la boucle (en fonction du nombre N de notes) ?
Correct !
Exercice à réponse courte
Combien y'a-t-il d'opération élémentaires après la boucle (seulement dans la fonction) ?
Correct !
Exercice à réponse courte
Combien y'a-t-il d'opération élémentaires en tout (en fonction du nombre N de notes) ?
Correct !
Exercice à réponse courte
La complexité est elle linéaire ?
Correct !

un curseur délicat

Nous avons déjà traité la question de la recherche d'un caractère dans un texte. Nous allons ici traiter un problème assez similaire mais un peu plus délicat : chercher une chaine dans une autre. Pour plus de clarté, nous appelerons mot la chaine recherchée, et texte la chaine dans laquelle on effectue la recherche.

Commencez par regarder la vidéo ci-dessous pour comprendre le problème.

Ecrivons le pseudo code :

fonction cherchePattern(mot,texte) :
     pos -> -1
     idx -> 0
     tant que idx < len(texte)-len(mot) and pos = -1
          if texte[idx:idx+len(mot)] == mot : pos = idx
          idx = idx + 1
     fin de tant que
     renvoyer pos

Exercice de code : reverse avec pop

Construction d'une liste par ajout successifs

retourner une liste par ajout successifs

On veut retourner une liste (c'est à dire ranger ses éléments dans l'ordre inverse) et on va procéder par ajouter successifs (il existe d'autres façons de le faire).
L'algorithme est assez simple :

fonction retourner(lst) :
    lstReverse = [ ]
    tant que lst non vide :
        retirer le dernier elem de lst et l'ajouter à la fin de  lstReverse
    fin de pour
    renvoyer lstReverse
Coder ceci en python ne pose pas de grosse difficulté, mais il faut se souvenir de la méthode pop et de son usage....
Voici un code qui va te servir d'exemple pour l'exercice.

Rappel :
lst = [1,2,3]
a = lst.pop()
-> a=3 et maintenant lst=[1,2]
lst.pop() retire le dernier élément de la liste lst et renvoie sa valeur.
Exercice de code : reverse avec pop

Au lieu de append, on pourrait aussi utiliser une concaténation de liste : lstR = lstR + [val] donnera le même résultat, essaye les deux façons.
(attention : en réalité ces deux façon de procéder ne sont pas équivalentes. Elle donnent le même résultat et pour cette année nous n'entreront pas dans les détails sr la différence de traitement entre les deux)

On pourrait même ne rien ajouter et ne rien concaténer :

def reversePOP(lst: list ) -> list :
    lstR=[0] * len(lst) # initialise une liste avec len(lst) 0
    idx=0
    while len(lst) > 0:
        e=lst.pop()
        lstR[idx]=e
        idx+=1
    return lstR
Et là encore, le résultat est le même mais les opérations réalisées ne sont pas les même en réalités. Et ici aussi nous n'entreront pas cette année dans les détails.

Toutefois nous aurions put écrire aussi bien cet algorithme :

fonction retourner(lst) :
    lstReverse = [ ]
    tant que lst non vide :
        retirer le premier elem de lst et l'ajouter au début de  lstReverse
    fin de pour
    renvoyer lstReverse
Tu pourra coder ce second algorithme en utilisant lst.pop(0) qui retire l'élément d'indice 0 et lstR.insert(0,val) qui insère val en tête de liste.

On pourrait aussi utiliser une concaténation de liste : lstR = [val] + lstR donnera le même résultat, essaye les deux façons, et surtout remarque bien :

ajouter en tête : lst.insert(0,val) ou lst=[val] + lst
ajouter en queue : lst.append(val) ou lst=lst+[val]
retirer en tête : val = lst.pop(0)
retirer en queue :val = lst.pop()

Rappel :
lst = [1,2,3]
lst.insert(0,7)
-> Maintenant lst vaut [7,1,2,3]
Exercice de code : reverse avec insert

Ou avec l'autre façon, sans rien changer à l'algorithme, mais en remplaçant insert par une concaténation de liste :

Rappel :
lst = [1,2,3]
lst [7]+lst
-> Maintenant lst vaut [7,1,2,3]
Exercice de code : reverse sans pop et sans insert

Complexité

Peut tu estimer la complexité des codes de retournement ? Sont ils tous équivalent ?

Trier les éléments

Vous aller programmer ici une fonction tri(lst) qui va trier une liste. L'algorithme utilisé, que nous reverrons plus tard dans une autre forme, est le suivant :

fonction triInsertion(lst) :
    listTriee=[]
    Tant que longueur(lst) > 0 :
        rechercher l'indice du min dans lst
        retirer cet élément (avec pop) et l'ajouter (en queue) dans listeTrie
    fin de tant que
    renvoyer listeTriee
(la fonction idxMin(list) est disponible dans cet exercice)
Exercice de code

Dans ce code, que vous venez de faire, on a pas vraiment de curseurs (il y en a cependant un, caché dans la fnction idxMin que vous avez utilisé), d'accumulateur, ou de compteur. En effet ici on ne compte rien, on ne somme rien, et on ne localise rien.

L'ajout successif d'élément dans une liste est un autre outil indispensable que vous serez souvent amenés à utiliser.

Estimons la complexité de cet algorithme expérimentalement avec time().

Exemple

En raison de limitations sur ce site, il n'est pas pssible de travailler sur des listes très grandes. Néanmoins, ici, on a pris une séries de valeurs de N allant de 10 à 2560. A chaque fois on multiplie N par 2. Le temps de calcul évolue d'abord linéairement pour les petites valeurs de N mais rapidement on voit que quand N est multiplié par 2, le temps est multiplié par 4 (2² !)

La complexité semble donc plutot propotionelle à N² dans cet exemple.

Combien de fois est exécutée la boucle while ? Combien d'opération dans cette boucle while ?

La réponse est que la boucle while est exécutée N fois, mais dans la boucle, on appelle idxMin. idxMin a une complexité en O(N), mais comme la liste lst est raccourcie au fur et a mesure, c'est un peu compliqué....

Au premier tour, idxMin cherche dans une liste de longueur N

A second tour dans une liste de longueur N-1

Au troisième tour, dans une liste de longueur N-1

etc... jusqu'au dernier tour, ou il ne reste qu'un élément.

si le temps de idxMin est N, alors le temps de tout les appels est :

1 + 2 + ... + N = N(N+1)/2 = N² /2 + N/2

pour de grandes valeurs de N, N² >> N, la complexité sera proportionnelle à N². On parle ici de complexité quadratique.