IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre Python et s'initier à la programmation

Partie 1 : Bases de la programmation


précédentsommaire

VI. Algorithme

Maintenant que l'on maitrise toutes les constructions et mécanismes de base d'un langage de programmation, on va pouvoir s'attaquer à ce qui nous motive à apprendre la programmation, à savoir la résolution de problèmes. Ce chapitre présente les notions de problème et d'algorithme et termine en détaillant quelques exemples de problèmes avec une solution les résolvant.

VI-A. Problème et algorithme

Lorsqu'on écrit un programme, c'est dans le but de résoudre un problème. Cette notion a déjà été présentée à la section 4.2, avec celle de décomposition en sous-problèmes. Définir un problème consiste à identifier deux éléments :

  • les données dont on dispose ;
  • le résultat à produire.

Voici, par exemple, la description d'un problème en langue naturelle :

« Étant donné un nombre naturel non nul, identifier le nombre de diviseurs stricts qu'il possède. »

On identifie immédiatement les données et le résultat. On reçoit un nombre naturel non nul (une donnée de type int) et on doit calculer le nombre de diviseurs stricts qu'il a (une donnée de type int).

Un algorithme est une description d'un ensemble d'opérations à effectuer, et de l'ordre dans lequel elles doivent l'être. Il s'agit de la description d'un processus qu'il est possible d'exécuter. Voici un algorithme, décrit en langue naturelle, qui résout le problème qui nous intéresse :

« Pour trouver le nombre de diviseurs stricts du nombre kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp, on va examiner tous les entiers compris entre kitxmlcodeinlinelatexdvp1finkitxmlcodeinlinelatexdvp (inclus) et kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp (exclu). Pour chacun de ces entiers, on vérifie s'il divise kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp ou non. En comptant le nombre d'entiers qui passent le test de la division, on obtient la solution au problème. »

Comme le résume la figure 1, un algorithme résout donc un problème. En pratique, on va vouloir résoudre une instance donnée d'un problème, c'est-à-dire que l'on connaitra les données dont on dispose. Dans notre exemple, une instance du problème est :

« Identifier le nombre de diviseurs stricts que possède le nombre naturel kitxmlcodeinlinelatexdvp42finkitxmlcodeinlinelatexdvp. »

Alors que l'algorithme n'est qu'une description, lorsqu'il s'agit de devoir l'exécuter sur une instance donnée d'un problème, il faut l'implémenter. L'implémentation d'un algorithme consiste à en écrire une version exécutable sur une machine à l'aide d'un langage de programmation, c'est-à-dire écrire un programme. Le listing suivant montre une implémentation possible de l'algorithme.

Image non disponible
Figure 1. Un algorithme permet de résoudre un problème et pour en résoudre une instance donnée, il faut implémenter l'algorithme à l'aide d'un programme.
Le fichier divisors.py contient une fonction qui compte le nombre de diviseurs stricts d'un nombre naturel non nul.
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
# Compte le nombre de diviseurs stricts d'un nombre naturel non nul
# Pre  : n est un entier strictement positif
# Post : la valeur renvoyée contient le nombre de diviseurs stricts
#        de n
def nbdivisors(n):
    result = 0
    for i in range(1, n):
        if n % i == 0:
            result += 1
    return result

print('Le nombre 42 possède', nbdivisors(42), 'diviseurs stricts.')

Notez que pour un problème donné, il peut exister plusieurs algorithmes différents. Il n'y a en effet pas toujours qu'une seule approche possible à un problème. De même, un algorithme donné peut être implémenté de plusieurs façons différentes. On aurait, par exemple, pu utiliser une boucle while au lieu d'une for dans l'exemple des diviseurs.

VI-B. Exemples

Cette section présente plusieurs problèmes résolus, pour lesquels on ne donne et discute que de l'implémentation en Python, sans passer par la description préalable d'un algorithme, en langue naturelle, par exemple. C'est l'occasion de mettre ensemble tout ce qui a été vu dans les cinq chapitres précédents en action, pour écrire des fonctions dignes de ce nom et qui résolvent des problèmes précis !

VI-B-1. Manipulation de nombres

Commençons avec quelques exemples simples qui manipulent des nombres à l'aide d'opérations arithmétiques et de fonctions du module math.

VI-B-1-a. Nombre de chiffres

Le premier problème consiste à déterminer le nombre de chiffres que possède un nombre naturel non nul. Pour cela, il suffit de procéder à des divisions entières successives par 10, jusqu'à atteindre kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp. En comptant le nombre de divisions que l'on a faites, on obtient le résultat escompté :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def nbdigits(n):
    result = 0
    while n != 0:
        n //= 10
        result += 1
    return result

Vous remarquerez qu'on initialise d'abord la variable qui contiendra le résultat recherché, qu'elle est mise à jour dans la boucle et enfin renvoyée en fin de fonction. Il existe également une solution plus immédiate qui se base sur les propriétés du logarithme en base 10. En effet, en se rappelant que kitxmlcodeinlinelatexdvp\log_{10} 10 = 1finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvp\log_{10} 100 = 2finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvp\log_{10} 1000 = 3finkitxmlcodeinlinelatexdvp… on arrive directement à la solution suivante :

 
Sélectionnez
1.
2.
3.
4.
from math import log10

def nbdigits(n):
    return int(log10(n)) + 1
VI-B-1-a-i. Inversion de nombre

Dans le deuxième problème, on reçoit un nombre naturel comme donnée et il faut produire comme résultat le nombre que l'on obtiendrait en le lisant à l'envers (de droite à gauche). Pour résoudre ce problème, on va se baser sur les deux propriétés suivantes, étant donné un nombre :

  • le reste de sa division entière par kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp donne son dernier chiffre ;
  • sa division entière par kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp l'ampute de son dernier chiffre.

Grâce à ces deux propriétés, on va pouvoir parcourir le nombre reçu en paramètre chiffre par chiffre, en partant du dernier. On aura parcouru tous les chiffres lorsqu'on arrivera à zéro, ce qui est la condition d'arrêt de la boucle while. L'implémentation de la fonction est assez directe :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
def reverse(n):
    result = 0
    while n != 0:
        result *= 10
        result += n % 10
        n //= 10
    return result

Comme on le constate, les chiffres extraits un à un doivent être combinés pour construire le résultat demandé. Pour cela, on initialise une variable result à zéro, et on la multiplie par kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp à chaque tour de boucle, avant de lui ajouter le chiffre extrait du nombre de départ. On obtient ainsi à la fin le nombre de départ dans le sens inverse.

VI-B-1-a-ii. Nombre de diviseurs stricts communs

Pour le troisième problème, on reçoit deux nombres naturels non nuls et il faut déterminer le nombre de diviseurs stricts qu'ils ont en commun. Pour rappel, un diviseur strict d'un nombre naturel kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est un naturel kitxmlcodeinlinelatexdvpdfinkitxmlcodeinlinelatexdvp compris entre kitxmlcodeinlinelatexdvp1finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp (exclu) tel que le reste de la division entière de kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp par kitxmlcodeinlinelatexdvpdfinkitxmlcodeinlinelatexdvp soit nul. Pour résoudre le problème, il suffit de parcourir tous les naturels de kitxmlcodeinlinelatexdvp1finkitxmlcodeinlinelatexdvp au plus petit des deux nombres reçus (exclu), et de compter ceux qui divisent les deux nombres :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def nbcommondivisors(a, b):
    result = 0
    for i in range(1, min(a, b)):
        if a % i == 0 and b % i == 0:
            result += 1
    return result

Vous pouvez constater l'utilisation de la fonction prédéfinie min qui renvoie le plus petit des deux nombres qu'elle reçoit en paramètres.

VI-B-1-a-iii. Liste des diviseurs

Dans ce dernier problème, il s'agit de calculer et renvoyer la liste de tous les diviseurs d'un nombre naturel donné. On a déjà vu précédemment comment faire pour retrouver les diviseurs d'un nombre, et il s'agira ici d'en plus les stocker dans une liste :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def divisors(n):
    result = []
    for i in range(1, n+1):
        if n % i == 0:
            result.append(i)
    return result

La variable result est initialisée à une liste vide et elle est complétée au fur et à mesure dans la boucle, par chaque diviseur, à l'aide de la fonction append appliquée sur la liste.

VI-B-1-b. Algorithme récursif

On va maintenant voir des exemples d'algorithmes récursifs, c'est-à-dire qu'ils seront implémentés par une fonction qui se rappelle elle-même. Rappelez-vous qu'il s'agit d'un choix d'implémentation, et que tous les problèmes ne se prêtent pas forcément à une implémentation récursive.

VI-B-1-b-i. Nombre de chiffres

Commençons avec une fonction récursive qui résout le problème du nombre de chiffres. On part des deux propriétés suivantes :

  • si le nombre est strictement inférieur à kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp, il n'a qu'un seul chiffre ;
  • sinon, il possède un chiffre de plus que le nombre de chiffres du résultat de sa division entière par kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp.

La première propriété correspond au cas de base qui permettra d'arrêter la récursion et la seconde propriété correspond au cas récursif. En exploitant ces deux propriétés, on obtient l'implémentation suivante :

 
Sélectionnez
1.
2.
3.
4.
def nbdigits(n):
    if n < 10:
        return 1
    return 1 + nbdigits(n // 10)

Notez que le choix du cas de base n'est pas unique. Pour cet exemple, on aurait pu se baser sur le fait qu'en faisant les divisions entières par kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp successives, on finirait par obtenir kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp. On peut utiliser cette valeur de n comme cas de base, en renvoyant la valeur kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp :

 
Sélectionnez
1.
2.
3.
4.
def nbdigits(n):
    if n == 0:
        return 0
    return 1 + nbdigits(n // 10)

Ce choix est néanmoins moins intuitif, car cela n'a que peu de sens que de considérer que le nombre de chiffres de kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp est nul. De plus, dans la définition du problème, il est clairement mentionné que n doit être un nombre naturel non nul.

VI-B-1-b-ii. Factorielle

La factorielle d'un nombre naturel non nul kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp, notée kitxmlcodeinlinelatexdvpn!finkitxmlcodeinlinelatexdvp, est le produit kitxmlcodeinlinelatexdvpn! = 1 \cdot 2 \cdot ... \cdot nfinkitxmlcodeinlinelatexdvp et avec kitxmlcodeinlinelatexdvp0! = 1finkitxmlcodeinlinelatexdvp, par convention. On peut la calculer en utilisant une simple boucle while ou for, ou alors de manière récursive en exploitant les deux propriétés suivantes :

kitxmlcodelatexdvp\left\{\begin{array}{ll} 0! = 1 \\ n! = n \cdot (n - 1)! & (\textrm{si } n > 0) \end{array}\right.finkitxmlcodelatexdvp

La première propriété correspond au cas de base et la seconde au cas récursif. On obtient dès lors directement l'implémentation suivante :

 
Sélectionnez
1.
2.
3.
4.
def fact(n):
    if n == 0:
        return 1
    return n * fact(n - 1)

On remarque que la structure est similaire à celle de l'exemple précédent. Le corps de la fonction commence avec une instruction if pour le cas de base puis vient le return du cas récursif, qui rappelle la fonction.

VI-B-1-b-iii. Plus grand commun diviseur

Le plus grand commun diviseur (PGCD) de deux naturels non nuls est le plus grand nombre entier qui divise à la fois les deux nombres. Pour le trouver, il suffit de tester tous les diviseurs possibles, compris entre le plus petit des deux nombres et kitxmlcodeinlinelatexdvp1finkitxmlcodeinlinelatexdvp, et de s'arrêter dès qu'on en trouve un qui divise les deux. On peut également se tourner vers une solution récursive en exploitant les propriétés suivantes :

kitxmlcodelatexdvp\left\{\begin{array}{ll} pgcd(a, 0) = a \\ pgcd(a, b) = pgcd(b, a) & (\textrm{si } a < b \textrm{ et } b \neq 0) \\ pgcd(a, b) = pgcd(b, a \,\%\, b) & (\textrm{si } a > b \textrm{ et } b \neq 0) \end{array}\right.finkitxmlcodelatexdvp

La première propriété est le cas de base et les deux autres sont des cas récursifs. On obtient directement l'implémentation suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def pgcd(a, b):
    if b == 0:
        return a
    if a > b and b != 0:
        return pgcd(b, a % b)
    return pgcd(b, a)

On a donc, cette fois-ci, deux cas récursifs. On commence toujours le code avec une instruction if pour le cas de base, puis vient le premier cas récursif, dans une instruction if également. On termine enfin le corps de la boucle avec le second cas récursif.

VI-B-1-b-iv. Nombre de Fibonacci

Dans la suite des nombres de Fibonacci, chaque nombre s'obtient comme la somme des deux termes précédents de la suite, sachant que les deux premiers sont fixés. Si on note par kitxmlcodeinlinelatexdvpF_nfinkitxmlcodeinlinelatexdvp, le kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvpe nombre de la suite, on peut définir la suite comme :

kitxmlcodelatexdvp\left\{\begin{array}{ll} F_1 = 1 \\ F_2 = 1 \\ F_n = F_{n - 1} + F_{n - 2} \end{array}\right.finkitxmlcodelatexdvp

On a cette fois-ci un exemple où l'on a deux cas de base (les deux premières propriétés) et un cas récursif (la troisième propriété) qui est une double récursion, c'est-à-dire qu'on se rappelle deux fois. L'implémentation d'une fonction permettant de calculer n'importe quel terme de la suite de Fibonacci est immédiat. On peut même la rendre plus courte en se rendant compte que le résultat à produire pour les deux cas de base est le même :

 
Sélectionnez
1.
2.
3.
4.
def fibo(n):
    if n == 1 or n == 2:
        return 1
    return fibo(n-1) + fibo(n-2)
VI-B-1-c. Interroger et manipuler des séquences

Pour terminer ce chapitre, voyons maintenant des algorithmes qui vont prendre comme données des séquences, pour les interroger ou pour effectuer des manipulations et transformations sur ces dernières.

VI-B-1-c-i. Somme des éléments

Le premier problème consiste simplement à faire la somme des éléments d'une liste ou d'un tuple de nombres. La solution est immédiate en ce sens qu'il suffit de parcourir tous les éléments de la séquence et d'en faire la somme à l'aide d'une variable :

 
Sélectionnez
1.
2.
3.
4.
5.
def sumall(data):
    result = 0
    for elem in data:
        result += elem
    return result

On commence par initialiser une variable result à zéro. On fait ensuite une boucle pour parcourir les éléments de la séquence. Comme on n'a pas besoin des indices, une boucle for est suffisante et plus lisible. On ajoute chacun des éléments parcourus à la variable result qu'on renvoie simplement à la fin.

VI-B-1-c-ii. Valeur minimale

Le deuxième problème consiste à trouver, dans une liste ou un tuple de nombres, la plus petite valeur qu'y s'y trouve. Pour ce faire, il suffit de parcourir tous les éléments de la séquence et de retenir dans une variable la plus petite valeur qu'on a déjà rencontrée.

Si la séquence reçue en paramètre est vide, le problème n'a pas de sens. On ne peut en effet pas trouver la valeur minimale d'une séquence qui ne contient aucun élément. On peut gérer ce cas particulier de deux manières différentes : soit on interdit tout simplement que le paramètre soit une séquence vide, soit on renvoie une valeur particulière dans le cas où la liste est vide, par exemple kitxmlcodeinlinelatexdvp+\inftyfinkitxmlcodeinlinelatexdvp. En faisant l'hypothèse que la séquence n'est pas vide, l'implémentation est assez directe :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def findmin(data):
    result = data[0]
    for elem in data:
        if elem < result:
            result = elem
    return result

On peut initialiser la variable result à la valeur de data[0] puisque la séquence n'est pas vide et contient donc au moins un élément. Ensuite, à chaque nouvel élément parcouru, on vérifie s'il est plus petit que celui stocké dans result et, si c'est le cas, on met à jour result avec cette valeur qui est donc la plus petite rencontrée jusqu'à présent.

Pour gérer le cas d'une séquence vide, il suffit simplement d'initialiser la variable result à la valeur math.inf qui représente l'infini positif, en ayant avant tout importé le module math, évidemment.

VI-B-1-c-iii. Recherche d'une sous-séquence

Le troisième problème consiste à vérifier si une séquence est une sous-séquence d'une autre. On a vu précédemment qu'on pouvait utiliser directement l'opérateur in pour les chaines de caractères, mais il n'est pas applicable aux autres séquences.

Une solution assez immédiate en Python se base sur le slicing. On va parcourir la séquence principale et en extraire successivement toutes les sous-séquences possibles et les comparer avec la sous-séquence que l'on cherche :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def issubsequence(subseq, seq):
    n = len(subseq)
    for i in range(0,len(seq)-n+1):
        if seq[i:i+n] == subseq:
            return True
    return False

On peut visualiser comment ce code fonctionne à l'aide de la figure 2. On va donc positionner la sous-séquence en face de la séquence principale, ce qui pourra se faire de l'indice kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp à l'indice len(seq) - len(subseq) (on doit faire +1 lorsqu'on utilise la fonction range, car la seconde borne n'est pas incluse). Grâce à un slicing, on découpe une partie de la séquence principale, de la même longueur que la sous-séquence, que l'on compare avec cette dernière avec l'opérateur ==. Si on a correspondance, on arrête tout en renvoyant True et si on n'a jamais trouvé correspondance, la boucle se termine et on renvoie False.

Image non disponible
Figure 2. On teste si une chaine est une sous-séquence d'une autre en l'alignant à toutes les positions possibles et en vérifiant si les valeurs aux indices correspondants sont les mêmes.
VI-B-1-c-iv. Nombre de voyelles

Le quatrième problème consiste à compter le nombre de voyelles que contient une chaine de caractères. Pour cela, il suffit de parcourir tous les caractères de la chaine, de tester s'il s'agit ou non d'une voyelle et de les compter pour avoir le résultat attendu :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def nbvowels(s):
    result = 0
    for c in s:
        if c in 'aeiou':
            result += 1
    return result

Remarquez que la condition c in 'aeiou' est un raccourci pour c == 'a' or c == 'e' or c == 'i' or c == 'o' or c == 'u', que l'on peut se permettre grâce à la présence de l'opérateur in.

VI-B-1-c-v. Caractères uniques

Ce cinquième problème consiste à extraire, depuis une chaine de caractères, la liste des caractères uniques qui y sont présents. Il suffit pour cela de parcourir la chaine de caractères, caractère par caractère. Chaque caractère passé en revue a soit déjà été rencontré ou est un nouveau. Il faut ignorer ceux qu'on a déjà rencontrés et stocker les nouveaux. Pour cela, on va à chaque fois vérifier si on a déjà rencontré le caractère grâce à l'opérateur in.

L'implémentation est directe, et utilise une liste seen qui contient, à tout moment, les caractères uniques déjà rencontrés.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def uniquechars(s):
    seen = []
    for c in s:
        if c not in seen:
            seen.append(c)
    return seen

On verra au chapitre suivant, dans la deuxième partie de ce cours, une structure de données plus adaptée pour représenter l'ensemble des caractères déjà rencontrés.

VI-B-1-c-vi. Filtrage

Le sixième problème consiste à filter une séquence de nombres selon un critère donné. Ici, on souhaite ne garder que les nombres qui sont strictement positifs. On parcourt les éléments de la séquence reçue en paramètre, ne gardant que ceux qui sont strictement positifs, en les ajoutant dans une autre liste, initialisée à [] et renvoyée par la fonction :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def filter_positive(data):
    result = []
    for elem in data:
        if elem > 0:
            result.append(elem)
    return result

On pourrait rendre cette fonction plus générique en passant le critère à appliquer en paramètre, comme on l'a fait à la section 4.1. On définit une nouvelle fonction filter avec un paramètre accept, qui est une fonction qui doit renvoyer un booléen qui signale si on doit garder ou non l'élément qu'on lui fournit en paramètre. On peut réécrire l'exemple précédent comme suit :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
def positive(n):
    return n > 0

def filter(data, accept):
    result = []
    for elem in data:
        if accept(elem):
            result.append(elem)
    return result

def filter_positive(data):
    return filter(data, positive)

précédentsommaire

Copyright © 2018 UKO. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.