Exercice 2 : Complexité temporelle
Problème guidé : Analyse fine de la complexité temporelle et limites du modèle RAM en Python
Section titled “Problème guidé : Analyse fine de la complexité temporelle et limites du modèle RAM en Python”Énoncé
Section titled “Énoncé”Le présent problème s’intéresse à l’évaluation rigoureuse de la complexité temporelle d’algorithmes écrits en Python. En classe préparatoire, l’analyse algorithmique repose généralement sur le modèle de la machine à accès aléatoire (RAM - Random Access Machine), où chaque opération élémentaire (affectation, addition, comparaison, accès à un indice de tableau) s’effectue en temps constant .
Cependant, la mise en œuvre pratique de ces algorithmes en Python confronte le scientifique à des subtilités d’implémentation (gestion dynamique de la mémoire, représentation des entiers de taille arbitraire, surcoût lié à l’interpréteur) qui invalident parfois les prédictions théoriques naïves.
L’objectif de ce problème est de formaliser l’analyse de complexité de plusieurs fonctions de manipulation de listes et de chaînes de caractères, puis de proposer une analyse critique des limites du modèle RAM face à la réalité de la machine physique et du langage Python.
Questions
Section titled “Questions”Partie I : Analyse théorique (Modèle RAM)
Section titled “Partie I : Analyse théorique (Modèle RAM)”-
(Échauffement) Soit la fonction suivante :
def somme_carres(n):s = 0for i in range(n):s += i * ireturn sDéterminer la complexité temporelle au pire des cas de
somme_carres(n)en fonction de , exprimée sous forme de grand . -
(Concaténation naïve) On considère la fonction suivante censée construire une chaîne de caractères par répétition :
def repete_chaine(c, n):res = ""for i in range(n):res += creturn resEn supposant naïvement que l’opération de concaténation de chaînes de longueur et s’exécute en temps , déterminer la complexité théorique de
repete_chaine(c, n)dans le modèle RAM. -
(Recherche et filtrage) Soit une liste de entiers distincts et un tableau de hachage (ou
seten Python) contenant éléments. On considère la fonction :def filtre_communs(L, S):resultat = []for x in L:if x in S:resultat.append(x)return resultatExprimer la complexité temporelle au pire des cas de cette fonction en fonction de et , en justifiant le coût des opérations sur le type
setet sur la liste.
Partie II : Analyse critique et limites du modèle RAM
Section titled “Partie II : Analyse critique et limites du modèle RAM”-
(Le coût de la représentation des données) Dans la Partie I, question 1, on a implicitement supposé que l’addition
s += i * is’effectue en .- Rappeler comment Python gère les entiers de grande taille (au-delà de ).
- Quel est le véritable coût temporel d’une addition de deux entiers de taille (en nombre de bits) ? En déduire une limite du modèle RAM pour des valeurs de très grandes.
-
(Immuabilité et allocation dynamique) Dans la question 2, la fonction utilise l’opérateur
+=sur les chaînes de caractères (qui sont immuables en Python).- Expliquer ce que fait réellement l’interpréteur Python lors de l’exécution de
res += c. - En déduire la véritable complexité temporelle de la fonction
repete_chaineet proposer une alternative optimale en .
- Expliquer ce que fait réellement l’interpréteur Python lors de l’exécution de
-
(Effets de seuil et mémoire cache) On mesure expérimentalement le temps d’exécution d’un accès aléatoire dans une liste Python de taille . Pour , le temps moyen par accès est de l’ordre de . Pour , ce temps passe à .
- Le modèle RAM permet-il d’expliquer cette variation ?
- Quel phénomène matériel majeur (absent du modèle RAM) permet d’expliquer cet écart ?
Pistes et indications
Section titled “Pistes et indications”- Q2 : Pensez à sommer les longueurs intermédiaires des chaînes de caractères à chaque itération de la boucle.
- Q3 : Rappelez la complexité amortie d’un ajout dans une liste (
append) et la complexité moyenne d’une recherche dans unseten Python (table de hachage). - Q5 : L’immuabilité implique qu’aucune modification en place n’est possible. Une nouvelle zone mémoire doit donc être allouée et le contenu recopié.
- Q6 : Songez à la hiérarchie mémoire des ordinateurs modernes (registres, caches L1/L2/L3, mémoire vive RAM).
Corrigé pas à pas
Section titled “Corrigé pas à pas”Partie I : Analyse théorique (Modèle RAM)
Section titled “Partie I : Analyse théorique (Modèle RAM)”-
Complexité de
somme_carres(n):- L’initialisation
s = 0prend un temps . - La boucle
for i in range(n)s’exécute fois. - À chaque itération, l’évaluation de
i * i(multiplication) et l’opération+=(addition et affectation) coûtent des opérations élémentaires, soit dans le modèle RAM. - La complexité totale est donc .
- L’initialisation
-
Complexité naïve de
repete_chaine(c, n):- Soit la longueur de la chaîne
resà l’itération . Au départ, . À l’itération , . - L’opération
res += ccrée une nouvelle chaîne de longueur . Si l’on suppose que le coût de la concaténation de deux chaînes de longueurs et est proportionnel à , le coût de la -ième étape est . - Le coût total est la somme des coûts de chaque itération :
- Attention : Une analyse superficielle aurait pu conclure à en comptant opérations de concaténation. La prise en compte de la taille croissante des données élève la complexité à .
- Soit la longueur de la chaîne
-
Complexité de
filtre_communs(L, S):- La liste contient éléments. La boucle
for x in Leffectue tours. - À chaque tour, le test d’appartenance
x in Ss’effectue sur unset(table de hachage). En moyenne, le coût d’une recherche dans unsetest en . - Si l’élément est présent, l’opération
resultat.append(x)est exécutée. En Python, la méthodeappendsur une liste possède une complexité amortie en . - La complexité totale moyenne est donc de . (Au pire, en cas de collisions massives dans la table de hachage, le test d’appartenance peut dégénérer en , menant à , mais le cas moyen est bien ).
- La liste contient éléments. La boucle
Partie II : Analyse critique et limites du modèle RAM
Section titled “Partie II : Analyse critique et limites du modèle RAM”-
Le coût de la représentation des entiers :
- En Python, les entiers ne sont pas limités à bits ; le langage gère automatiquement les entiers à précision arbitraire (bignums). Lorsque la valeur dépasse la taille native de la machine, Python utilise un tableau de blocs mémoire.
- L’addition de deux entiers de taille bits (exprimée en nombre de mots mémoire) ne se fait plus en temps unitaire , mais en temps proportionnel au nombre de bits, soit (voire avec des algorithmes sophistiqués).
- Limite du modèle RAM : Lorsque devient très grand (par exemple ), la variable
ine tient plus dans un mot machine. L’hypothèse que l’arithmétique élémentaire coûte est mise en défaut.
-
Immuabilité et allocation dynamique :
- Les chaînes de caractères en Python sont immuables. Il est impossible de modifier un caractère “en place”.
- L’instruction
res += coblige l’interpréteur à allouer un nouvel espace mémoire d’une taille égale à la longueur actuelle plus un, à y copier l’ancien contenu deres, puis à y ajouter le caractèrec. C’est cette copie séquentielle qui induit le coût en démontré à la question 2. - Alternative optimale : Pour obtenir une complexité en , il convient de stocker les caractères dans une liste (structure mutable permettant des ajouts en temps amorti constant), puis d’utiliser la méthode
"".join(liste_c)qui effectue une unique allocation mémoire globale :def repete_chaine_optimal(c, n):res = [c for _ in range(n)]return "".join(res)
-
Effets de seuil et mémoire cache :
- Inadéquation du modèle RAM : Le modèle RAM postule que l’accès à n’importe quelle case mémoire se fait avec le même coût constant , qu’il s’agisse de la première ou de la milliardième.
- Réalité matérielle : La hiérarchie des caches processeur (L1, L2, L3) et la RAM physique violent cette hypothèse.
- Pour , la structure de données tient entièrement dans les caches rapides du processeur (accès très rapide ).
- Pour , la structure dépasse la capacité du cache. Le processeur subit des défauts de cache (cache misses), l’obligeant à aller chercher l’information dans la mémoire vive principale (RAM physique), ce qui multiplie le temps d’accès par un facteur significatif ().
- Conclusion critique : Le modèle RAM fournit une borne asymptotique purement théorique qui ignore la localité des données et l’architecture matérielle. En pratique, un algorithme théoriquement optimal en peut s’avérer plus lent qu’un algorithme en s’il génère de nombreux défauts de cache (ce qui est fréquent en Python en raison de l’indirection des pointeurs dans les listes, qui ne stockent que des références d’objets et non les données brutes de manière contiguë).