Partie II - Structures de donnees · Chapitre 7

Chaînes de caractères

3 sections de cours · fiche d'exercices corrigee

1Chaines : structure, Unicode et methodes

Definition

Une chaine de caracteres (str) est une sequence ordonnee et immuable de caracteres Unicode. Immuable signifie qu'aucune operation ne peut modifier une chaine existante : toute « modification » construit en realite une nouvelle chaine.

s = "MPSI"
s[0]              # 'M'
s[0] = "X"        # TypeError: 'str' object does not support item assignment
s = "X" + s[1:]   # correct : on construit une nouvelle chaine, "XPSI"

Ecriture des litteraux

'simple'                  "double"          # equivalents
"l'apostrophe"            'le "guillemet"'  # choisir le delimiteur qui evite l'echappement
"""chaine
sur plusieurs lignes"""
"tabulation\tretour\nligne"                 # sequences d'echappement
r"C:\nouveau\dossier"                       # chaine BRUTE : \n n'est pas interprete
SequenceSignification
\nretour a la ligne
\ttabulation
\\anti-slash litteral
\', \"apostrophe, guillemet
\u00e9caractere Unicode par son code (ici é)

Unicode, ord et chr

Chaque caractere possede un point de code entier. Les fonctions ord et chr font la conversion dans les deux sens.

ord("A")      # 65        ord("a")  # 97
ord("0")      # 48        ord("é")  # 233
chr(66)       # 'B'
chr(ord("a") + 3)         # 'd'
Proprietes utiles (table ASCII)
  • les majuscules A..Z occupent les codes 65 a 90 ;
  • les minuscules a..z les codes 97 a 122 ;
  • l'ecart minuscule − majuscule vaut donc 32 pour toute lettre ;
  • les chiffres 0..9 occupent 48 a 57, d'ou int(c) == ord(c) - 48.
def majuscule(c):
    """Version majuscule de c si c est une minuscule non accentuee."""
    if "a" <= c <= "z":
        return chr(ord(c) - 32)
    return c

Operations de base

s = "informatique"
len(s)              # 12
s[3]                # 'o'
s[-1]               # 'e'
s[0:5]              # 'infor'
s[::-1]             # 'euqitamrofni'
"a" in s            # True
s.count("i")        # 3
s.index("m")        # 4       (ValueError si absent)
s.find("z")         # -1      (pas d'exception)
"info" + "rmatique" # concatenation
"ab" * 3            # 'ababab'

Les methodes indispensables

MethodeEffetExemple
lower(), upper()casse "MPSI".lower() → 'mpsi'
strip()retire les blancs aux extremites " a ".strip() → 'a'
split(sep)decoupe en liste "a,b".split(",") → ['a','b']
sep.join(t)recolle une liste "-".join(["a","b"]) → 'a-b'
replace(a, b)remplace toutes les occurrences "aaa".replace("a","b") → 'bbb'
startswith, endswithprefixe, suffixe "fichier.py".endswith(".py")
isdigit, isalpha, isupper tests de nature"42".isdigit() → True
Toutes ces methodes RENVOIENT une chaine
s = "  MPSI  "
s.strip()          # renvoie 'MPSI' mais s est INCHANGEE
s = s.strip()      # il faut reaffecter
Le piege de la concatenation en boucle
s = ""
for c in t:
    s += c          # MAUVAIS : Theta(n^2) au total

Chaque += recopie toute la chaine (elle est immuable !). Sur \(n\) caracteres, le cout total est \(1 + 2 + \cdots + n = \Theta(n^2)\). La bonne facon :

s = "".join(t)      # Theta(n)

Sur \(10^5\) caracteres, la difference est de plusieurs secondes contre quelques millisecondes.

2Algorithmes sur les chaines et chiffrement

Parcours d'une chaine

s = "MPSI"

for c in s:                      # par caractere
    print(c)

for i in range(len(s)):          # par indice
    print(i, s[i])

for i, c in enumerate(s):        # les deux
    print(i, c)

Algorithmes classiques

def compte_voyelles(s):
    """Nombre de voyelles (non accentuees) dans s."""
    c = 0
    for x in s.lower():
        if x in "aeiouy":
            c += 1
    return c

def est_palindrome(s):
    """s est-elle un palindrome, en ignorant casse et espaces ?"""
    t = [c.lower() for c in s if c.isalpha()]
    g, d = 0, len(t) - 1
    while g < d:
        if t[g] != t[d]:
            return False
        g += 1
        d -= 1
    return True

def frequences(s):
    """Dictionnaire lettre -> nombre d'occurrences."""
    f = {}
    for c in s.lower():
        if c.isalpha():
            f[c] = f.get(c, 0) + 1
    return f

def inverse_mots(phrase):
    """'le chat dort' -> 'dort chat le'."""
    return " ".join(phrase.split()[::-1])

assert compte_voyelles("Informatique") == 6
assert est_palindrome("Esope reste ici et se repose")
assert inverse_mots("le chat dort") == "dort chat le"

Recherche d'un motif

Probleme

Etant donne un texte \(T\) de longueur \(n\) et un motif \(M\) de longueur \(m\), determiner les positions \(i\) telles que \(T[i..i+m-1] = M\).

def recherche_naive(texte, motif):
    """Liste des positions d'apparition de motif dans texte. O(n*m)."""
    n, m = len(texte), len(motif)
    positions = []
    for i in range(n - m + 1):
        j = 0
        while j < m and texte[i+j] == motif[j]:
            j += 1
        if j == m:
            positions.append(i)
    return positions

assert recherche_naive("abababa", "aba") == [0, 2, 4]

Dans le pire des cas (texte "aaaa...a", motif "aaa...ab"), chaque position echoue au dernier caractere : \(\Theta((n-m+1)m)\) comparaisons. Les algorithmes de Knuth-Morris-Pratt ou de Boyer-Moore ramenent ce cout a \(\Theta(n+m)\) en exploitant l'information des comparaisons deja faites.

Chiffrement par decalage (chiffre de Cesar)

Principe

Chaque lettre est remplacee par celle situee \(k\) rangs plus loin dans l'alphabet, modulo 26. Si \(x\) est le rang de la lettre (\(0\) pour a), la lettre chiffree a pour rang \((x+k) \bmod 26\). Le dechiffrement se fait avec \(-k\).

def cesar(s, k):
    """Chiffre s par decalage de k. Les caracteres non alphabetiques sont conserves."""
    res = []
    for c in s:
        if "a" <= c <= "z":
            res.append(chr((ord(c) - ord("a") + k) % 26 + ord("a")))
        elif "A" <= c <= "Z":
            res.append(chr((ord(c) - ord("A") + k) % 26 + ord("A")))
        else:
            res.append(c)
    return "".join(res)

assert cesar("abc xyz", 3) == "def abc"
assert cesar(cesar("Bonjour MPSI !", 7), -7) == "Bonjour MPSI !"

Le % 26 gere a la fois le retour au debut de l'alphabet et les decalages negatifs, grace au signe du modulo Python.

Cryptanalyse par force brute

Il n'y a que 26 cles possibles : le chiffre de Cesar se casse instantanement.

def casse_cesar(chiffre):
    """Affiche les 26 dechiffrements possibles."""
    for k in range(26):
        print(f"{k:2} : {cesar(chiffre, -k)}")

Une methode plus fine utilise l'analyse frequentielle : en francais, le e represente environ 15 % des lettres. Le decalage cherche est celui qui envoie la lettre la plus frequente du chiffre sur e.

def casse_par_frequence(chiffre):
    """Retrouve la cle en supposant que la lettre la plus frequente code un 'e'."""
    f = frequences(chiffre)
    if not f:
        return 0
    plus_frequente = max(f, key=f.get)
    return (ord(plus_frequente) - ord("e")) % 26

3Cesar interactif, formatage et conversions

Chiffre de Cesar : chiffrement pas a pas
Saisir un texte, choisir un decalage, puis « Initialiser ».

Formatage et affichage

nom, note = "Amine", 15.5
f"{nom} : {note}/20"              # 'Amine : 15.5/20'
f"{nom:<10}|"                     # aligne a gauche sur 10 colonnes
f"{nom:>10}|"                     # a droite
f"{nom:^10}|"                     # centre
f"{note:.1f}"                     # une decimale
f"{0.856:.1%}"                    # '85.6%'
# tableau aligne
eleves = [("Amine", 15.5), ("Sara", 18.0), ("Youssef", 12.25)]
print(f"{'Nom':<12}{'Note':>8}")
print("-" * 20)
for nom, n in eleves:
    print(f"{nom:<12}{n:>8.2f}")

Conversions chaine / nombre

int("42")           # 42
int("1010", 2)      # 10     : lecture en base 2
int("ff", 16)       # 255
float("3.14")       # 3.14
str(42)             # '42'
f"{255:b}"          # '11111111'
f"{255:x}"          # 'ff'
def vers_entier(s):
    """Convertit une chaine de chiffres en entier, sans utiliser int()."""
    n = 0
    for c in s:
        if not ("0" <= c <= "9"):
            raise ValueError(f"caractere invalide : {c!r}")
        n = n * 10 + (ord(c) - ord("0"))     # schema de Horner en base 10
    return n

assert vers_entier("2024") == 2024

L'essentiel du chapitre

  • Une chaine est immuable : toute modification cree une nouvelle chaine.
  • Ne jamais concatener en boucle : utiliser "".join(liste) pour rester en \(\Theta(n)\).
  • ord / chr font le lien avec les entiers ; l'ecart minuscule/majuscule vaut 32.
  • split et join sont reciproques l'une de l'autre.
  • La recherche naive d'un motif coute \(O(nm)\) ; KMP la ramene a \(\Theta(n+m)\).
  • Le chiffre de Cesar repose sur \((x+k) \bmod 26\) et se casse par analyse frequentielle.
Fiche d'exercices — chapitre 7Enonces gradues, du calcul d'application au niveau concours, avec corriges detailles.
Ouvrir la fiche →