Tout le programme officiel — structures, algorithmes, BDD, réseaux, cryptographie
Une liste chaînée est une suite de nœuds où chaque nœud contient une valeur et un pointeur vers le nœud suivant. L'accès est séquentiel (pas d'index direct).
class Noeud:
def __init__(self, val, suivant=None):
self.val = val
self.suivant = suivant
class ListeChainee:
def __init__(self):
self.tete = None
def inserer_debut(self, val):
self.tete = Noeud(val, self.tete)
def supprimer_debut(self):
if self.tete is not None:
self.tete = self.tete.suivant
| Opération | Complexité | Remarque |
|---|---|---|
| Accès par index | O(n) | Parcours séquentiel |
| Insertion en tête | O(1) | Mise à jour du pointeur |
| Suppression en tête | O(1) | |
| Recherche | O(n) | Parcours nécessaire |
Une pile est une structure LIFO (Last In, First Out). On empile (push) et dépile (pop) uniquement par le sommet.
class Pile:
def __init__(self):
self._data = []
def empiler(self, val):
self._data.append(val) # O(1)
def depiler(self):
if not self.est_vide():
return self._data.pop() # O(1)
def sommet(self):
return self._data[-1]
def est_vide(self):
return len(self._data) == 0
Usages clés : évaluation d'expressions, parcours DFS itératif, gestion de la récursion (pile d'appels), vérification de parenthèses.
Une file est une structure FIFO (First In, First Out). Enfilement par la queue, défilement par la tête.
from collections import deque
class File:
def __init__(self):
self._data = deque()
def enfiler(self, val):
self._data.appendleft(val) # O(1)
def defiler(self):
if not self.est_vide():
return self._data.pop() # O(1)
def est_vide(self):
return len(self._data) == 0
Usages clés : parcours BFS, gestion de tâches, files d'attente OS, buffering réseau.
Un arbre binaire est une structure récursive : un nœud racine avec au plus deux fils (gauche et droit). Un arbre vide est noté None.
Vocabulaire essentiel :
| Terme | Définition |
|---|---|
| Racine | Nœud sans parent |
| Feuille | Nœud sans enfant |
| Hauteur | Longueur du plus long chemin racine→feuille |
| Taille | Nombre total de nœuds |
| Sous-arbre | Arbre enraciné en un nœud quelconque |
| Arbre complet | Chaque nœud a 0 ou 2 fils |
| Arbre parfait | Complet + toutes feuilles à même profondeur |
class Noeud:
def __init__(self, val, gauche=None, droit=None):
self.val = val
self.gauche = gauche
self.droit = droit
def hauteur(arbre):
if arbre is None:
return -1 # arbre vide : hauteur -1 (ou 0 selon convention)
return 1 + max(hauteur(arbre.gauche), hauteur(arbre.droit))
def taille(arbre):
if arbre is None:
return 0
return 1 + taille(arbre.gauche) + taille(arbre.droit)
# Préfixe (racine, G, D)
def prefixe(a):
if a is None: return
print(a.val)
prefixe(a.gauche)
prefixe(a.droit)
# Infixe (G, racine, D)
def infixe(a):
if a is None: return
infixe(a.gauche)
print(a.val)
infixe(a.droit)
# Suffixe / postfixe (G, D, racine)
def suffixe(a):
if a is None: return
suffixe(a.gauche)
suffixe(a.droit)
print(a.val)
# Largeur (BFS avec file)
from collections import deque
def largeur(a):
q = deque([a])
while q:
n = q.popleft()
if n: print(n.val)
if n.gauche: q.append(n.gauche)
if n.droit: q.append(n.droit)
Pour un arbre parfait de hauteur h : taille = 2h+1 − 1. Hauteur minimale d'un arbre à n nœuds : ⌊log₂(n)⌋.
Un ABR est un arbre binaire tel que pour tout nœud n : toutes les valeurs du sous-arbre gauche sont < n.val et toutes celles du droit sont > n.val.
def rechercher(arbre, val):
if arbre is None:
return False
if val == arbre.val:
return True
elif val < arbre.val:
return rechercher(arbre.gauche, val)
else:
return rechercher(arbre.droit, val)
def inserer(arbre, val):
if arbre is None:
return Noeud(val)
if val < arbre.val:
arbre.gauche = inserer(arbre.gauche, val)
elif val > arbre.val:
arbre.droit = inserer(arbre.droit, val)
return arbre
| Opération | Moy. (équilibré) | Pire cas (dégénéré) |
|---|---|---|
| Recherche | O(log n) | O(n) |
| Insertion | O(log n) | O(n) |
| Suppression | O(log n) | O(n) |
ABR dégénéré (insertions triées) → liste chaînée → O(n). Solution : arbres AVL ou rouge-noir (hors programme mais à connaître comme concept).
Un graphe G = (S, A) est un ensemble de sommets S et d'arêtes A ⊆ S×S. Orienté si les arêtes ont un sens, non-orienté sinon. Pondéré si chaque arête a un poids.
Deux représentations principales :
Matrice d'adjacence
# M[i][j] = 1 si arête i→j
M = [
[0, 1, 1, 0], # 0→1, 0→2
[0, 0, 1, 0], # 1→2
[0, 0, 0, 1], # 2→3
[0, 0, 0, 0],
]
→ O(n²) mémoire. Bon si graphe dense.
Liste d'adjacence
# adj[sommet] = liste voisins
adj = {
0: [1, 2],
1: [2],
2: [3],
3: [],
}
→ O(n+a) mémoire. Bon si graphe creux.
| Représentation | Mémoire | Tester arête (u,v) | Lister voisins de u |
|---|---|---|---|
| Matrice d'adjacence | O(n²) | O(1) | O(n) |
| Liste d'adjacence | O(n+a) | O(deg(u)) | O(deg(u)) |
Un dictionnaire associe des clés à des valeurs. Implémenté par une table de hachage : une fonction de hachage h(clé) mappe chaque clé vers un indice du tableau.
Complexité amortie en moyenne : O(1) pour insertion, recherche, suppression. Pire cas O(n) en cas de trop de collisions.
# En Python, dict est une table de hachage
d = {}
d["Alice"] = 25 # insertion O(1) moyen
d["Bob"] = 30
print(d["Alice"]) # accès O(1) moyen
"Alice" in d # test appartenance O(1) moyen
Collision : deux clés donnent le même hash. Résolution par chaînage (liste à chaque case) ou adressage ouvert. En Python, gérée automatiquement.
Une relation (table) est un ensemble de n-uplets (lignes/tuples) partageant les mêmes attributs (colonnes). Le schéma d'une relation décrit ses attributs et leurs domaines.
Relation Eleve(id, nom, age, classe)
| id | nom | age | classe |
|---|---|---|---|
| 1 | Alice | 17 | Tle A |
| 2 | Bob | 18 | Tle B |
| 3 | Clara | 17 | Tle A |
| Contrainte | Description |
|---|---|
| Clé primaire (PRIMARY KEY) | Identifie de façon unique chaque tuple. Non nulle, unique. |
| Clé étrangère (FOREIGN KEY) | Référence la clé primaire d'une autre relation. Assure l'intégrité référentielle. |
| NOT NULL | L'attribut ne peut pas être nul. |
| UNIQUE | Toutes les valeurs de l'attribut sont distinctes. |
| CHECK | Condition booléenne sur la valeur. |
-- Création de table
CREATE TABLE Eleve (
id INTEGER PRIMARY KEY,
nom TEXT NOT NULL,
age INTEGER CHECK(age >= 0),
classe TEXT
);
CREATE TABLE Note (
id INTEGER PRIMARY KEY,
eleve_id INTEGER NOT NULL,
matiere TEXT,
note REAL,
FOREIGN KEY (eleve_id) REFERENCES Eleve(id)
);
DROP TABLE Eleve; -- supprime la table
ALTER TABLE Eleve ADD COLUMN email TEXT;
-- Insertion
INSERT INTO Eleve (id, nom, age, classe)
VALUES (1, 'Alice', 17, 'Tle A');
-- Mise à jour
UPDATE Eleve SET age = 18 WHERE id = 1;
-- Suppression
DELETE FROM Eleve WHERE age < 15;
-- Structure générale
SELECT attributs
FROM table(s)
[WHERE condition]
[GROUP BY attribut]
[HAVING condition_agrégat]
[ORDER BY attribut [ASC|DESC]]
[LIMIT n];
-- Exemples
SELECT * FROM Eleve;
SELECT nom, age FROM Eleve WHERE classe = 'Tle A';
SELECT nom FROM Eleve WHERE age >= 17 ORDER BY nom ASC;
-- Fonctions d'agrégat
SELECT COUNT(*) FROM Eleve;
SELECT AVG(note), MAX(note), MIN(note) FROM Note;
-- GROUP BY
SELECT classe, COUNT(*) AS effectif
FROM Eleve
GROUP BY classe
HAVING COUNT(*) > 10;
-- INNER JOIN : tuples ayant correspondance des deux côtés
SELECT Eleve.nom, Note.matiere, Note.note
FROM Eleve
INNER JOIN Note ON Eleve.id = Note.eleve_id;
-- LEFT JOIN : tous les élèves, même sans notes
SELECT Eleve.nom, Note.note
FROM Eleve
LEFT JOIN Note ON Eleve.id = Note.eleve_id;
-- Jointure avec filtre
SELECT E.nom, AVG(N.note) AS moyenne
FROM Eleve E
JOIN Note N ON E.id = N.eleve_id
WHERE E.classe = 'Tle A'
GROUP BY E.nom
ORDER BY moyenne DESC;
Ordre d'exécution SQL : FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. On ne peut pas utiliser un alias de SELECT dans WHERE.
| Opération | Symbole | SQL équivalent | Description |
|---|---|---|---|
| Sélection | σcond(R) | WHERE | Filtre les lignes |
| Projection | πattr(R) | SELECT attr | Garde certaines colonnes |
| Jointure naturelle | R ⋈ S | JOIN ON | Joint sur attributs communs |
| Union | R ∪ S | UNION | Réunit deux relations |
| Différence | R − S | EXCEPT | Tuples dans R mais pas S |
| Produit cartésien | R × S | CROSS JOIN | Toutes combinaisons |
| Renommage | ρnew(R) | AS | Renomme attribut/relation |
Architecture des ordinateurs modernes : 4 composants reliés par des bus.
| Composant | Rôle |
|---|---|
| UC (Unité de Contrôle) | Décode et coordonne l'exécution des instructions |
| UAL (Unité Arithmétique et Logique) | Effectue les calculs (add, sub, AND, OR, comparaisons...) |
| PC (Program Counter) | Registre contenant l'adresse de la prochaine instruction |
| IR (Instruction Register) | Contient l'instruction en cours d'exécution |
| RAM | Mémoire volatile, accès rapide, stocke code et données |
| Bus de données | Transporte les données entre composants |
| Bus d'adresses | Indique l'adresse mémoire ciblée |
1. Fetch : charger l'instruction à l'adresse PC → IR, incrémenter PC
2. Decode : l'UC décode l'instruction dans IR
3. Execute : l'UAL exécute l'opération
→ Répéter indéfiniment
| Niveau | Type | Vitesse | Taille typique | Volatile |
|---|---|---|---|---|
| Registres CPU | SRAM | 1 cycle | < 1 Ko | Oui |
| Cache L1/L2/L3 | SRAM | 2–50 cycles | Ko–Mo | Oui |
| RAM (mémoire centrale) | DRAM | ~100 cycles | Go | Oui |
| Stockage SSD | Flash | μs | To | Non |
| Stockage HDD | Magnétique | ms | To | Non |
Plus un niveau est proche du CPU, plus il est rapide et petit. Le principe de localité (spatiale et temporelle) justifie l'efficacité des caches.
Un système d'exploitation (OS) est un logiciel qui gère les ressources matérielles et offre des services aux programmes via des appels système.
Fonctions principales :
Un processus est un programme en cours d'exécution. Il possède son propre espace mémoire, un PID (identifiant), et un état.
| Algorithme | Principe | Avantage | Inconvénient |
|---|---|---|---|
| FCFS | Premier arrivé, premier servi | Simple | Effet convoi |
| SJF | Plus court d'abord | Optimal théorique | Famine possible |
| Round-Robin | Quantum de temps tournant | Équitable | Nombreux changements de contexte |
| Priorité | Processus haute priorité en premier | Flexible | Famine des basse priorité |
# Navigation
pwd # répertoire courant
ls -la # liste fichiers avec droits
cd /chemin/vers/dossier
# Manipulation
cp source dest # copie
mv source dest # déplace/renomme
rm fichier # supprime fichier
mkdir dossier # crée dossier
cat fichier # affiche contenu
grep "motif" fichier # recherche dans fichier
# Droits (rwx = lecture/écriture/exécution)
chmod 755 fichier # rwxr-xr-x
chmod u+x fichier # ajoute exécution pour propriétaire
# Processus
ps aux # liste processus
kill -9 PID # termine processus
top # moniteur en temps réel
| Couche OSI | N° | Protocoles clés | Rôle |
|---|---|---|---|
| Application | 7 | HTTP, HTTPS, DNS, FTP, SMTP | Services réseau pour applications |
| Présentation | 6 | SSL/TLS, MIME | Format, chiffrement |
| Session | 5 | NetBIOS | Gestion sessions |
| Transport | 4 | TCP, UDP | Fiabilité, ports |
| Réseau | 3 | IP, ICMP, OSPF, BGP | Adressage, routage |
| Liaison | 2 | Ethernet, WiFi (802.11) | Adresses MAC, trames |
| Physique | 1 | RJ45, fibre optique | Bits, signaux électriques |
Modèle TCP/IP (4 couches) : Application | Transport | Internet (IP) | Accès réseau. C'est ce modèle qui est réellement utilisé.
Une adresse IP v4 est codée sur 32 bits, notée en 4 octets décimaux (ex: 192.168.1.1). Un masque de sous-réseau délimite la partie réseau et la partie hôte.
IP : 192.168.1.42 → en binaire : 11000000.10101000.00000001.00101010
Masque : 255.255.255.0 → /24 → 24 bits réseau, 8 bits hôte
Réseau : 192.168.1.0/24 → hôtes de .1 à .254
Broadcast : 192.168.1.255
| Classe | Plage | Masque par défaut | Nb hôtes |
|---|---|---|---|
| A | 1.0.0.0 – 126.255.255.255 | /8 | ~16 millions |
| B | 128.0.0.0 – 191.255.255.255 | /16 | ~65 000 |
| C | 192.0.0.0 – 223.255.255.255 | /24 | 254 |
Adresses privées (RFC 1918) : 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Non routables sur Internet.
| Caractéristique | TCP | UDP |
|---|---|---|
| Connexion | Orienté connexion (3-way handshake) | Sans connexion |
| Fiabilité | Accusés de réception, retransmission | Aucune garantie |
| Ordre | Ordre garanti | Non garanti |
| Vitesse | Plus lent (overhead) | Plus rapide |
| Usage | HTTP, HTTPS, FTP, SSH | DNS, streaming, jeux |
3-way handshake TCP :
1. Client → SYN → Serveur
2. Client ← SYN-ACK ← Serveur
3. Client → ACK → Serveur → connexion établie
HTTP (HyperText Transfer Protocol) est un protocole client-serveur sans état, couche application, port 80. HTTPS = HTTP + TLS (chiffrement), port 443.
# Requête HTTP GET
GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: text/html
# Réponse HTTP
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1234
<html>...</html>
| Code | Signification |
|---|---|
| 200 | OK — succès |
| 301 | Moved Permanently — redirection permanente |
| 400 | Bad Request — requête malformée |
| 403 | Forbidden — accès refusé |
| 404 | Not Found — ressource introuvable |
| 500 | Internal Server Error — erreur serveur |
| Méthode | Action | Idempotent |
|---|---|---|
| GET | Récupère une ressource | Oui |
| POST | Crée/envoie données | Non |
| PUT | Remplace une ressource | Oui |
| DELETE | Supprime une ressource | Oui |
| PATCH | Modifie partiellement | Non |
DNS (Domain Name System) traduit les noms de domaine en adresses IP. Hiérarchique : résolveur → serveur racine → TLD (.fr, .com) → serveur autoritaire.
Le routage détermine le chemin que prend un paquet IP. Un routeur consulte sa table de routage pour forwarder le paquet vers le prochain saut (next hop).
Algorithme de routage :
Pour chaque paquet reçu :
1. Lire l'adresse IP destination
2. Chercher dans la table de routage la route la plus spécifique (masque le plus long)
3. Forwarder vers l'interface/next-hop correspondant
4. Si aucune route → DROP (ou route par défaut 0.0.0.0/0)
La complexité temporelle mesure l'évolution du temps d'exécution en fonction de la taille n de l'entrée, en notation grand O. On ne garde que le terme dominant, sans constante.
| Notation | Nom | Exemple | n=10⁶ |
|---|---|---|---|
| O(1) | Constant | Accès tableau, hash | Instantané |
| O(log n) | Logarithmique | Recherche dichotomique, ABR équil. | 20 ops |
| O(n) | Linéaire | Parcours liste, recherche séquentielle | 10⁶ ops |
| O(n log n) | Quasi-linéaire | Tri fusion, tri rapide (moy) | 2×10⁷ ops |
| O(n²) | Quadratique | Tri bulles, tri insertion (pire) | 10¹² ops |
| O(2ⁿ) | Exponentielle | Sous-ensembles, certains récursifs naïfs | Impossible |
Pour calculer la complexité : compter les boucles imbriquées (chacune × n → O(n^k)), vérifier si chaque itération divise le problème (→ O(log n)).
Distinguer pire cas (worst case), cas moyen (average) et meilleur cas. Le grand O désigne généralement le pire cas.
def tri_insertion(t):
for i in range(1, len(t)):
cle = t[i]
j = i - 1
while j >= 0 and t[j] > cle:
t[j + 1] = t[j]
j -= 1
t[j + 1] = cle
return t
# Stable | En place | Pire: O(n²) | Meilleur: O(n)
def fusionner(g, d):
res, i, j = [], 0, 0
while i < len(g) and j < len(d):
if g[i] <= d[j]:
res.append(g[i]); i += 1
else:
res.append(d[j]); j += 1
return res + g[i:] + d[j:]
def tri_fusion(t):
if len(t) <= 1:
return t
m = len(t) // 2
return fusionner(tri_fusion(t[:m]), tri_fusion(t[m:]))
# Stable | NON en place | Toujours O(n log n)
def tri_rapide(t):
if len(t) <= 1:
return t
pivot = t[len(t) // 2]
gauche = [x for x in t if x < pivot]
milieu = [x for x in t if x == pivot]
droite = [x for x in t if x > pivot]
return tri_rapide(gauche) + milieu + tri_rapide(droite)
# Non stable | Pire cas O(n²) si pivot mal choisi
| Algorithme | Meilleur | Moyen | Pire | En place | Stable |
|---|---|---|---|---|---|
| Insertion | O(n) | O(n²) | O(n²) | Oui | Oui |
| Sélection | O(n²) | O(n²) | O(n²) | Oui | Non |
| Fusion | O(n log n) | O(n log n) | O(n log n) | Non | Oui |
| Rapide | O(n log n) | O(n log n) | O(n²) | Oui | Non |
Recherche dans un tableau trié en divisant à chaque étape l'espace de recherche par 2. Complexité : O(log n).
def dichotomie(t, cible):
g, d = 0, len(t) - 1
while g <= d:
m = (g + d) // 2
if t[m] == cible:
return m # trouvé à l'indice m
elif t[m] < cible:
g = m + 1 # chercher à droite
else:
d = m - 1 # chercher à gauche
return -1 # non trouvé
Précondition absolue : le tableau doit être trié. Sur tableau non trié → résultat incorrect, pas d'erreur.
def dfs(graphe, debut):
visites = set()
pile = [debut]
while pile:
sommet = pile.pop()
if sommet not in visites:
visites.add(sommet)
print(sommet)
for voisin in graphe[sommet]:
if voisin not in visites:
pile.append(voisin)
return visites # O(|S| + |A|)
from collections import deque
def bfs(graphe, debut):
visites = {debut}
file = deque([debut])
while file:
sommet = file.popleft()
print(sommet)
for voisin in graphe[sommet]:
if voisin not in visites:
visites.add(voisin)
file.append(voisin)
return visites # O(|S| + |A|)
| DFS | BFS | |
|---|---|---|
| Structure utilisée | Pile (ou récursion) | File |
| Plus court chemin | Non garanti | Oui (non pondéré) |
| Détection cycle | Oui | Oui |
| Usage typique | Tri topologique, labyrinthe | Plus court chemin, niveaux |
| Complexité | O(|S|+|A|) | O(|S|+|A|) |
Trouve le plus court chemin depuis un sommet source vers tous les autres dans un graphe à poids positifs.
import heapq
def dijkstra(graphe, source):
# graphe[s] = [(voisin, poids), ...]
dist = {s: float('inf') for s in graphe}
dist[source] = 0
tas = [(0, source)] # (distance, sommet)
while tas:
d, u = heapq.heappop(tas)
if d > dist[u]:
continue # sommet déjà traité avec meilleure dist
for v, poids in graphe[u]:
if dist[u] + poids < dist[v]:
dist[v] = dist[u] + poids
heapq.heappush(tas, (dist[v], v))
return dist # O((|S|+|A|) log |S|)
Dijkstra ne fonctionne pas avec des poids négatifs. Pour les poids négatifs : algorithme de Bellman-Ford (hors programme).
Technique d'optimisation qui résout un problème en décomposant en sous-problèmes qui se chevauchent, en mémorisant les résultats intermédiaires (mémoïsation ou tableau bottom-up).
Exemple : Fibonacci avec mémoïsation
# Naïf : O(2^n)
def fib_naif(n):
if n <= 1: return n
return fib_naif(n-1) + fib_naif(n-2)
# Mémoïsation top-down : O(n)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memo(n):
if n <= 1: return n
return fib_memo(n-1) + fib_memo(n-2)
# Bottom-up : O(n) temps, O(1) espace
def fib_dp(n):
if n <= 1: return n
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, a + b
return b
Conditions pour la prog. dynamique : (1) sous-structure optimale — la solution optimale du problème contient des solutions optimales des sous-problèmes ; (2) chevauchement des sous-problèmes.
Paradigme algorithmique : Diviser le problème, Régner (résoudre récursivement), Combiner les solutions. Ex : tri fusion, recherche dichotomique.
Théorème maître (cas courants) pour T(n) = a·T(n/b) + f(n) :
Si f(n) = O(nlog_b(a)-ε) → T(n) = Θ(nlog_b(a))
Si f(n) = Θ(nlog_b(a)) → T(n) = Θ(nlog_b(a) log n)
Tri fusion : T(n) = 2T(n/2) + O(n) → T(n) = O(n log n)
| Paradigme | Principe | Exemples |
|---|---|---|
| Impératif | Suite d'instructions modifiant l'état | C, Python (style) |
| Fonctionnel | Fonctions pures, pas d'état mutable, composition | Haskell, OCaml, Python (map/filter) |
| Objet (OOP) | Objets encapsulant état + comportements | Java, Python, C++ |
| Déclaratif | Décrit le quoi, pas le comment | SQL, HTML |
# Fonctions de première classe
def appliquer(f, x):
return f(x)
# Lambda
carre = lambda x: x ** 2
# map, filter, reduce
carres = list(map(lambda x: x**2, [1,2,3,4])) # [1,4,9,16]
pairs = list(filter(lambda x: x%2==0, [1,2,3,4])) # [2,4]
from functools import reduce
somme = reduce(lambda a,b: a+b, [1,2,3,4]) # 10
class Animal:
# Attribut de classe (partagé)
categorie = "Vertébré"
def __init__(self, nom, age):
# Attributs d'instance
self.nom = nom
self.age = age
def parler(self):
raise NotImplementedError
def __str__(self):
return f"{self.nom} ({self.age} ans)"
class Chien(Animal): # Héritage
def parler(self):
return "Woof!"
def __init__(self, nom, age, race):
super().__init__(nom, age) # appel constructeur parent
self.race = race
rex = Chien("Rex", 3, "Labrador")
print(rex) # Rex (3 ans)
print(rex.parler()) # Woof!
isinstance(rex, Animal) # True
| Concept | Définition |
|---|---|
| Encapsulation | Regrouper données + méthodes, cacher l'implémentation |
| Héritage | Une classe hérite attributs/méthodes d'une classe parente |
| Polymorphisme | Même méthode, comportements différents selon la classe |
| Abstraction | Interface publique, détails cachés |
# Assertions
def division(a, b):
assert b != 0, "Division par zéro !"
return a / b
# Tests unitaires avec unittest
import unittest
class TestDivision(unittest.TestCase):
def test_normale(self):
self.assertEqual(division(10, 2), 5)
def test_zero(self):
with self.assertRaises(AssertionError):
division(1, 0)
if __name__ == '__main__':
unittest.main()
Types de tests : unitaires (une fonction isolée), intégration (plusieurs modules), fonctionnels (comportement global). Le débogage s'aide de print, assert, breakpoints, et lecture de tracebacks.
# Tuple (immuable, ordonné)
point = (3, 4)
x, y = point # déstructuration
# Liste (mutable, ordonnée)
t = [1, 2, 3]
t.append(4)
t.sort()
[x**2 for x in t if x > 1] # compréhension de liste
# Dictionnaire (mutable, clé→valeur)
d = {"a": 1, "b": 2}
d["c"] = 3
d.get("z", 0) # valeur par défaut si absent
d.items() # paires (clé, valeur)
# Ensemble (mutable, non ordonné, sans doublon)
s = {1, 2, 3}
s.add(4)
s1 & s2 # intersection
s1 | s2 # union
s1 - s2 # différence
La cryptographie protège la confidentialité (seul le destinataire peut lire), l'intégrité (message non modifié) et l'authenticité (l'expéditeur est bien celui qu'il prétend).
| Type | Clé | Vitesse | Exemples | Usage |
|---|---|---|---|---|
| Symétrique | 1 clé partagée | Rapide | AES, DES | Chiffrement de données volumineuses |
| Asymétrique | Paire clé publique/privée | Lent | RSA, ECC | Échange de clé, signature |
| Hachage | Pas de clé | Très rapide | SHA-256, MD5 | Intégrité, mots de passe |
L'expéditeur et le destinataire partagent la même clé secrète. Problème : comment transmettre la clé de façon sécurisée ?
Chiffrement XOR (exemple pédagogique)
def chiffrer_xor(message, cle):
# Répéter la clé si plus courte que le message
return bytes(m ^ cle[i % len(cle)]
for i, m in enumerate(message.encode()))
message = "NSI"
cle = b'\x42' # clé d'un octet
chiffre = chiffrer_xor(message, cle)
dechiffre = chiffrer_xor(chiffre.decode('latin1'), cle)
# XOR est son propre inverse : chiffrer deux fois = original
AES (Advanced Encryption Standard) est le standard actuel. Clés de 128, 192 ou 256 bits. Utilisé dans HTTPS, WiFi WPA2, chiffrement de disque.
Chaque utilisateur a une clé publique (diffusée à tous) et une clé privée (secrète). Ce qui est chiffré avec la clé publique ne peut être déchiffré qu'avec la clé privée, et inversement.
Génération clés RSA :
1. Choisir deux grands premiers p et q
2. n = p × q, φ(n) = (p−1)(q−1)
3. Choisir e tel que pgcd(e, φ(n)) = 1 (souvent e = 65537)
4. Calculer d tel que e × d ≡ 1 mod φ(n) (inverse modulaire)
5. Clé publique : (n, e) | Clé privée : (n, d)
La sécurité RSA repose sur la difficulté de factoriser de grands entiers. Si on peut factoriser n = p×q, on casse le chiffrement. Un ordinateur quantique (algorithme de Shor) pourrait factoriser en O(log³n).
Une fonction de hachage transforme un message de taille arbitraire en une empreinte (hash) de taille fixe. Elle doit être : déterministe, rapide, à sens unique (non inversible), résistante aux collisions.
import hashlib
h = hashlib.sha256("mot de passe".encode())
print(h.hexdigest())
# a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
# Vérification d'intégrité
def verifier(fichier, hash_attendu):
with open(fichier, "rb") as f:
h = hashlib.sha256(f.read()).hexdigest()
return h == hash_attendu
| Algorithme | Taille hash | Statut |
|---|---|---|
| MD5 | 128 bits | Obsolète (collisions connues) |
| SHA-1 | 160 bits | Déprécié |
| SHA-256 | 256 bits | Standard actuel |
| SHA-3 | 224–512 bits | Moderne |
Mots de passe : ne jamais stocker en clair. Utiliser bcrypt ou PBKDF2 (hachage lent + sel) plutôt que SHA-256 simple (trop rapide → brute force).
TLS (Transport Layer Security) combine cryptographie asymétrique (échange de clé) et symétrique (données) pour sécuriser les communications. HTTPS = HTTP + TLS.
Handshake TLS simplifié :
1. Client → ClientHello (versions TLS supportées, chiffrements)
2. Serveur → ServerHello + certificat (clé publique + signé par CA)
3. Client vérifie le certificat (autorité de certification)
4. Échange d'une clé de session symétrique (Diffie-Hellman ou RSA)
5. Communication chiffrée symétriquement (AES)
Un certificat numérique lie une clé publique à une identité, signé par une autorité de certification (CA) de confiance. Protège contre les attaques man-in-the-middle.
Une fonction est récursive si elle s'appelle elle-même. Toute définition récursive comporte un (ou plusieurs) cas de base (arrêt) et un (ou plusieurs) cas récursifs (réduction vers le cas de base).
# Factorielle
def fact(n):
if n == 0: # cas de base
return 1
return n * fact(n - 1) # cas récursif
# Pile d'appels pour fact(3) :
# fact(3) → 3 * fact(2)
# → 2 * fact(1)
# → 1 * fact(0)
# → 1
Sans cas de base (ou non atteignable) → récursion infinie → RecursionError: maximum recursion depth exceeded. Python limite à ~1000 appels par défaut.
# Somme d'une liste
def somme(t):
if t == []: return 0
return t[0] + somme(t[1:])
# Exponentiation rapide O(log n)
def puissance(x, n):
if n == 0: return 1
if n % 2 == 0:
moitie = puissance(x, n // 2)
return moitie * moitie
return x * puissance(x, n - 1)
# Miroir d'une chaîne
def miroir(s):
if len(s) <= 1: return s
return miroir(s[1:]) + s[0]
# Hanoi (n disques : 2^n - 1 mouvements)
def hanoi(n, source, cible, intermediaire):
if n == 1:
print(f"Déplacer disque 1 de {source} vers {cible}")
return
hanoi(n-1, source, intermediaire, cible)
print(f"Déplacer disque {n} de {source} vers {cible}")
hanoi(n-1, intermediaire, cible, source)
# Aplatir une liste de listes
def aplatir(lst):
if not isinstance(lst, list):
return [lst]
if lst == []: return []
return aplatir(lst[0]) + aplatir(lst[1:])
# Parcours infixe ABR récursif
def infixe(arbre):
if arbre is None: return []
return (infixe(arbre.gauche)
+ [arbre.val]
+ infixe(arbre.droit))
# DFS récursif sur graphe
def dfs_rec(graphe, sommet, visites=None):
if visites is None: visites = set()
visites.add(sommet)
for voisin in graphe[sommet]:
if voisin not in visites:
dfs_rec(graphe, voisin, visites)
return visites
Terminaison : montrer qu'il existe un variant (quantité entière, positive, strictement décroissante à chaque appel). Ex : pour fact(n), le variant est n, qui décroît de 1 à chaque appel jusqu'à 0.
Correction par récurrence : poser l'invariant, le vérifier au cas de base, puis montrer que si la propriété est vraie au rang k, elle l'est au rang k+1 (en supposant les appels récursifs corrects — hypothèse de récurrence).
Preuve de fact(n) :
Variant : n (entier ≥ 0, décroît de 1 à chaque appel → terminaison).
Invariant P(n) : "fact(n) retourne n!". Base : P(0) : fact(0) = 1 = 0! ✓.
Hérédité : si P(n−1) est vrai (fact(n−1) = (n−1)!), alors fact(n) = n × fact(n−1) = n × (n−1)! = n! ✓.