30 janvier 2022
Cette collection de notes est mise à disposition selon les termes de la Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 2.0 France.
Se trouve sur la page https://eduscol.education.fr/2661/banque-des-epreuves-pratiques-de-specialite-nsi, dans https://eduscol.education.fr/document/33178/download
Question : Écrire une fonction
recherche
qui prend en paramètrescaractere
, un caractère, etmot
, une chaîne de caractères, et qui renvoie le nombre d’occurrences decaractere
dansmot
, c’est-à-dire le nombre de fois oùcaractere
apparaît dansmot
.
Clairement, le souhait est de normaliser le choix des noms - ça facilite les corrections -, on ne laisse pas d’autre possibilité que de commencer par
def recherche(caractere, mot):
...
Critique : le nom imposé recherche
est assez malheureux. L’usage est que le nom d’une fonction décrive le résultat qu’elle calcule. “Recherche”, ça ne dit pas ce qui est recherché. La première position du caractère dans le mot ? juste un booléen qui dit si on l’a trouvé ? …
Bref, un nom comme nombre_occurrences
ou nb_occurrences
aurait été plus approprié.
Une solution simple, dans le style impératif, est de
for c in mot: ...
)if c == caractere:
def recherche(caractere, mot):
= 0 # nombre d'occurrences, 0 au départ
nb for c in mot: # pour chaque caractère c du mot
if c == caractere: # - est-ce le bon ?
+= 1 # - oui ! un de plus !
nb return nb # et voilà le total.
Je suppose que c’est la solution à laquelle les correcteurs s’attendent le plus. Mais on peut faire autrement.
Une chaîne de caractères est aussi une liste de caractères. À ce titre,
[ c in mot if c == caractere ]
len
) de cette liste.def recherche(caractere, mot):
return len([c for c in mot if c == caractere])
count
des chaînesQuand on connaît le langage Python, on sait que les chaînes sont des séquences qui ont une méthode count
qui retourne le nombre d’occurrences d’une sous-séquence.
def recherche3(caractere, mot):
return mot.count(caractere)
bref, c’est tout fait.
Sachant que
on peut s’orienter vers une solution récursive
def recherche(caractere, mot):
if len(mot) == 0:
return 0
else:
= mot[0]
premier = mot[1:]
suite if premier == caractere:
return 1 + recherche(caractere, suite)
else:
return recherche(caractere, suite)
qu’on peut présenter de diverses façons équivalentes, plus ou moins détaillées, par exemple en utilisant seulement une variable pour le nombre d’occurrences qui figurent dans la suite :
def recherche(caractere, mot):
if len(mot) == 0:
return 0
else:
= recherche(mot[1:])
nb_occ_suite if mot[0] == caractere:
return 1 + nb_occ_suite
else:
return nb_occ_suite
ou encore, en jonglant avec les expressions conditionnelles de Python
def recherche (caractere, mot):
return 0 if len(mot) == 0 else (
1:])
recherche(caractere, mot[+ (1 if caractere == mot[0] else 0))
qui se lit :
Cette solution fournit un résultat correcte, mais elle a un défaut, sa complexité temporelle, liée au fait que la construction de mot[1:]
pour un mot de taille implique une copie des éléments concernés dans une nouvelle liste.
En gros, si on part d’un mot de 10 lettres, le passage au sous-mot de 9 lettres nécessitera de copier 9 éléments. Mais pour traiter ce mot de 9 lettres, il faudra aussi faire une copie du sous-mot de 8 lettres, et le traiter.
Bref, tout ça va nous coûter 9+8+….+1 étapes. Pour un mot de taille , ça nous fera étapes, une quantité qui grandit comme le carré de . On parle de complexité temporelle quadratique.
Comme nous disposons d’une autre solution (itérative) qui n’est pas compliquée et qui fait le calcul en un temps qui est simplement proportionnel à (complexité linéaire), en pratique, on n’utilisera évidemment pas la solution récursive présentée ci-dessus.
Ceci ne veut pas dire du tout qu’une approche récursive soit intrinsèquement inefficace.
Ce qui coûte ici, c’est de faire la copie de la fin du mot, pour la passer en paramètre.
À la place, on peut passer le mot inchangé, et un paramètre qui indique l’indice du premier caractère restant à traiter.
Ce qu’on peut présenter sous cette forme
def recherche(caractere, mot, debut = 0):
if debut == len(mot):
return 0
else:
= recherche(caractere, mot, debut + 1)
r if caractere == mot[debut]:
return r + 1
else:
return r
Ce coup-ci, le temps de calcul est linéaire.
On retrouve cette manière de faire une récursion sur une sous-liste dans l’exercice précédent.
Note : les expressions conditionnelles de Python permettent de réduire les 4 dernières lignes à
return r + 1 if caractere == mot[debut] else r
qui se lit naturellement “retourne r+1 si ceci-cela, et 0 sinon”.
Profitons de l’occasion pour utiliser la technique de la “variable tampon”, qui est également présente dans l’exercice suivant.
En gros, on se sert d’un paramètre supplémentaire qui sert à construire le résultat final. Ici, total
indiquera le nombre d’occurrences trouvé jusque là.
Quand la récursion est terminée, la fonction retourne le contenu du tampon
def recherche(caractere, mot, debut = 0, total = 0):
if debut == len(mot):
return total
= 1 if caractere == mot[debut] else 0
t return recherche(caractere, mot, debut + 1, total + t)
La technique du paramètre tampon est souvent utilisée pour obtenir un algorithme récursif où la récursion est terminale (c’est-à-dire que l’appel récursif est la dernière chose qu’on fait dans la fonction).
L’intérêt de la récursion terminale, c’est que l’interprète n’a rien à mémoriser dans la pile lors de l’appel, et peut même transformer l’appel récursif en boucle. Comme on n’empile rien, la taille de la pile utilisée reste constante (si l’interprète est bien fait), au lieu de croître linéairement avec les versions récursives précédentes.
Ce qui nous ramène au premier algorithme, avec la correspondance entre debut
et i
pour les indices, et total
avec nb
pour les compteurs.
Un bout de programme Python à trous, à compléter
= [100,50,20,10,5,2,1]
Pieces
def rendu_glouton(arendre, solution=[], i=0):
if arendre == 0:
return ... # 1
= pieces[i] # BUG
p if p <= ... : # 2
# 3
solution.append(...) return rendu_glouton(arendre - p, solution, i)
else :
return rendu_glouton(arendre, solution, ...) # 4
comportement attendu
>>>rendu_glouton_r(68,[],0)
[50, 10, 5, 2, 1]
>>>rendu_glouton_r(291,[],0)
[100, 100, 50, 20, 20, 1]
Critiques :
Pieces
commence par une majuscule, il est utilisé sans.a_rendre
, et pas arendre
.PIECES
).À ces considérations syntaxiques, dont les rédacteurs du sujet semblent ignorer autant l’existence https://www.python.org/dev/peps/pep-0008 que l’importance (y compris pédagogique), s’ajoute
>>>rendu_glouton_r(68)
[50, 10, 5, 2, 1]
>>>rendu_glouton_r(291)
[100, 100, 50, 20, 20, 1]
Il n’y a jamais que 4 trous à compléter.
Quelques observations
i
est un indice dans le tableau Pieces
(ligne du bug, initialement la première. Il faudra bien le faire avancer un jour (trou 4).Il parait donc raisonnable
arendre == 0
), on retourne la solution qu’on a construite ; return solution # 1
;if p <= arendre: #2
pour savoir si il faut rendre cette pièce ou pas ;solution.append(p) # 3
.return rendu_glouton(arendre, solution, i + 1) # 4
= [100, 50, 20, 10, 5, 2, 1]
PIECES
def rendu_glouton(a_rendre, solution = [], i = 0):
if a_rendre == 0:
return solution # 1
= PIECES[i]
p if p <= a_rendre: # 2
# 3
solution.append(p) return rendu_glouton(a_rendre - p, solution, i)
else :
return rendu_glouton(a_rendre, solution, i + 1) # 4
Dans le code ci-dessus les appels à rendu_glouton
sont terminaux (on ne fait rien après).
Ce qui veut dire qu’on peut convertir ce code sous forme itérative (avec des boucle).
Un appel terminal signifie : recommencer avec les valeurs indiquée en paramètres. En détail
rendu_glouton(a_rendre - p, solution, i)
: diminuer la somme à rendre, et recommencer avec la même piècerendu_glouton(a_rendre, solution, i + 1)
: recommencer avec la pièce suivanteD’où l’idée d’avoir deux boucles imbriquées
Le paramètre tampon solution
se transforme en variable locale de la fonction, retournée à la sortie de la fonction
Une solution simple :
= [100,50,20,10,5,2,1]
PIECES
def rendu_iteratif(a_rendre):
= []
solution for piece in PIECES:
while a_rendre >= piece:
solution.append(piece)= a_rendre - piece
a_rendre return solution
print ("Tests :")
assert rendu_iteratif(68) == [50, 10, 5, 2, 1]
assert rendu_iteratif(291) == [100, 100, 50, 20, 20, 1]
print ("Ok !")
Que l’on peut accélérer légèrement en retournant dès qu’une pièce a suffi pour épuiser la somme à rendre
def rendu_iteratif(a_rendre):
= []
solution for piece in PIECES:
while a_rendre >= piece:
solution.append(piece)= a_rendre - piece
a_rendre if a_rendre == 0:
return solution
# on ne passe jamais ici
Dans cette version, on ne sort jamais “par la fin” de la boucle for
. Le fait que
PIECES
,permet de conclure qu’il arrivera nécessairement que la condition arendre == 0
soit vraie, au plus tard en rencontrant la pièce 1. Et donc le return solution
sera forcément exécuté, qui fera sortir “sauvagement” de la boucle for
et de la fonction.