• Aucun résultat trouvé

6.3 Problèmes sur les chaînes de caractères

6.3.1 Réductions

Le principe de réduction d’une structure de données, comme une séquence, consiste à synthétiser une information «plus simple» en parcourant les éléments contenus dans la structure.

Pour les chaînes de caractères, les problèmes de réduction consistent donc à produire une information «simple» - le plus fréquemment de type bool ou int - synthétisée à partir du parcours (complet ou non) des éléments de la chaîne.

Les fonctions de réduction possèdent une signature de la forme : — str -> int : réduction d’une chaîne vers le type entier — str -> bool : réduction d’une chaîne vers le type booléen et plus généralement :

— str * ... -> T : réduction d’une chaîne vers le type T (avec éventuellement des para- mètres supplémentaires).

Pour réaliser une réduction de chaîne, on dispose principalement de deux approches complémen- taires :

— la réduction par itération des éléments de la chaîne avecfor, ou — la réduction par parcours des indices de la chaîne.

6.3.1.1 Réduction par itération Comme les intervalles et tous les autres types de sé- quences, on peut parcourir les caractères d’une chaîne par itération avec la boucle for. La syntaxe pour les chaînes se déduit du principe plus général sur les séquences décrit précé- demment :

for <var> in <chaîne>: <corps>

Avec le principe d’interprétation correspondant :

— le<corps> de la boucle est une suite d’instructions qui est exécutée une fois pour chaque caractère de la<chaîne>, selon leur ordre d’indice.

— dans le<corps>, la variable <var> est liée au caractère courant : premier caractère (indice 0) au premier tour, deuxième caractère (indice 1) au deuxième tour . . . jusqu’au dernier tour de boucle avec le dernier caractère de la séquence (indice longueur − 1).

— la variable<var> n’est plus disponible après le dernier tour de boucle.

6.3.1.1.1 Exemple 1 : longueur d’une chaîne de caractères Comme premier problème de réduction, calculons une information fondamentale sur les chaînes (et des séquences en général) : leur longueur.

Définition : la longueur d’une chaîne est le nombre de caractères qui la composent.

Par exemple, la chaîne'Ceci est une chaîne' possède 19 caractères indicés de 0 à 18. Cette chaîne est donc de longueur 19 soit le nombre de caractères ou encore «l’indice du dernier caractère plus un».

La spécification de la fonction longueur est la suivante : def longueur(s):

"""str -> int

retourne la longueur de la chaîne s."""

>>> longueur('Ceci est une chaîne')

19

>>> longueur('vingt-quatre')

12

Cas particulier : la chaîne vide est de longueur 0 >>> longueur('')

0

Autre cas particulier : un caractère est une chaîne de longueur 1. >>> longueur('a')

1

La fonctionlongueur peut être définie de la façon suivante : def longueur(s):

"""str -> int

retourne la longueur de la chaîne s.""" # l : int

l = 0 # comptage de la longueur initialement à zéro # c : str (caractère courant)

for c in s:

l = l + 1 # longueur incrémentée pour chaque caractère

return l

# jeu de tests

assert longueur('Ceci est une chaîne') == 19 assert longueur('vingt-quatre') == 12

assert longueur('') == 0 assert longueur('a') == 1

La réduction longueur de chaîne est tellement primordiale qu’elle est en fait prédéfinie en Python. Il s’agit de la fonctionlen qui est utilisable sur n’importe quelle séquence.

>>> len('Ceci est une chaîne')

19

>>> len('vingt-quatre')

>>> len('')

0

>>> len('a')

1

Remarque : en pratique, on utilisera toujours la fonction len prédéfinie qui est beaucoup plus efficace quelongueur, la vocation de cette dernière étant essentiellement pédagogique. En effet,longueur parcourt la chaîne en entier alors que len ne fait aucun calcul puisque Python maintient systématiquement la longueur de la liste.

6.3.1.1.2 Exemple 2 : nombre d’occurrences d’un caractère On a déjà vu qu’un même caractère peut apparaître à plusieurs indices d’une même chaîne.

Par exemple : >>> # ch : str ... ch = 'les revenantes' >>> ch[1] 'e' >>> ch[5] 'e' >>> ch[7] 'e' >>> ch[12] 'e'

On dit qu’il y quatre occurrences du caractère'e' dans la chaîne ci-dessus : une occurrence à l’indice 1, une autre à l’indice 5, une troisième à l’indice 7 et une dernière à l’indice 12. Un problème typique concernant les chaînes consiste à définir une fonction occurrences qui compte le nombre d’occurrences d’un caractère donné dans une chaîne. Il s’agit d’une réduction vers le typeint.

Par exemple :

>>> occurrences('e', 'les revenantes')

4

>>> occurrences('t', 'les revenantes')

>>> occurrences('e', 'la disparition')

0

La fonctionoccurrences peut être définie de la façon suivante : def occurrences(c, s):

"""str * str -> int Hypothèse : len(c) == 1

retourne le nombre d'occurrences du caractère c dans la chaîne s""" # nb : int

nb = 0 # nombre d'occurrences du caractère # d : str (caractère courant) for d in s: if d == c: nb = nb + 1 return nb # jeu de tests

assert occurrences('e', 'les revenantes') == 4 assert occurrences('t', 'les revenantes') == 1 assert occurrences('e', 'la disparition') == 0 assert occurrences('z', '') == 0

6.3.1.1.3 Exemple 3 : présence d’un caractère Un sous-problème très classique du comptage d’occurrences est le test de la présence d’un caractère dans une chaîne. Il s’agit d’une réduction vers le typebool.

On peut déduire une solution simple en considérant la propriété suivante :

Un caractère est présent dans une chaîne si son nombre d’occurrence est strictement positif.

def presence(c, s):

"""str * str -> bool Hypothèse : len(c) == 1

retourne True si le caractère c est présent dans la chaîne s, ou False sinon"""

return occurrences(c, s) > 0

# Jeu de tests

assert presence('e', 'les revenantes') == True assert presence('e', 'la disparition') == False

Cette solution fonctionne mais ne peut satisfaire l’informaticien toujours féru d’efficacité. Considérons en effet l’exemple ci-dessous :

>>> presence('a', 'abcdefghijklmnopqrstuvwxyz') True

La réponse est bien sûrTrue mais pour l’obtenir, la fonction occurrences (appelée par presence) a parcouru l’ensemble de la chaîne'abcdefghijklmnopqrstuvwxyz' pour compter les occur- rences de'a', soit 26 comparaisons.

On aimerait ici une solution «directe» au problème de présence qui s’arrête dès que le caractère cherché est rencontré. Dans le pire des cas, si le caractère recherché n’est pas présent, alors on effectuera autant de comparaisons que dans la version qui compte les occurrences, mais dans les autres cas on peut gagner de précieuses nanosecondes de temps de calcul !

Pour arrêter le test de présence dès que l’on a trouvé notre caractère, nous allons effectuer une sortie anticipée de la fonction avec return.

def presence(c, s):

"""str * str -> bool Hypothèse : len(c) == 1

Retourne True si le caractère c est présent dans la chaîne s, ou False sinon"""

# d : str (caractère courant)

for d in s: if d == c:

return True # c'est gagné, on sort de la fonction

# (et donc de la boucle).

return False # si on a parcouru toute la chaîne s,

# on sait que c n'est pas présent. # jeu de tests

assert presence('e', 'les revenantes') == True assert presence('e', 'la disparition') == False assert presence ('z', '') == False

Effectuons une simulation pour presence('e', 'les revenantes'), donc pour c='e' et s='les revenantes' afin de constater le gain obtenu :

Tour de boucle variabled

entrée -

1er l

2e e

Exercice : comparer la simulation précédente avec celle de occurrences('e', 'les revenantes')

6.3.1.2 Réduction par parcours des indices Dans certains cas, le parcours des chaînes par itération sur les caractères qui la composent ne permet pas de résoudre simplement le problème posé. Dans ces cas-là, on peut utiliser un parcours basé sur les indices des caractères dans les chaînes.

Le problème sans doute le plus simple dans cette catégorie est une variante du test de présence. On souhaite définir une fonction recherche qui recherche un caractère dans une chaîne, et retourne l’indice de la première occurrence de ce caractère s’il est présent. Si le caractère est absent, alors la fonction retourne la valeurNone.

La spécification est la suivante : def recherche(c, s):

"""str * str -> int + NoneType Hypothèse: len(c) == 1

retourne l'indice de la première occurrence du caractère c dans s, ou None si le caractère est absent."""

Par exemple :

>>> recherche('e', 'les revenantes')

1

>>> recherche('n', 'les revenantes')

8

>>> recherche('e', 'la disparition')

Dans ce dernier cas, Python ne produit aucun affichage. C’est le signe que la valeurNone (unique valeur de typeNoneType) a été retournée. Vérifions ce fait :

>>> type(recherche('e', 'la disparition')) NoneType

Cette caractéristique particulière fait de la fonctionrecherche un fonction partielle. Prenons un peu de temps pour préciser ce concept important.

Dans le cas précis de la fonctionrecherche :

— on retourne un résultat de typeint si dans recherche(c, s) la chaîne s possède au moins une occurrence du caractèrec,

— on ne retourne pas de résultat - donc on retourneNone - si le caractère c n’est pas présent danss.

La signature correspondante est : str * str -> int + NoneType que l’on peut interpréter par :

Une fonction partielle qui prend deux chaînes de caractères en paramètres, et retourne soit un résultat entier soit pas de résultat.

Dans le cas général, une fonction partielle retournant un typeT possède dans sa signature le type de retourT + NoneType. Nous verrons d’autres exemples de fonctions partielles dans ce cours et les suivants.

La définition proposée pourrecherche est la suivante : def recherche(c, s):

"""str * str -> int + NoneType Hypothèse: len(c) == 1

retourne l'indice de la première occurrence du caractère c dans s, ou None si le caractère est absent."""

# i : int

i = 0 # indice courant, en commençant par l'indice du premier caractère

while i < len(s): # on "regarde" les indices de 0 (premier caractère)

# à len(s) - 1 (dernier caractère)

if s[i] == c:

return i # si c est présent, on retourne directement

# l'indice courant

else:

i = i + 1 # sinon on passe à l'indice suivant

return None # si on a parcouru tous les indices,

# alors le caractère n'est pas présent, # donc on retourne "pas de résultat". # Jeu de tests

assert recherche('e', 'les revenantes') == 1 assert recherche('n', 'les revenantes') == 8 assert recherche('e', 'la disparition') == None

Puisque l’on connaît maintenant les itérations sur les intervalles, il est possible de proposer une définition plus concise de la fonctionrecherche en itérant sur l’intervalle range(0, len(s)). Cet intervalle contient tous les entiers allant de0 (inclus) à len(s) (exclus), ce qui correspond à tous les indices des caractères de la chaînes.

On peut donc proposer une définition plus concise de la fonction recherche : def recherche(c, s):

"""str * str -> int + NoneType Hypothèse: len(c) == 1

retourne l'indice de la première occurrence du caractère c dans s, ou None si le caractère est absent."""

# i : int

for i in range(0,len(s)): if s[i] == c:

return i return None

# Jeu de tests

assert recherche('e', 'les revenantes') == 1 assert recherche('n', 'les revenantes') == 8 assert recherche('e', 'la disparition') == None