Implémentation de Tableaux associatifs - Corrige
Table of Contents
1. Listes de couples
1.1. Recherche
def lc_get(t, c, defaut ) : for couple in t : if couple[0] == c : return couple[1] return defaut
La complexité de la fonction est linéaire en le nombre de clés.
1.2. Suppression
def lc_del(t, c ) : for i in range(len(t)) : if t[i][0] == c : t.pop(i) return True return False
La complexité de la fonction est linéaire en le nombre de clés. Au pire des cas
toutes les clés sont parcouru et la fonction pop
( $\mathtrm O(n) ) est
appelée au plus une fois.
1.3. Ajout et Modification
Implémenter une fonction lc_set
qui prend en entrée un tableau associatif,
une clé et une valeur et qui :
- modifie la valeur associée à la clé si la clé est déjà présente dans le tableau.
- ajoute le couple (clé,valeur) dans le tableau si la clé n’est pas présente.
Donnez la complexité de l’opération et prouvez sa correction.
def lc_set(t, c, v ) : for i in range(len(t)) : if t[i][0] == c : t[i] = (c,v) # Attention un tuple est immutable return False # Premier point de sortie t.append((c,v)) return True # Deuxième point de sortie
La complexité de la fonction est linéaire en le nombre de clés. Pour la correction :
- On rappel la pré-condition et post-condition :
- Pré-condition :
t
est un tableau associatif etc
appartient au domaine de clé. - Post-condition :
t
est toujours un tableau associatif et(c,v)
est danst
.
- Pré-condition :
- On note qu’il y a deux point de sortie de la fonction qui correspondent aux
deux cas à traiter :
- Cas 1.
c
est une clé det
. - Cas 2.
c
n’est pas une clé det
.
- Cas 1.
- Le cas d’une sortie dans la boucle correspond au cas 1. Si on sort de la
fonction par le premier point de sortie, la condition
t[i][0] =
c= est vérifiée etc
est danst
. L’exécution det[i] = (c,v)
garanti qu’à la sortie,(c,v)
est danst
.t
est toujours un tableau associatif car la cléc
apparaît toujours une seule fois danst
à l’indicei
. Le cas d’une sortie hors de la boucle correspond au cas 2. Intuitivement, si on sort de la boucle, on peut en conclure que la condition
t[i][0] =
c= est fausse pour touti
et quec
n’est pas dans le tableau associatif. On prouve ceci formellement grâce à l’invariant de boucle suivant :\[ \forall j < i, t[j][0] \neq c \]
En effet, l’invariant est vrai avant d’entrée dans la boucle. S’il est vrai au début du tour de boucle
i
:- Soit
t[i][0] =
c= et on met fin à la fonction - Soit
t[i][0] !
c=, on atteint la fin de la boucle et au début du touri+1
on a bien \(\forall j < i+1, t[j][0] \neq c\).
On en conclut que l’ajout de
(c,v)
danst
préserve la propriété d’unicité des clés et quet
reste un tableau associatif. La post-condition est donc vérifiée.- Soit
2. Fonction de hachage
L’ensemble des clés possibles seront les chaînes de caractère et les entiers.
On considère la fonction de hachage suivante : \[ \cal F(k) = \begin{cases} k & \text{ si } k \in \mathbb N \\ \sum_{i = 0}^{\mathtt{len}(k) - 1} \mathtt{ord}(k[i]) * 256^i & \text{ si } k \text{ est une chaîne } \end{cases} \]
2.1. Etude de \(\cal F\)
- On note \(\mathcal K\) l’ensemble de clés. \(\mathbb N = \mathcal F(\mathbb N) \subseteq \mathcal F(\mathcal K) \subseteq \mathbb N\)
On note \(\Sigma^*\) l’ensemble des chaînes de caractère et \(\Sigma^{50}\) l’ensemble des chaînes de caractères de tailles 50. On remarque que \(\mathcal F(s)\) convertir en base 10 le nombre écrit en base 256 obtenu en remplaçant chaque caractère par son code ASCII entre 0 et 255. Donc \(\mathcal F\) est injective sur \(\Sigma^{*}\) et surjective de \(\Sigma^{50}\) sur \(\{ 0 = \mathcal F(""), \dots, \mathcal F(\mathtt{chr}(255) * 50 ) \}\).
\(\mathcal F( \{ 0, \dots, 2^{64} \} \cup \Sigma^{50}) = \{ 0, \dots, \max(2^{64}, \sum_{i = 0}^{50 - 1} 255 * 256^i ) \}\). Or \(\sum_{i = 0}^{50 - 1} 255 * 256^i < 256^{50} = (2^{8})^{50} = 2^{200}\), donc \(\mathcal F(\{ 0, \dots, 2^{64} \} \cup \Sigma^{50}) = \{ 0, \dots, 2^{200} \}\).
- Le seul cas pour lequel il y a collision est pour une chaîne \(s\) et l’entier \(\mathcal F(s)\). La fonction de hachage n’est pas parfaite.
- L’ensemble des index générés par \(\mathcal F\) est infini, on ne peut pas l’utiliser comme fonction de hachage. Si on borne l’ensemble des clés possibles, le nombre de case à allouer dans le tableau reste très grand.
def hash(k) : if type(k) == int : return k sum = 0 for c in reversed(k) : sum = sum*256 + ord(c) return sum
- \(\mathrm O(1)\)
- \(\mathrm O(|k|)\)
2.2. Etude de \(\cal F_n\)
On considère l’ensemble des fonctions de hachages suivant : \[\forall n \in \mathbb N, \cal F_n(k) = \cal F(k) \, \% \, n \]
- \(\{ 0, n-1 \}\)
- \(\{ 0, n-1 \}\)
On note donc que \(\forall 0 \leq i < n\,A, \mathbb P(\mathcal F(u) = i) = \frac 1 {n\,A}\). On pose \(E_i = \{ i + q\, n, 0 \leq q \leq A-1\}\) pour tout \(0 \leq i \leq n-1\).
\begin{align*} \forall u, v \in \mathcal U, \, \mathbb P(\mathcal F_n(u) = \mathcal F_n(v)) & = \sum_{i = 0}^{n-1} \mathbb P(\mathcal F_n(u) = i \cap \mathcal F_n(v) = i ) \\ & = \sum_{i = 0}^{n-1} \mathbb P(\mathcal F_n(u) = i) \, \mathbb P(\mathcal F_n(v) = i | \mathcal F_n(u) = i ) \\ & = \sum_{i = 0}^{n-1} \mathbb P(\mathcal F_n(u) = i) \, \mathbb P(\mathcal F_n(v) = i) \\ & = \sum_{i = 0}^{n-1} \mathbb P(\mathcal F(u) \in E_i ) \, \mathbb P(\mathcal F(v) \in E_i) \\ & = \sum_{i = 0}^{n-1} \frac {|E_i|^2} {n^2\,A^2} \\ & = \frac {n \,A^2} {n^2 \, A^2} \\ & = \frac {1} {n} \\ \end{align*}- Oui, la fonction est rapide à calculer et le nombre d’index s’adapte pour les
différentes valeur de
n
. def hashn(k, n) : return hash(k) % n
- \(\mathrm O(1)\)
- \(\mathrm O(|k|)\)
3. Table de hachage avec résolution des collisions par chaînage
3.1. Création
import numpy as np def th_new() : T = np.empty(2,dtype=object) for i in range(len(T)) : T[i] = [] return [T,0,2]
3.2. Recherche
def th_get(assoc, cle, defaut) : T, m, n = assoc h = hashn(cle,n) return lc_get(T[h],cle,defaut)
La complexité de th_get
est la plus grande complexité entre le calcul de
l’index de la clé, \(\mathrm O(|cle|)\) et la plus grande taille de liste de
couple dans le tableau, \(\mathrm O(\max(|T[i]|, 0 \leq i \leq |T|))\). Au pire
des cas, c’est toutes les clés du tableau, \(\mathrm O(|T|)\).
3.3. Complexité de la Recherche en moyenne.
- \(\sum_{i = 0}^{n-1} k_i = m\)
- Si deux clés on la même image par \(\mathcal F_n\), i, alors la recherche de ces clé consiste en une recherche linéaire parmi les clés de l’alvéole \(i\) de la table. La complexité est en \(\mathrm O(k_i)\).
- D’après l’énoncé, \(p_i = \frac 1 n\) donc \(\mathcal C(\cal T) = \mathrm O (\frac m n)\).
- On note que \(\mathcal C(\cal T) = \frac m n\) est indépendant de \(\mathcal T\). Donc : \[\cal C(m) = \sum_{ \cal T \in \mathbb T_m} \frac 1 {|\mathbb T_m|} \cal C(\cal T) = \mathrm O (\frac m n \sum_{ \cal T \in \mathbb T_m} \frac 1 {|\mathbb T_m|}) = \mathrm O (\frac m n) \]
On note \(n_m\) le \(n\) utilisé pour une table remplie avec \(m\) éléments. On rappelle que le facteur de remplissage est égal à \(\frac m {n_m}\). Supposons qu’il existe \(\tau\) un réel tel que \(\forall m \in \mathbb N, \frac m {n_m} \leq \tau\) alors \(\cal C(m) = \mathrm O(\frac m {n_m}) = \mathrm O(\tau) = \mathrm O (1)\).
3.4. Suppression
def th_del(assoc, cle, defaut) : T, m, n = assoc h = hashn(cle,n) b_suppr = lc_del(T[h],cle) if b_suppr : assoc[1] -= 1 return b_suppr
3.5. Redimensionnement
def th_extend(assoc) : T, m, n = assoc # Création T_double = np.empty(2 * n,dtype=object) for i in range(len(T_double)) : T_double[i] = [] # Remplissage for i in range(len(T)) : for cle, val in T[i] : h = hashn(cle,2*n) T_double[h].append((cle,val)) # Modification de assoc assoc[0] = T_double assoc[2] = 2 * n return None
3.6. Ajout et Modification
def th_set(assoc, cle, val) : T, m, n = assoc h = hashn(cle,n) b_ajout = lc_set(T[h], cle, val) if b_ajout : assoc[1] += 1 if ( (m+1) / n ) > 0.5 : th_extend(assoc) return b_ajout