Expressions régulièresDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Théorie des langages · C2 Langages réguliers · Chapitre 3 · 5 h

Expressions régulières

Syntaxe et sémantique ; théorème de Kleene, expressions régulières ↔ automates finis ; construction de Thompson et élimination d'états.

Vous connaissez déjà les expressions régulières sans le savoir : c'est la syntaxe \d{2}/\d{2} qui valide une date dans un formulaire, [a-z]+@[a-z]+\.[a-z]+ qui filtre une adresse de courriel, le motif tapé dans la recherche d'un éditeur de texte. Ce chapitre montre ce qu'elles sont vraiment — une troisième façon de décrire un langage régulier, purement textuelle — et révèle le résultat qui unifie tout le bloc II : le théorème de Kleene, qui affirme qu'expressions régulières et automates finis décrivent exactement les mêmes langages.

Trois descriptions, un seul monde. C'est l'un des plus beaux résultats du cours, et le plus utile en pratique.

Syntaxe : trois opérations et rien d'autre

Une expression régulière sur un alphabet Σ\Sigma se construit inductivement (chapitre 2), à partir de briques élémentaires et de trois opérations. Les briques :

Les opérations, appliquées à des expressions déjà construites :

ÉcritureNomPriorité
EFE \mid Funion (alternative)la plus faible
EFE \cdot F ou EFEFconcaténationintermédiaire
EE^*étoile de Kleenela plus forte

La priorité se lit comme en arithmétique : l'étoile est le « puissance », la concaténation le « produit », l'union la « somme ». Ainsi abcab^* \mid c se lit (a(b))c\big(a(b^*)\big) \mid c, et les parenthèses servent à forcer un autre regroupement, comme dans (ab)(a \mid b)^*.

Ce sont exactement les trois opérations sur les langages du chapitre 1. Une expression régulière n'est rien d'autre qu'une notation compacte pour les combiner — pas un mécanisme nouveau.

Sémantique : le langage dénoté

À chaque expression EE correspond un langage L(E)L(E), défini en suivant sa construction :

L(a)={a},L(EF)=L(E)L(F),L(EF)=L(E)L(F),L(E)=L(E).L(a) = \{a\}, \quad L(E \mid F) = L(E) \cup L(F), \quad L(E \cdot F) = L(E) \cdot L(F), \quad L(E^*) = L(E)^*.

Lisons quelques exemples sur Σ={a,b}\Sigma = \{a, b\} — cette lecture est le savoir-faire du chapitre :

Ce dernier exemple mérite qu'on s'y arrête : ce que l'AFD du chapitre 3 exprimait par trois états et six transitions tient en une courte expression. C'est la force de la notation — mais rappelez-vous qu'elle décrit exactement le même langage, ni plus ni moins.

Deux pièges classiques, tous deux liés au chapitre 1 :

Quiz · 1 question

Que dénote l'expression régulière (a|b)*a sur Σ = {a, b} ?

  • Les mots qui contiennent au moins un aprésence d'un a
  • Les mots qui se terminent par la lettre adernière lettre
  • Les mots formés uniquement de aque des a

Réponse : (a|b)* dénote n'importe quelle suite de a et de b — c'est-à-dire Σ* tout entier, un préfixe quelconque. La concaténation avec le a final impose que ce préfixe quelconque soit suivi d'un a : le mot se TERMINE donc par a. « Contenir au moins un a » se noterait (a|b)*a(a|b)* ; « uniquement des a » se noterait a*. La position du a dans l'expression fixe sa position dans le mot.

Le théorème de Kleene

Voici le résultat qui referme le bloc et justifie tout ce qui précède :

Théorème de Kleene. Un langage est décrit par une expression régulière si et seulement si il est reconnu par un automate fini.

Les trois descriptions — AFD, AFN, expression régulière — sont donc équivalentes : elles définissent une seule et même classe, les langages réguliers. C'est pourquoi on peut passer librement de l'une à l'autre selon ce qui est commode : écrire une expression régulière (concis), la transformer en automate pour l'exécuter efficacement (rapide), raisonner sur l'automate pour prouver une propriété.

La preuve se fait dans les deux sens, et chaque sens est une construction algorithmique — pas seulement une existence. C'est ce qui la rend utile : elle donne les recettes qu'emploient les outils réels.

D'une expression à un automate : Thompson

Le sens « expression → automate » emploie la construction de Thompson. Elle procède inductivement, exactement selon la structure de l'expression, en assemblant de petits AFN avec des ε-transitions (chapitre 4) — dont c'est ici l'usage principal :

Le résultat est un AFN avec ε-transitions, qu'on rend déterministe par la construction du chapitre 4 si l'on veut l'exécuter. C'est très exactement la chaîne qu'un moteur d'expressions régulières parcourt en interne.

D'un automate à une expression : élimination d'états

Le sens inverse, « automate → expression », emploie l'élimination d'états. On retire les états un par un ; à chaque retrait, on reporte l'information perdue sur les transitions restantes, en les étiquetant non plus par des lettres mais par des expressions régulières. À la fin, il ne reste qu'une transition de l'entrée à la sortie, étiquetée par l'expression régulière cherchée.

Vous n'avez pas à mémoriser les détails de ces deux constructions en L2 ; vous devez retenir qu'elles existent, qu'elles sont effectives (on peut les programmer), et qu'ensemble elles prouvent le théorème de Kleene par double construction.

Quiz · 1 question

Un ingénieur écrit une expression régulière pour valider un format de saisie, puis a besoin de la vérifier des millions de fois par seconde sur un flux. Que permet le théorème de Kleene dans ce contexte ?

  • Rien : les expressions régulières et les automates sont des mondes séparésmondes séparés
  • Transformer l'expression en automate (Thompson, puis déterminisation) pour obtenir une reconnaissance rapide en une seule passeconversion vers un automate exécutable
  • Prouver que l'expression est fausse si elle décrit un langage infinilangage infini

Réponse : Le théorème de Kleene garantit qu'à toute expression régulière correspond un automate reconnaissant le même langage, et la construction de Thompson suivie de la déterminisation (chapitre 4) le produit effectivement. On écrit donc l'expression pour sa concision, puis on la compile en AFD pour l'exécuter en une seule passe, en temps proportionnel à la longueur de l'entrée. C'est exactement ce que fait un analyseur lexical (chapitre 9). Un langage infini n'a rien d'anormal — a* en décrit déjà un.

À vous

L'exercice donne aux expressions régulières leur sens en code. Vous implémentez la sémantique — chaque opérateur (union, concaténation, étoile) est l'opération correspondante sur les langages du chapitre 1 — puis vous calculez le langage de (ab)ab(a \mid b)^* ab et vérifiez quels mots il contient.

L'étoile vous forcera à traiter le cas « zéro copie » : oublier le mot vide initial rend l'opération fausse. Et vous devrez borner par une longueur, parce que le langage est infini — une petite expression décrit un langage sans fin, tout comme un automate avec ses boucles. C'est là, concrètement, le théorème de Kleene.

Exercice de code

Implémentez la sémantique d'une expression régulière : chaque opérateur (union, concaténation, étoile) est une opération sur les langages du chapitre 1. Calculez le langage de (a|b)*ab et vérifiez quels mots il contient — l'étoile vous rappellera le piège du « zéro copie ».

Point de départ

// Une expression régulière décrit un LANGAGE. Ici on la représente par un
// arbre, et on calcule le langage qu'elle dénote — jusqu'à une longueur max,
// car l'étoile produit un ensemble infini.
//
// Opérateurs (rappel chapitre 1) :
//   lettre  -> { "a" }              vide -> {}          epsilon -> { "" }
//   union(E,F)  = L(E) ∪ L(F)
//   concat(E,F) = L(E) · L(F)   (tous les u·v, u∈L(E), v∈L(F))
//   etoile(E)   = L(E)*         (zéro, un ou plusieurs mots de L(E))

// Constructeurs d'arbre :
const lettre = (c) => ({ type: "lettre", c });
const epsilon = { type: "epsilon" };
const vide = { type: "vide" };
const union  = (g, d) => ({ type: "union", g, d });
const concat = (g, d) => ({ type: "concat", g, d });
const etoile = (e) => ({ type: "etoile", e });

// ── À VOUS : la sémantique ──────────────────────────────────────────────────
// langage(expr, max) rend l'ENSEMBLE (Set) des mots de longueur <= max décrits
// par expr.
function langage(expr, max) {
  switch (expr.type) {
    case "vide":    return new Set();          // aucun mot
    case "epsilon": return new Set([""]);      // le seul mot vide
    case "lettre":  return new Set([expr.c]);  // un seul mot d'une lettre
    case "union": {
      // à compléter : réunir les deux langages
      return new Set();
    }
    case "concat": {
      // à compléter : tous les u+v (u du gauche, v du droit), longueur <= max
      return new Set();
    }
    case "etoile": {
      // à compléter : ε, puis L, puis L·L, ... tant que la longueur <= max
      return new Set();
    }
  }
}

// ── L'expression (a|b)*ab  :  « se termine par ab » ─────────────────────────
const expr = concat(etoile(union(lettre("a"), lettre("b"))), concat(lettre("a"), lettre("b")));

const mots = [...langage(expr, 3)].sort((x, y) => x.length - y.length || x.localeCompare(y));
console.log("(a|b)*ab, mots de longueur <= 3 :");
console.log("  [" + mots.join(", ") + "]");
console.log("  ab present ?", mots.includes("ab"));   // attendu true
console.log("  aab present ?", mots.includes("aab")); // attendu true
console.log("  ba present ?", mots.includes("ba"));   // attendu false

Solution

function langage(expr, max) {
  switch (expr.type) {
    case "vide":    return new Set();
    case "epsilon": return new Set([""]);
    case "lettre":  return new Set([expr.c]);
    case "union": {
      const g = langage(expr.g, max), d = langage(expr.d, max);
      return new Set([...g, ...d]);              // ∪
    }
    case "concat": {
      const g = langage(expr.g, max), d = langage(expr.d, max);
      const r = new Set();
      for (const u of g) for (const v of d)
        if (u.length + v.length <= max) r.add(u + v);   // ·, borné
      return r;
    }
    case "etoile": {
      // ε toujours (zéro copie), puis on ajoute L·(ce qu'on a déjà) tant
      // que ça grandit et que la longueur reste <= max.
      const base = langage(expr.e, max);
      const r = new Set([""]);
      let frontiere = new Set([""]);
      while (frontiere.size > 0) {
        const suivante = new Set();
        for (const u of frontiere) for (const v of base) {
          const w = u + v;
          if (w.length <= max && !r.has(w)) { r.add(w); suivante.add(w); }
        }
        frontiere = suivante;
      }
      return r;
    }
  }
}
// (a|b)*ab, longueur <= 3 : ab, aab, bab, abab n'apparaît pas (longueur 4).
// → [ab, aab, bab]. « ab » et « aab » présents, « ba » absent. ✓

// ── Ce que l'exercice enseigne ──────────────────────────────────────────────
//
// 1. Une expression régulière n'est PAS de la magie de recherche : c'est une
//    notation pour trois opérations sur les langages du chapitre 1 — union,
//    concaténation, étoile de Kleene. Chaque case du switch EST une de ces
//    opérations. Comprendre une regex = savoir quel langage elle dénote.
//
// 2. L'étoile force à gérer le cas « zéro copie » (ε ∈ L* toujours) — le
//    piège du chapitre 1 ressort ici : oublier le new Set([""]) initial rend
//    l'étoile fausse.
//
// 3. On a dû BORNER par une longueur max : le langage de (a|b)*ab est infini.
//    Une expression finie décrit un langage potentiellement infini — c'est
//    précisément ce que fait aussi un automate avec ses boucles, et le
//    théorème de Kleene dit que ce sont les MÊMES langages.

Ce que la suite en fait

Le bloc II tient debout : trois descriptions équivalentes des langages réguliers, reliées par des constructions effectives. Une question reste ouverte, et c'est la plus importante — tous les langages sont-ils réguliers ?

Le chapitre 6 y répond, et la réponse est non. Il donne d'abord l'outil pour ranger un automate (la minimisation), recense ce que la classe régulière sait faire (les propriétés de clôture), puis l'outil qui prouve qu'un langage échappe aux automates : le lemme de pompage, second point qui coince de l'année, avec son exemple canonique anbna^n b^n — celui-là même que l'exercice du chapitre 1 avait déjà mis de côté.

À retenir

Flashcards · 4 cartes

De quelles briques et de quelles opérations une expression régulière est-elle faite ?
Briques : ∅ (langage vide), ε (le mot vide), et chaque lettre a. Opérations : union E|F (∪), concaténation E·F (·), étoile E* (étoile de Kleene) — par priorité croissante étoile > concaténation > union. Ce sont EXACTEMENT les trois opérations sur les langages du chapitre 1 ; une expression régulière n'est qu'une notation compacte pour les combiner.
Que dit le théorème de Kleene ?
Un langage est décrit par une expression régulière si et seulement s'il est reconnu par un automate fini. Les trois descriptions — AFD, AFN, expression régulière — sont donc équivalentes et définissent une seule classe : les langages réguliers. On passe de l'une à l'autre par des constructions effectives (Thompson dans un sens, élimination d'états dans l'autre).
Comment passe-t-on d'une expression régulière à un automate, et inversement ?
Expression → automate : la construction de Thompson, inductive sur la structure de l'expression, assemble de petits AFN à l'aide d'ε-transitions (une branche par |, un branchement pour ·, une boucle sautable pour *). Automate → expression : l'élimination d'états retire les états un à un en reportant l'information sur des transitions étiquetées par des expressions régulières. Les deux sont effectives — on peut les programmer.
Quel piège du chapitre 1 ressort dans l'interprétation d'une étoile ?
E* dénote toujours un langage contenant ε (la « zéro copie »), même si ε ∉ L(E). Oublier ce cas rend l'interprétation fausse. Corollaire souvent piégeant : ∅* = {ε}, alors que ∅ seul est le langage vide. C'est la distinction ε / ∅ du chapitre 1, qui revient à l'identique.

Exercices d'entraînement

Exercice 1

Écrire des expressions régulières

Sur Σ={a,b}\Sigma = \{a, b\}, donner une expression régulière pour chacun de ces langages :

  1. les mots contenant au moins un a ;
  2. les mots de longueur paire ;
  3. les mots ne contenant pas le facteur aa.

Correction

  1. (ab)a(ab)(a \mid b)^* \, a \, (a \mid b)^* : un a quelque part, entouré de n'importe quoi.
  2. ((ab)(ab))\big((a \mid b)(a \mid b)\big)^* : on répète des blocs de deux lettres, ce qui donne exactement les longueurs paires (dont ε\varepsilon).
  3. (bab)(aε)(b \mid ab)^* \, (a \mid \varepsilon) : tout a est immédiatement suivi d'un b (bloc ab), sauf éventuellement un a final isolé. Aucun aa ne peut ainsi apparaître.

Exercice 2

Décrire un langage

Décrire en français le langage dénoté par chacune de ces expressions, sur Σ={a,b}\Sigma = \{a, b\} :

  1. aba^* b^* ;
  2. (ab)(ab)^* ;
  3. (ab)aa(ab)(a \mid b)^* \, aa \, (a \mid b)^*.

Correction

  1. Les mots formés d'une suite de a suivie d'une suite de b (chacune éventuellement vide) : autrement dit, aucun a n'apparaît après un b. Attention, ce n'est pas {anbn}\{a^n b^n\} : les nombres de a et de b sont indépendants.
  2. Les mots obtenus en répétant le bloc ab : ε\varepsilon, ab, abab, ababab, …
  3. Les mots contenant le facteur aa (deux a consécutifs quelque part).

Exercice 3

Du théorème de Kleene

Donner une expression régulière décrivant le langage reconnu par l'AFD « mots se terminant par a » (croisé au chapitre 3). Que garantit le théorème de Kleene à propos d'une telle conversion ?

Correction

Un tel mot est une suite quelconque de lettres, suivie d'un a final : (ab)a.(a \mid b)^* \, a.

Le théorème de Kleene garantit que cette conversion est toujours possible, dans les deux sens : à tout automate fini correspond une expression régulière du même langage (par élimination d'états), et réciproquement (par la construction de Thompson). Automates et expressions régulières décrivent exactement la même classe — les langages réguliers.