💻

NSI - Bac 2023

Métropole - Session normale

Epreuve du 15 juin 2023

Duree : 3h30
3 questions
Coef. 16

Consigne officielle

Le candidat traite les trois exercices.

exercice

Exercice 1

6 points

Enonce

Exercice 1 — Structures de données (6 points)

[Contexte] On considère une structure de données représentant un arbre binaire de recherche (ABR) stockant des entiers. Chaque nœud est représenté par un dictionnaire avec les clés 'valeur', 'gauche' et 'droite'. L'arbre vide est représenté par None.

# Code Python à analyser/compléter
class ABR:
    def __init__(self, valeur):
        self.valeur = valeur
        self.gauche = None
        self.droite = None
    
    def inserer(self, valeur):
        """Insère la valeur dans l'ABR"""
        if valeur < self.valeur:
            if self.gauche is None:
                self.gauche = ABR(valeur)
            else:
                self.gauche.inserer(valeur)
        elif valeur > self.valeur:
            if self.droite is None:
                self.droite = ABR(valeur)
            else:
                self.droite.inserer(valeur)
    
    def recherche(self, valeur):
        """Recherche la valeur dans l'ABR"""
        if self is None:
            return False
        if valeur == self.valeur:
            return True
        elif valeur < self.valeur:
            if self.gauche is None:
                return False
            else:
                return self.gauche.recherche(valeur)
        else:
            if self.droite is None:
                return False
            else:
                return self.droite.recherche(valeur)
    
    def parcours_infixe(self):
        """Retourne la liste des valeurs dans l'ordre croissant"""
        resultat = []
        if self.gauche is not None:
            resultat.extend(self.gauche.parcours_infixe())
        resultat.append(self.valeur)
        if self.droite is not None:
            resultat.extend(self.droite.parcours_infixe())
        return resultat

# Exemple d'utilisation
arbre = ABR(10)
arbre.inserer(5)
arbre.inserer(15)
arbre.inserer(3)
arbre.inserer(7)
print(arbre.parcours_infixe())  # Affiche [3, 5, 7, 10, 15]
  1. La méthode recherche contient une erreur. Expliquez pourquoi la condition if self is None: à la ligne 25 ne fonctionnera jamais comme prévu, et proposez une correction.

  2. Quelle est la complexité temporelle (dans le pire des cas) des méthodes inserer et recherche dans cet ABR ? Justifiez votre réponse.

  3. Complétez la classe ABR en ajoutant une méthode hauteur(self) qui retourne la hauteur de l'arbre (la hauteur d'un arbre vide est 0, la hauteur d'un arbre avec un seul nœud est 1).

Notions :Structures de donnéesPythonArbres binaires de rechercheComplexité algorithmique
Difficulte : moyen
Mode examen

Methode

Pour résoudre cet exercice sur les arbres binaires de recherche (ABR), il faut adopter une démarche méthodique. D'abord, analyser attentivement le code fourni pour comprendre la structure de données et le fonctionnement des méthodes. Pour la question 1, tester mentalement l'exécution de la méthode 'recherche' avec différents cas (arbre vide, valeur présente, valeur absente) pour identifier le problème logique. Pour la question 2 sur la complexité, rappeler la définition du pire des cas pour un ABR (arbre dégénéré en liste chaînée) et analyser le nombre d'opérations en fonction du nombre n de nœuds. Pour la question 3, définir récursivement la hauteur : c'est 1 + le maximum des hauteurs des sous-arbres gauche et droit. Il est crucial de gérer le cas de base (nœud feuille) où les sous-arbres sont None. Penser à écrire un code propre, lisible, et à le tester avec des exemples simples (arbre vide via un appel sur None, arbre à un nœud, arbre déséquilibré).

Points cles

  • 1Dans un ABR, pour tout nœud, toutes les valeurs du sous-arbre gauche sont strictement inférieures à sa valeur, et toutes les valeurs du sous-arbre droit sont strictement supérieures. Cette propriété permet une recherche et une insertion efficaces par comparaisons successives.
  • 2La méthode 'recherche' est appelée sur un objet ABR existant (self). La condition 'if self is None:' est donc toujours fausse car 'self' référence l'objet courant, non None. Pour gérer un arbre vide, il faut vérifier si l'arbre racine est None avant d'appeler la méthode, ou modifier la conception (méthode statique).
  • 3La complexité dans le pire des cas des opérations 'inserer' et 'recherche' est O(n), où n est le nombre de nœuds. Ce cas se produit lorsque l'arbre est dégénéré (chaque nœud n'a qu'un seul enfant), ressemblant à une liste chaînée. La hauteur de l'arbre est alors égale à n.
  • 4La hauteur d'un arbre est définie récursivement : la hauteur d'un arbre vide (None) est 0. La hauteur d'un nœud non vide est 1 + max(hauteur(sous-arbre_gauche), hauteur(sous-arbre_droit)). Cette définition doit être implémentée en gérant soigneusement les cas où les enfants sont None.
  • 5L'implémentation récursive est naturelle pour les opérations sur les arbres. Il est essentiel de bien définir le ou les cas de base (comme un nœud sans enfant) pour que la récursivité termine. Chaque appel récursif doit se faire sur un sous-problème plus petit (un sous-arbre).
exercice

Exercice 2

6 points

Enonce

Exercice 2 — Algorithmique (6 points)

On considère l'algorithme suivant qui permet de déterminer si un tableau d'entiers contient des doublons (valeurs apparaissant au moins deux fois).

def contient_doublons(tab):
    """
    tab : liste d'entiers
    Renvoie True si tab contient au moins un doublon, False sinon
    """
    n = len(tab)
    for i in range(n):
        for j in range(i + 1, n):
            if tab[i] == tab[j]:
                return True
    return False
  1. Donner la complexité temporelle de cet algorithme dans le pire des cas, en fonction de n (la taille du tableau). Justifier votre réponse.

  2. Proposer une version améliorée de cet algorithme qui aurait une complexité temporelle meilleure dans le pire des cas. Expliquer brièvement pourquoi votre solution est plus efficace.

  3. Compléter le code Python ci-dessous qui implémente votre solution améliorée :

def contient_doublons_ameliore(tab):
    """
    tab : liste d'entiers
    Renvoie True si tab contient au moins un doublon, False sinon
    Version améliorée
    """
    # À compléter
Notions :AlgorithmiquePythonComplexitéStructures de données
Difficulte : moyen
Mode examen

Methode

Pour résoudre cet exercice d'algorithmique, il faut d'abord analyser l'algorithme donné en identifiant ses structures de contrôle et leurs impacts sur la complexité. La première question demande une analyse de complexité : il faut compter le nombre d'opérations élémentaires dans le pire cas en fonction de n (taille du tableau). Pour la deuxième question, il faut réfléchir à des structures de données permettant d'accélérer la recherche de doublons, comme les ensembles ou les dictionnaires qui offrent des accès en temps constant. La troisième question implique l'implémentation concrète en Python en veillant à la correction syntaxique et algorithmique. Il est crucial de justifier chaque choix par des arguments de complexité et d'expliquer clairement pourquoi la solution proposée est plus efficace que l'originale.

Points cles

  • 1La complexité temporelle de l'algorithme initial est O(n²) dans le pire cas car il utilise deux boucles imbriquées parcourant toutes les paires d'indices distincts du tableau.
  • 2Une amélioration possible consiste à utiliser un ensemble (set) pour stocker les éléments déjà rencontrés, permettant une vérification d'appartenance en temps constant O(1) en moyenne.
  • 3L'algorithme amélioré a une complexité temporelle moyenne O(n) car il ne parcourt le tableau qu'une seule fois, chaque vérification dans l'ensemble étant en O(1).
  • 4Dans le pire cas, la complexité de l'algorithme amélioré reste O(n) pour le parcours, avec des opérations sur ensemble en O(1) amorti, ce qui est nettement meilleur que O(n²).
  • 5Il faut également considérer la complexité spatiale : l'algorithme amélioré utilise un ensemble pouvant contenir jusqu'à n éléments, donc O(n) en mémoire, contre O(1) pour l'algorithme initial.
exercice

Exercice 3

6 points

Enonce

Exercice 3 — Bases de données (6 points)

On considère une base de données relationnelle contenant une table 'Etudiants' avec les attributs suivants:

  • id_etudiant (INT, clé primaire)
  • nom (TEXT)
  • prenom (TEXT)
  • annee_naissance (INT)
  • specialite (TEXT)

On souhaite créer une fonction Python qui permet de récupérer les étudiants nés après une certaine année et triés par nom.

import sqlite3

def get_etudiants_apres_annee(annee, specialite=None):
    """
    Récupère les étudiants nés après 'annee'.
    Si 'specialite' est spécifié, filtre également par spécialité.
    Retourne une liste de tuples (nom, prenom, annee_naissance).
    """
    conn = sqlite3.connect('universite.db')
    cursor = conn.cursor()
    
    if specialite:
        requete = "SELECT nom, prenom, annee_naissance FROM Etudiants WHERE annee_naissance > ? AND specialite = ? ORDER BY nom"
        cursor.execute(requete, (annee, specialite))
    else:
        requete = "SELECT nom, prenom, annee_naissance FROM Etudiants WHERE annee_naissance > ? ORDER BY nom"
        cursor.execute(requete, (annee,))
    
    resultats = cursor.fetchall()
    conn.close()
    return resultats

# Exemple d'utilisation
print(get_etudiants_apres_annee(2000))
print(get_etudiants_apres_annee(2000, "NSI"))
  1. Expliquez pourquoi l'utilisation des paramètres '?' dans les requêtes SQL est préférable à la concaténation de chaînes. Quels risques cela évite-t-il ?

  2. Proposez une modification de la fonction pour qu'elle retourne également l'âge actuel de chaque étudiant (en supposant l'année actuelle 2023). Quelle serait la complexité temporelle de votre solution ?

  3. Complétez la fonction suivante qui doit permettre d'ajouter un nouvel étudiant dans la base de données, en vérifiant d'abord que l'id_etudiant n'existe pas déjà :

def ajouter_etudiant(id_etudiant, nom, prenom, annee_naissance, specialite):
    """
    Ajoute un étudiant s'il n'existe pas déjà.
    Retourne True si l'ajout a réussi, False sinon.
    """
    conn = sqlite3.connect('universite.db')
    cursor = conn.cursor()
    
    # Vérifier si l'id existe déjà
    # À COMPLÉTER
    
    # Ajouter l'étudiant
    # À COMPLÉTER
    
    conn.close()
    return False  # À MODIFIER
Notions :Bases de donnéesPythonSQLSécurité informatique
Difficulte : moyen
Mode examen

Methode

Pour résoudre cet exercice sur les bases de données en Python, il faut adopter une démarche structurée. Commencez par analyser chaque question séparément. Pour les questions sur SQL, rappelez-vous des bonnes pratiques de sécurité et d'efficacité. Identifiez d'abord les concepts sous-jacents : injection SQL, gestion des connexions, écriture de requêtes paramétrées et calcul de données dérivées. Pour la partie programmation, décomposez le problème en étapes logiques : connexion, vérification, exécution, gestion des résultats et fermeture. Testez mentalement chaque ligne de code avec des cas limites (données existantes, paramètres manquants). Vérifiez toujours la cohérence des types de données entre Python et SQL. Enfin, pour les questions de complexité, analysez les opérations dominantes (accès base de données, boucles) en fonction de la taille des données.

Points cles

  • 1Sécurité avec les requêtes paramétrées : L'utilisation des '?' (placeholders) et du tuple de paramètres dans cursor.execute() prévient les injections SQL. Cela évite qu'une entrée utilisateur malveillante ne soit interprétée comme du code SQL, car les valeurs sont traitées comme des données et non comme du code. La concaténation de chaînes est extrêmement risquée.
  • 2Gestion du cycle de vie d'une connexion : Il est crucial d'ouvrir (connect) et de fermer (close) la connexion à la base de données pour libérer les ressources. Dans un scénario réel avec modifications (INSERT), il faut aussi gérer les transactions avec commit() et rollback().
  • 3Logique conditionnelle pour construire des requêtes : La fonction doit adapter la requête SQL et la liste des paramètres en fonction de l'argument optionnel 'specialite'. Il ne faut pas concaténer la valeur dans la chaîne SQL, mais utiliser un placeholder supplémentaire.
  • 4Vérification d'existence avant insertion : Pour éviter les doublons sur une clé primaire (id_etudiant), il faut exécuter une requête SELECT pour vérifier si l'identifiant existe déjà. L'insertion ne doit avoir lieu que si le résultat de cette vérification est vide (fetchone() retourne None).
  • 5Calcul de données dérivées : L'âge est une donnée dérivée de l'année de naissance et de l'année de référence. Il se calcule côté Python après récupération des données brutes depuis la base. La complexité est linéaire O(n) par rapport au nombre d'étudiants retournés, car il faut parcourir la liste des résultats pour effectuer le calcul une fois.

Informations

MatiereNSI
Session2023
CentreMétropole
Filieregenerale
Coefficient16
Source : Généré par IA (DeepSeek) - Sujet type Bac