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.
Ecrire une fonction qui prend en argument une liste de nombres et renvoi le nombre de multiples de 3 dans cette liste
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)
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.
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)
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.
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)
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 :
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
Ecrire l'algorithme d'une fonction sommePaires(lst) qui renvoie la somme des valeurs paires d'une liste.
Ecrire un second algorithme pour moyPaires qui renvoie la moyenne des valeurs paires d'une liste.
N'oubliez pas que vous pouvez visualiser l'exécution de votre code pour vous aider à le mettre au point. |
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
Construction d'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 lstReverseCoder 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.
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 lstREt 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 lstReverseTu 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]
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]
Complexité
Peut tu estimer la complexité des codes de retournement ? Sont ils tous équivalent ?
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)
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().
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.