Modèle de données “ensemble”
Généralités
La formalisation du concept d'ensemble et l'édiction des règles qui permettent de les construire a été l'un des plus grand défis des mathématiques à la charnière entre le xix-ème siècle et le xx-ème siècle. Cette question a été centrale pour la résolution de la crise des fondements des mathématiques et a abouti à la construction axiomatique que l'on connaît aujourd'hui. Elle sera abordée dans le chapitre sur la calculabilité du cours de master Complexité algorithmique. Nous nous contenterons ici d'en donner une définition informelle qui suffira pour les besoins de ce cours.
Il existe un unique ensemble qui ne contient aucun élément, c'est l'ensemble vide. On le note \(\varnothing\) ou encore \(\{\}\). On appelle singleton tout ensemble réduit à un unique élément et paire tout ensemble réduit à deux éléments. On appelle sous-ensemble ou partie d'un ensemble \(F\) tout ensemble \(E\) dont tous les éléments sont aussi des éléments de \(F\), soit \(\forall x, x\in E\Rightarrow x\in F\). On écrit alors \(E\subseteq F\) où \(\subseteq\) est le symbole dit d'inclusion et on dit que \(E\) est inclus dans \(F\). Deux ensembles \(E\) et \(F\) sont égaux si et seulement si \(E\subseteq F\) et \(F\subseteq E\). L'ensemble de toutes les parties d'un ensemble \(E\) est appelé ensemble des parties de \(E\) et on le note \({\mathcal P}(E)\). Il contient en particulier la partie vide \(\varnothing\) et \(E\). Pour tout ensemble \(E\), l'inclusion définit une relation d'ordre partiel sur \({\mathcal P}(E)\). Si \(E\subseteq F\) et \(E\not = F\), on écrit \(E\subset F\) et on dit que \(E\) est strictement inclus dans \(F\).
On décrit un ensemble à l'aide d'accolades ouvrante \(\{\) et fermante \(\}\), qui servent de délimiteurs. Il existe deux manières de décrire un ensemble :Opérations ensemblistes
Soient \(E\) et \(F\) deux ensembles. On définit la différence ensembliste \(E\setminus F\) (on dit \(E\) privé de \(F\)) des deux ensembles \(E\) et \(F\) par l'ensemble des éléments de \(E\) qui n'appartiennent pas à \(F\) : \begin{equation} E\setminus F:=\{x\mid (x\in E)\wedge (x\not\in F)\}. \end{equation} Dans le cas particulier où \(F\) est une partie de \(E\), la différence symétrique \(E\setminus F\) est appelée complémentaire de \(F\) dans \(E\) et notée \(\complement_E F\). En général le contexte est non ambigü et l'ensemble contenant \(E\) est omis dans l'écriture, on écrit donc plus simplement \(\complement F\) ou \(\overline{F}\). On définit la réunion \(E\cup F\) des deux ensembles \(E\) et \(F\) par l'ensemble des éléments qui appartiennent à \(E\) ou à \(F\) : \begin{equation} E\cup F:=\{x\mid (x\in E)\vee (x\in F)\}. \end{equation} On définit l'intersection \(E\cap F\) des deux ensembles \(E\) et \(F\) par l'ensemble des éléments qui appartiennent à \(E\) et à \(F\) : \begin{equation} E\cap F:=\{x\mid (x\in E)\wedge (x\in F)\}. \end{equation} Deux ensembles dont l'intersection est vide sont dits disjoints. On définit la différence symétrique des deux ensembles \(E\) et \(F\) par l'ensemble des éléments qui appartiennent à \(E\) ou à \(F\) mais pas aux deux, i.e. \begin{equation} E\,\Delta\, F:=(E\cup F)\setminus(E\cap F). \end{equation}
Vous pouvez visualiser le résultat de ces opérations ensemblistes en les survolant ci-dessous. La zone en surbrillance fournit le résultat. Ce type de représentation s'appelle un diagramme de Venn :
Structures de données
Le lecteur trouvera dans le chapitre Modèle et structures de données liste les définitions des tableaux et des listes chaînées qui vont être utilisés ici. Pour l'aspect statique des opérations, au sens ou les ensembles impliqués ne sont pas modifiés, nous devons bien sûr pouvoir accéder à chaque élément d'un ensemble et les opérations courantes sont :Pour l'aspect dynamique, nous devons pouvoir :
Tableaux et listes
Ce sont en général les structures de données associées au modèle de données liste qui sont utilisées pour coder les ensembles. Autrement dit, un ensemble est codé par une liste de ses éléments, il bénéficie donc des mécanismes d'indexation quand la structure associée le permet, comme un tableau par exemple. Dans ce cas les éléments de l'ensemble sont par conséquent ordonnés à travers leurs positions dans la structure (cf. exercice ci-dessous). Dans toute la suite, \(E\) et \(F\) désignent deux ensembles de cardinal \(n\) et \(m\) respectivement.
Tableaux . Nous allons aborder succinctement les algorithmes réalisant les 8 opérations précédentes dans le cas où la structure pour coder un ensemble \(E\) est un tableau. Sans autre hypothèse, tester l'appartenance d'un objet \(x\) à \(E\) nécessite de parcourir la structure associée et le coût est de \(\Theta(1)\) dans le meilleur des cas (\(x\) est en première position) et \(\Theta(n)\) dans le pire des cas (\(x\) n'appartient pas au tableau). En revanche, si l'ensemble \(E\) peut-être muni d'une relation d'ordre total et que les éléments de \(E\) sont triés dans le tableau, la recherche peut se faire en \(\Theta(\log n)\) par dichotomie si \(n=\#E\).
Calculer le cardinal de \(E\) est une opération qui est généralement réalisée en \(\Theta(1)\), cette information étant souvent rangée dans la structure de donnée et mise à jour en cas d'ajout ou de suppression d'un élément. Dans le cas contraire, il faut évidemment \(\Theta(n)\) opérations.
La réunion de deux ensembles \(E\) et \(F\) consiste à créer un tableau résultat et à y ranger successivement les objets de \(E\) puis ceux de \(F\), le coût est donc \(\Theta(n+m)\).
Si les objets ne sont pas triés, le calcul de l'intersection se fait en créant un tableau résultat et en y rangeant chaque élément de \(E\) uniquement s'il appartient à \(F\). Le coût est donc \(\Theta(nm)\). Si les tableaux sont triés, on teste alors l'appartenance d'un élément du plus petit des deux ensembles (disons \(E\)) dans \(F\) par dichotomie, le coût est alors réduit en \(\Theta(n\log m)\).
La différence \(E\setminus F\) s'obtient de manière similaire au calcul de l'intersection, en s'assurant cette fois que l'élément courant n'appartient pas à \(F\). Les complexités sont donc identiques.
La différence symétrique est obtenue en appliquant successivement la réunion et l'intersection. (Peut-on faire un peu mieux ?)
Compte tenu de la structure statique de tableau, l'ajout d'un nouvel élément \(x\) dans \(E\) n'est pas réalisable directement. Cela nécessite de créer un nouveau tableau de taille \(n+1\) et d'y recopier le tableau initial avant de rajouter \(x\). Ceci suppose que \(x\) n'appartenait pas à \(E\) avant l'opération. Le coût est donc en \(\Theta(n\)). La suppression d'un élément \(x\) se passe de manière similaire en allouant un nouveau tableau de taille \(n-1\) puis en testant lors de la copie si l'élément courant à ranger est différent de \(x\). Le nouveau tableau peut bien sûr remplacer l'ancien si l'ajout ou la suppression d'un objet \(x\) nécessite ou non de le conserver.
Listes chaînées . Nous reprenons la même démarche que pour les tableaux mais cette fois avec la structure dynamique de liste chaînée. Tester l'appartenance d'un objet \(x\) nécessite de parcourir la liste, on retrouve donc la même complexité. En revanche l'absence d'accès direct ne permet pas de faire une recherche dichotomique pour baisser le coût.
La réunion est réalisée de la même manière que pour une structure statique, le coût est donc identique en \(\Theta(n+m)\). En revanche, si l'on n'a pas la nécessité de conserver les deux listes, il suffit de chaîner la dernière cellule de la liste chaînée codant \(E\) vers la première cellule de la liste chaînée codant \(F\) et dans ce cas le coût est \(\Theta(1)\).
L'intersection est réalisée de la même manière que pour les tableaux sans pouvoir bénéficier du tri des éléments, le coût reste donc en \(\Theta(nm)\).
Pour ajouter un élément qui n'est pas déjà dans la liste, il suffit de créer une nouvelle cellule en tête de liste ou en bout de liste, le coût est donc en \(\Theta(1)\). La suppression d'un objet présent dans le liste demande de le cherche en parcourant la liste, la suppression en elle même n'a qu'un coût de \(\Theta(1)\) puisqu'il suffit de décrocher la cellule et de la libérer de la mémoire, le coût est donc \(\Theta(n)\).
Nous verrons au chapitre Fonctions et tables de hachage une autre structure permettant, entre autres, de coder les ensembles et comment elle permet d'obtenir des opérations parfois moins coûteuses.
Algèbre de Boole
En logique, les trois opérateurs de négation, de disjonction et de conjonction sont notés respectivement \(\neg a\), \(a\vee b\) et \(a\wedge b\), mais le mathématicien britannique George Boole a noté des similitudes entre le calcul algébrique sur les nombres et le calcul en logique propositionnelle, il a donc algébraisé la logique. En interprétant \(0\) comme la valeur de vérité faux et 1 comme la valeur de vérité vrai, on peut munir l'ensemble \(B:=\{0,1\}\) d'une structure algébrique particulière appelée algèbre de Boole à l'aide de ces trois opérateurs. Pour cela, on leur réserve une notation plus adaptée au calcul algébrique :
|
|
|
Ces trois opérateurs fonctionnent de manière analogue à ce que leur notation algébrique suggère en héritant essentiellement des mêmes propriétés que s'il s'agissait du calcul dans \({\mathbf Z}\). La loi multiplicative est ainsi souvent notée \(\cdot\) et on omet parfois de l'écrire dans les expressions algébriques quand il n'y aucune ambiguité, \(x(y+z)\) au lieu de \(x.(y+z)\) ou \(x\times(y+z)\). La négation est donc involutive i.e. \(\overline{\overline{a}}=a\), le tiers exclus s'exprime \(a.\overline{a}=0\) et la non-contradiction \(a+\overline{a}=1\). On dispose d'autre part des propriétés suivantes :
Le principe de dualité permet de déduire les mêmes propriétés pour l'opérateur \(\times\). Il suffit d'échanger le rôle des opérateurs \(+\) et \(\times\) et celui des valeurs \(0\) et \(1\). Par exemple \(a\times 1=1\times a=a\) est la propriété duale de \(a+0=0+a=a\). Notons que \(1\) n'admet pas de symétrique pour l'addition mais qu'il est son propre symétrique pour la multiplication. On retrouve également les lois de De Morgan (du mathématicien britannique Augustus De Morgan) qui sont deux lois duales : \begin{align} \overline{a+b} &\equiv \overline{a}.\overline{b}\\ \overline{a.b} &\equiv \overline{a}+\overline{b} \end{align}
On définit trois nouveaux opérateurs, la disjonction exclusive, l'implication et l'équivalence à partir des trois premiers à l'aide des équivalences logiques suivantes : \begin{align} a\oplus b&\equiv (a+b)\cdot \overline{(a\cdot b)}\\ a\Rightarrow b&\equiv \overline{a}+b\\ a\Leftrightarrow b&\equiv (\overline{a}+b).(a+\overline{b}) \end{align} Leurs tables de vérité sont :
|
|
|
Si \(E\) est muni de deux lois de composition, \(\star\) et \(\bullet\), on peut disposer de la propriété de distributivité de \(\star\) sur \(\bullet\) :
Si la loi est commutative, la distributivité à gauche entraîne la distributivité à droite et réciproquement. Dans ce cas, on ne précise plus à gauche ou à droite.
Un ensemble \(E\) muni d'une loi \(\star\) associative, disposant d'un élément neutre et d'un élément symétrique pour tout élément de \(E\), constitue ce que l'on appelle un groupe. Ce groupe est un groupe commutatif si la loi \(\star\) est commutative. Par exemple \(({\mathbf R},+)\) est un groupe commutatif, tout comme \(({\mathbf R}^*,\times)\). De plus la distributivité de \(\times\) sur \(+\) font du triplet \(({\mathbf R},+,\times)\) ce que l'on nomme un corps (commutatif ici car la loi multiplicative \(\times\) est commutative).
Comme pour l'ensemble des nombres réels \({\mathbf R}\), on peut munir la paire \(\{0,1\}\) d'une structure de corps commutatif avec deux lois de composition internes, l'addition notée \(+\) et la multiplication notée \(\times\) ou \(\cdot\), dont les graphes sont fournis par les tables ci-dessous. On note ce corps \({\mathbf F}_2\).
|
|
Pour l'addition, les deux éléments \(0\) et \(1\) sont leurs propres symétriques (on dit aussi opposés pour une loi additive), i.e. \(0+0=0\) et \(1+1=0\) et pour la multiplication \(1\) est son propre symétrique (on dit aussi inverse pour une loi multiplicative), i.e. \(1\times 1=1\).
Soit \(n\) un entier naturel. Le produit cartésien \({\mathbf F}_2^n\) de \(n\) copies de l'ensemble \(\{0,1\}\) est muni d'une structure d'espace vectoriel à l'aide d'une loi de composition interne \(+\) et une loi de loi de composition externe \(\cdot\) définies par \begin{align} (x_1,x_2,\ldots,x_n)+(y_1,y_2,\ldots,y_n)&:=(x_1+y_1,x_2+y_2,\ldots,x_n+y_n)\\ \lambda\cdot(x_1,x_2,\ldots,x_n)&:=(\lambda\cdot x_1,\lambda\cdot x_2,\ldots,\lambda\cdot x_n)\\ \end{align} Aucune ambiguïté n'étant possible et afin de ne pas allourdir les notations, on utilise généralement les mêmes symboles \(+\) et \(\cdot\) pour désigner ces opérations sur les deux ensembles, \({\mathbf F}_2^n\) et \({\mathbf F}_2\). Un élément de \(({\mathbf F}_2^n,+,\cdot)\) est appelé vecteur binaire. Le corps \({\mathbf F}_2\) et le \({\mathbf F}_2\)-espace vectoriel \({\mathbf F}_2^n\) jouent un rôle absolument fondamental en informatique.
Vecteurs caractéristiques
Nous allons étudier à présent une autre structure de données très utilisée en pratique car extrêmement performante en terme de temps et d'espace. Soit \(E\) un ensemble. On définit la fonction caractéristique \({\boldsymbol 1}_A:E\rightarrow\{0,1\}\) d'une partie \(A\) de \(E\) par \begin{equation} {\boldsymbol 1}_A(x)= \begin{cases} 1,&\text{si}\ x\in A.\\ 0,&\text{sinon}. \end{cases} \end{equation} Supposons à présent que \(E\) soit un fini est notons \(n\) son cardinal. On peut donc numéroter ses éléments et écrire \(E=\{x_0,x_1,\ldots,x_{n-1}\}\) (cf. exercice). On définit une application \(\chi:{\mathcal P}(E)\rightarrow\{0,1\}^n\) par \begin{equation} \chi(A):=({\boldsymbol 1}_A(x_0),{\boldsymbol 1}_A(x_1),\ldots, {\boldsymbol 1}_A(x_{n-1})) \end{equation}
Prenons l'exemple d'un jeu de \(32\) cartes. Comment représenter une main, c'est-à-dire un sous-ensemble de cartes ? Il faut tout d'abord se fixer une énumération de l'ensemble \(E\) des \(32\) cartes, par exemple dans l'ordre des couleurs et des valeurs suivantes :
Notons que pour tester l'appartenance \(x\in A\), l'opération \((1\ll x)\wedge A\) renvoie un entier non-nul si \(x\in A\) et 0 sinon. Pour les langages de programmation, les opérateurs bit à bit agissant sur des registres vus comme des entiers, il est donc impossible de conserver les notations usuelles de l'algèbre de Boole puisque l'opérateur \(+\) par exemple est généralement dédié à l'addition sur les entiers et non pas au ou logique. Le langage \(C\) utilise les symboles ~A, A | B, A & B, A ^ B, A << s et A >> s respectivement à la table ci-dessus.
Soit \(N\) un entier naturel. Calculez l'entier \(N\wedge (N-1)\) et comparez son écriture binaire avec celle de \(N\). Que remarquez vous ? En déduire un nouvel algorithme de calcul du poids binaire. Calculez sa complexité.
On supposera qu'il y a 4 joueurs. Le gagnant est celui qui se sera débarrassé avant les autres des cartes de sa main. Initialement on distribue au hasard 5 cartes à chacun et les 12 cartes restantes sont placées en pile face cachée au milieu de la table. Celle pile constitue le lac.
À tour de rôle chaque joueur se débarrasse de toutes les paires qu'il peut constituer avec les cartes dans sa main en les déposant en pile dans son seau. S'il n'a pas ou plus de paire, il demande à un joueur de son choix (choisi au hasard par le programme) une carte qui lui permettrait de constituer une paire (choisie au hasard parmi les cartes qui restent dans sa main). Deux situations se présentent :
Estimez de manière expérimentale la durée moyenne d'une partie, l'unité de mesure étant le nombre d'actions réalisées, soit une paire déposée dans un seau ou une carte pêchée dans le lac. En numérotant les joueurs de 1 à 4 dans l'ordre de jeu, le joueur numéro 1 étant celui qui commence, calculez expérimentalement la probabilité \(p_i\) que le joueur numéro \(i\) gagne la partie.