Révision Bac NSI Terminale

Tout le programme officiel — structures, algorithmes, BDD, réseaux, cryptographie

Structures de données SQL Algorithmique Réseaux Cryptographie Récursivité OS & Archi Python

Sommaire

  1. 1Structures de données
  2. 2Bases de données & SQL
  3. 3Architecture & OS
  4. 4Réseaux
  5. 5Algorithmique
  6. 6Langages & Prog.
  7. 7Cryptographie
  8. 8Récursivité
Chapitre 1
Structures de données

1.1 — Listes chaînées

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).

[val1 | •]——→[val2 | •]——→[val3 | ∅] tête queue
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érationComplexitéRemarque
Accès par indexO(n)Parcours séquentiel
Insertion en têteO(1)Mise à jour du pointeur
Suppression en têteO(1)
RechercheO(n)Parcours nécessaire

1.2 — Piles (Stack)

Une pile est une structure LIFO (Last In, First Out). On empile (push) et dépile (pop) uniquement par le sommet.

┌─────┐ ← sommet (push/pop ici) │ c │ │ b │ │ a │ └─────┘
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.

1.3 — Files (Queue)

Une file est une structure FIFO (First In, First Out). Enfilement par la queue, défilement par la tête.

entrée → [c][b][a] → sortie queue 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.

1.4 — Arbres binaires

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.

[8] ← racine / \ [3] [10] ← nœuds internes / \ \ [1] [6] [14] ← feuilles

Vocabulaire essentiel :

TermeDéfinition
RacineNœud sans parent
FeuilleNœud sans enfant
HauteurLongueur du plus long chemin racine→feuille
TailleNombre total de nœuds
Sous-arbreArbre enraciné en un nœud quelconque
Arbre completChaque nœud a 0 ou 2 fils
Arbre parfaitComplet + 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)

Parcours d'arbres binaires

# 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)⌋.

1.5 — Arbres Binaires de Recherche (ABR)

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.

[8] / \ [3] [10] Infixe → [1,3,4,6,7,8,10,13,14] (trié) / \ \ [1] [6] [14] / \ / [4][7][13]
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érationMoy. (équilibré)Pire cas (dégénéré)
RechercheO(log n)O(n)
InsertionO(log n)O(n)
SuppressionO(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).

1.6 — Graphes

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ésentationMémoireTester arête (u,v)Lister voisins de u
Matrice d'adjacenceO(n²)O(1)O(n)
Liste d'adjacenceO(n+a)O(deg(u))O(deg(u))

1.7 — Dictionnaires & Tables de hachage

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.

clé "Alice" → h("Alice") = 3 → tableau[3] = 25 clé "Bob" → h("Bob") = 7 → tableau[7] = 30

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.

Chapitre 2
Bases de données & SQL

2.1 — Modèle relationnel

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)

idnomageclasse
1Alice17Tle A
2Bob18Tle B
3Clara17Tle A

Contraintes d'intégrité

ContrainteDescription
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 NULLL'attribut ne peut pas être nul.
UNIQUEToutes les valeurs de l'attribut sont distinctes.
CHECKCondition booléenne sur la valeur.

2.2 — SQL : DDL (Définition)

-- 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;

2.3 — SQL : DML (Manipulation)

-- 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;

2.4 — SQL : SELECT (Requêtes)

-- 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;

Jointures

-- 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.

2.5 — Algèbre relationnelle

OpérationSymboleSQL équivalentDescription
Sélectionσcond(R)WHEREFiltre les lignes
Projectionπattr(R)SELECT attrGarde certaines colonnes
Jointure naturelleR ⋈ SJOIN ONJoint sur attributs communs
UnionR ∪ SUNIONRéunit deux relations
DifférenceR − SEXCEPTTuples dans R mais pas S
Produit cartésienR × SCROSS JOINToutes combinaisons
Renommageρnew(R)ASRenomme attribut/relation
Chapitre 3
Architecture matérielle & Systèmes d'exploitation

3.1 — Modèle de Von Neumann

Architecture des ordinateurs modernes : 4 composants reliés par des bus.

┌──────────────────────────────────────────────┐ │ CPU │ │ ┌──────────────┐ ┌──────────────────────┐ │ │ │ Unité de │ │ Unité Arithmétique │ │ │ │ Contrôle │ │ et Logique (UAL) │ │ │ │ (UC/CU) │ │ │ │ │ └──────────────┘ └──────────────────────┘ │ │ Registres: PC, IR, SP, accumulateurs │ └──────────────────┬───────────────────────────-┘ │ Bus (données, adresses, contrôle) ┌─────────┴───────────┐ │ │ ┌────┴────┐ ┌────┴────────┐ │ Mémoire │ │ Entrées / │ │ centrale│ │ Sorties │ │ (RAM) │ │ (I/O) │ └─────────┘ └─────────────┘
ComposantRô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
RAMMémoire volatile, accès rapide, stocke code et données
Bus de donnéesTransporte les données entre composants
Bus d'adressesIndique l'adresse mémoire ciblée

Cycle Fetch–Decode–Execute

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

3.2 — Hiérarchie mémoire

NiveauTypeVitesseTaille typiqueVolatile
Registres CPUSRAM1 cycle< 1 KoOui
Cache L1/L2/L3SRAM2–50 cyclesKo–MoOui
RAM (mémoire centrale)DRAM~100 cyclesGoOui
Stockage SSDFlashμsToNon
Stockage HDDMagnétiquemsToNon

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.

3.3 — Systèmes d'exploitation

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 :

  • Gestion des processus (création, ordonnancement, terminaison)
  • Gestion de la mémoire (allocation, pagination, mémoire virtuelle)
  • Gestion du système de fichiers
  • Gestion des entrées/sorties (drivers)
  • Sécurité et contrôle d'accès

Processus

Un processus est un programme en cours d'exécution. Il possède son propre espace mémoire, un PID (identifiant), et un état.

États d'un processus : [Nouveau] ──créer──→ [Prêt] ──élu──→ [En cours] ↑ │ [préempté] [bloqué] │ │ └──[I/O terminée]──┘ ↓ [Terminé]

Ordonnancement

AlgorithmePrincipeAvantageInconvénient
FCFSPremier arrivé, premier serviSimpleEffet convoi
SJFPlus court d'abordOptimal théoriqueFamine possible
Round-RobinQuantum de temps tournantÉquitableNombreux changements de contexte
PrioritéProcessus haute priorité en premierFlexibleFamine des basse priorité

Gestion des fichiers — commandes Unix

# 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
Chapitre 4
Réseaux

4.1 — Modèle OSI et TCP/IP

Couche OSIProtocoles clésRôle
Application7HTTP, HTTPS, DNS, FTP, SMTPServices réseau pour applications
Présentation6SSL/TLS, MIMEFormat, chiffrement
Session5NetBIOSGestion sessions
Transport4TCP, UDPFiabilité, ports
Réseau3IP, ICMP, OSPF, BGPAdressage, routage
Liaison2Ethernet, WiFi (802.11)Adresses MAC, trames
Physique1RJ45, fibre optiqueBits, 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é.

4.2 — Adressage IP

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

ClassePlageMasque par défautNb hôtes
A1.0.0.0 – 126.255.255.255/8~16 millions
B128.0.0.0 – 191.255.255.255/16~65 000
C192.0.0.0 – 223.255.255.255/24254

Adresses privées (RFC 1918) : 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Non routables sur Internet.

4.3 — TCP vs UDP

CaractéristiqueTCPUDP
ConnexionOrienté connexion (3-way handshake)Sans connexion
FiabilitéAccusés de réception, retransmissionAucune garantie
OrdreOrdre garantiNon garanti
VitessePlus lent (overhead)Plus rapide
UsageHTTP, HTTPS, FTP, SSHDNS, streaming, jeux

3-way handshake TCP :
1. Client → SYN → Serveur
2. Client ← SYN-ACK ← Serveur
3. Client → ACK → Serveur → connexion établie

4.4 — Protocole HTTP/HTTPS

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>
CodeSignification
200OK — succès
301Moved Permanently — redirection permanente
400Bad Request — requête malformée
403Forbidden — accès refusé
404Not Found — ressource introuvable
500Internal Server Error — erreur serveur

Méthodes HTTP

MéthodeActionIdempotent
GETRécupère une ressourceOui
POSTCrée/envoie donnéesNon
PUTRemplace une ressourceOui
DELETESupprime une ressourceOui
PATCHModifie partiellementNon

4.5 — DNS et routage

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)

Chapitre 5
Algorithmique

5.1 — Complexité

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.

NotationNomExemplen=10⁶
O(1)ConstantAccès tableau, hashInstantané
O(log n)LogarithmiqueRecherche dichotomique, ABR équil.20 ops
O(n)LinéaireParcours liste, recherche séquentielle10⁶ ops
O(n log n)Quasi-linéaireTri fusion, tri rapide (moy)2×10⁷ ops
O(n²)QuadratiqueTri bulles, tri insertion (pire)10¹² ops
O(2ⁿ)ExponentielleSous-ensembles, certains récursifs naïfsImpossible

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.

5.2 — Algorithmes de tri

Tri par insertion — O(n²)

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)

Tri fusion (Merge Sort) — O(n log 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)

Tri rapide (Quick Sort) — O(n log n) moy.

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
AlgorithmeMeilleurMoyenPireEn placeStable
InsertionO(n)O(n²)O(n²)OuiOui
SélectionO(n²)O(n²)O(n²)OuiNon
FusionO(n log n)O(n log n)O(n log n)NonOui
RapideO(n log n)O(n log n)O(n²)OuiNon

5.3 — Recherche dichotomique

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.

5.4 — Parcours de graphes

DFS — Parcours en Profondeur (Depth-First Search)

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|)

BFS — Parcours en Largeur (Breadth-First Search)

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|)
DFSBFS
Structure utiliséePile (ou récursion)File
Plus court cheminNon garantiOui (non pondéré)
Détection cycleOuiOui
Usage typiqueTri topologique, labyrinthePlus court chemin, niveaux
ComplexitéO(|S|+|A|)O(|S|+|A|)

5.5 — Algorithme de Dijkstra

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).

5.6 — Programmation dynamique

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.

5.7 — Diviser pour régner

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)

Chapitre 6
Langages & Programmation

6.1 — Paradigmes de programmation

ParadigmePrincipeExemples
ImpératifSuite d'instructions modifiant l'étatC, Python (style)
FonctionnelFonctions pures, pas d'état mutable, compositionHaskell, OCaml, Python (map/filter)
Objet (OOP)Objets encapsulant état + comportementsJava, Python, C++
DéclaratifDécrit le quoi, pas le commentSQL, HTML

Programmation fonctionnelle en Python

# 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

6.2 — Programmation orientée objet

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
ConceptDéfinition
EncapsulationRegrouper données + méthodes, cacher l'implémentation
HéritageUne classe hérite attributs/méthodes d'une classe parente
PolymorphismeMême méthode, comportements différents selon la classe
AbstractionInterface publique, détails cachés

6.3 — Mise au point, tests, assertions

# 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.

6.4 — Types construits Python

# 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
Chapitre 7
Cryptographie & Sécurité

7.1 — Principes généraux

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).

TypeCléVitesseExemplesUsage
Symétrique1 clé partagéeRapideAES, DESChiffrement de données volumineuses
AsymétriquePaire clé publique/privéeLentRSA, ECCÉchange de clé, signature
HachagePas de cléTrès rapideSHA-256, MD5Intégrité, mots de passe

7.2 — Chiffrement symétrique

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.

7.3 — Chiffrement asymétrique (RSA)

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.

Alice veut envoyer un message secret à Bob : 1. Bob publie sa clé publique (n, e) 2. Alice chiffre : C = M^e mod n 3. Alice envoie C à Bob 4. Bob déchiffre : M = C^d mod n (seul Bob connaît d) Pour signer (authenticité) : 1. Alice signe avec sa clé PRIVÉE : S = M^d_Alice mod n 2. Bob vérifie avec la clé PUBLIQUE d'Alice : M = S^e_Alice mod n

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).

7.4 — Fonctions de hachage

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
AlgorithmeTaille hashStatut
MD5128 bitsObsolète (collisions connues)
SHA-1160 bitsDéprécié
SHA-256256 bitsStandard actuel
SHA-3224–512 bitsModerne

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).

7.5 — HTTPS et TLS

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.

Chapitre 8
Récursivité

8.1 — Principe

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.

8.2 — Exemples classiques

# 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)

8.3 — Récursion sur les structures

# 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

8.4 — Preuve de terminaison et correction

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! ✓.