NSI - Bac 2024
Métropole - Session normale
Epreuve du 15 juin 2024
Consigne officielle
Le candidat traite les trois exercices.
Exercice 1
Enonce
Exercice 1 — Structures de données (6 points)
[Contexte] On considère une structure de données permettant de représenter un arbre binaire. Un arbre binaire est soit vide, soit un nœud contenant une valeur et deux sous-arbres (gauche et droit).
class Noeud:
def __init__(self, valeur, gauche=None, droit=None):
self.valeur = valeur
self.gauche = gauche
self.droit = droit
def parcours_prefixe(arbre):
"""Retourne la liste des valeurs de l'arbre dans l'ordre préfixe."""
if arbre is None:
return []
else:
return [arbre.valeur] + parcours_prefixe(arbre.gauche) + parcours_prefixe(arbre.droit)
# Exemple d'arbre :
# 1
# / \
# 2 3
# / \
# 4 5
arbre_exemple = Noeud(1,
Noeud(2,
Noeud(4),
Noeud(5)),
Noeud(3))
print(parcours_prefixe(arbre_exemple)) # Affiche [1, 2, 4, 5, 3]
-
Quelle est la complexité temporelle de la fonction
parcours_prefixeen fonction du nombre n de nœuds de l'arbre ? Justifiez votre réponse. -
On souhaite maintenant implémenter une fonction
hauteur(arbre)qui retourne la hauteur de l'arbre (la hauteur d'un arbre vide est -1, la hauteur d'un arbre réduit à un nœud est 0). Proposez une implémentation récursive de cette fonction. -
Complétez la fonction
est_equilibre(arbre)ci-dessous qui retourneTruesi l'arbre est équilibré, c'est-à-dire si pour chaque nœud, les hauteurs de ses sous-arbres gauche et droit diffèrent d'au plus 1, etFalsesinon.
def est_equilibre(arbre):
"""Retourne True si l'arbre est équilibré, False sinon."""
if arbre is None:
return True
else:
# À compléter
Methode
Pour résoudre cet exercice sur les arbres binaires, il faut d'abord bien comprendre la structure de données récursive. Un arbre binaire est soit vide (None), soit un nœud avec une valeur et deux sous-arbres. La récursivité est l'outil principal pour manipuler ces structures. Pour la complexité, on analyse le nombre d'appels récursifs en fonction du nombre de nœuds. Pour les fonctions de hauteur et d'équilibre, on utilise une approche récursive qui traite le cas de base (arbre vide) puis combine les résultats des sous-arbres. Il est crucial de bien définir les cas de base et les cas récursifs, et de s'assurer que tous les cas sont couverts. On vérifie toujours les arbres vides pour éviter les erreurs. Pour l'équilibre, on doit à la fois calculer la hauteur et vérifier la condition d'équilibre, ce qui peut se faire en une seule traversée ou avec des appels séparés selon l'implémentation.
Points cles
- 1Complexité des parcours d'arbres : Un parcours récursif comme le parcours préfixe visite chaque nœud exactement une fois. La complexité temporelle est donc linéaire, O(n), où n est le nombre de nœuds. Chaque appel effectue un travail constant (ajout dans une liste) et fait deux appels récursifs, mais le nombre total d'appels est proportionnel à n.
- 2Hauteur d'un arbre : La hauteur est définie comme la longueur du chemin le plus long de la racine à une feuille. Pour un arbre vide, la hauteur est souvent définie comme -1 (ou parfois 0 pour un arbre à un nœud). La formule récursive est : hauteur(arbre) = 1 + max(hauteur(gauche), hauteur(droit)) pour un nœud non vide, avec -1 pour None.
- 3Arbre équilibré : Un arbre binaire est équilibré si pour chaque nœud, la différence de hauteur entre ses sous-arbres gauche et droit est au plus 1. Cette propriété doit être vérifiée récursivement sur tous les nœuds. On peut la vérifier en calculant les hauteurs et en testant la condition.
- 4Récursivité sur les arbres : Les algorithmes sur les arbres binaires suivent un schéma récursif classique : traiter le cas de l'arbre vide, puis pour un nœud, traiter la valeur, et appeler récursivement la fonction sur les sous-arbres gauche et droit. La combinaison des résultats se fait selon l'objectif (liste, hauteur, booléen).
- 5Implémentation en Python : Utiliser la classe Noeud fournie. Les fonctions prennent en paramètre un arbre (un objet Noeud ou None). Toujours vérifier si l'arbre est None en premier. Pour l'équilibre, on peut soit faire deux fonctions (une pour la hauteur, une pour vérifier l'équilibre en appelant la hauteur), soit une fonction qui retourne à la fois hauteur et équilibre en un seul parcours pour plus d'efficacité.
Exercice 2
Enonce
Exercice 2 — Algorithmique (6 points)
On considère l'algorithme suivant qui permet de déterminer si une chaîne de caractères est un palindrome (elle se lit de la même façon de gauche à droite et de droite à gauche).
def est_palindrome(chaine):
"""
Détermine si une chaîne de caractères est un palindrome.
"""
n = len(chaine)
for i in range(n // 2):
if chaine[i] != chaine[n - 1 - i]:
return False
return True
-
Expliquez ce que fait la fonction
est_palindromeet donnez sa complexité temporelle en fonction de la longueur n de la chaîne. -
On souhaite maintenant écrire une fonction récursive
est_palindrome_recqui réalise la même tâche. Complétez le code ci-dessous :
def est_palindrome_rec(chaine):
"""
Version récursive pour déterminer si une chaîne est un palindrome.
"""
n = len(chaine)
if n <= 1:
return True
if chaine[0] != chaine[n - 1]:
return False
# À compléter
- Modifiez la fonction
est_palindrome_recpour qu'elle ignore les espaces et la casse (majuscules/minuscules). Par exemple, "Ésope reste ici et se repose" doit être considéré comme un palindrome.
Methode
Pour résoudre cet exercice sur les palindromes, il faut d'abord comprendre le fonctionnement de l'algorithme itératif fourni. Analyser la boucle et les conditions permet de saisir la logique de comparaison symétrique des caractères. Pour la version récursive, identifier le cas de base (chaîne vide ou d'un seul caractère) et le cas récursif (comparer les extrémités puis rappeler la fonction sur la sous-chaîne restante) est essentiel. Ensuite, pour ignorer espaces et casse, il faut prétraiter la chaîne ou adapter la comparaison en utilisant des méthodes de chaîne comme lower() et replace(). La complexité temporelle s'évalue en comptant le nombre d'opérations élémentaires en fonction de la longueur n de la chaîne. Une approche systématique consiste à tester chaque fonction avec des exemples variés (palindromes simples, avec espaces, avec majuscules) pour vérifier son bon fonctionnement.
Points cles
- 1Principe de vérification d'un palindrome : comparer les caractères symétriquement par rapport au centre de la chaîne. L'algorithme itératif parcourt la moitié de la chaîne en comparant chaque caractère avec son symétrique.
- 2Complexité temporelle : la fonction itérative a une complexité en O(n) où n est la longueur de la chaîne, car elle effectue au plus n/2 comparaisons. C'est une complexité linéaire.
- 3Récursivité : pour une version récursive, le cas de base est une chaîne de longueur 0 ou 1 (toujours un palindrome). Le cas récursif compare les premier et dernier caractères, puis rappelle la fonction sur la sous-chaîne intermédiaire (en excluant ces deux caractères).
- 4Gestion des espaces et de la casse : pour ignorer les différences de casse, convertir la chaîne en minuscules avec la méthode lower(). Pour ignorer les espaces, les supprimer avec replace(' ', ''). Ce prétraitement doit être fait avant la vérification du palindrome.
- 5Indices et découpage de chaînes : en Python, on accède aux caractères avec chaine[i] et on extrait des sous-chaînes avec le slicing chaine[1:-1]. Attention aux indices négatifs qui permettent d'accéder facilement à la fin de la chaîne.
Exercice 3
Enonce
Exercice 3 — Bases de données (6 points)
Un développeur travaille sur une application de gestion de bibliothèque. Il utilise une base de données relationnelle contenant les tables suivantes :
- LIVRES (id_livre, titre, auteur, annee_publication, id_genre)
- GENRES (id_genre, nom_genre)
- EMPRUNTS (id_emprunt, id_livre, date_emprunt, date_retour)
Il souhaite créer une fonction Python qui permet de récupérer les livres empruntés pendant une période donnée, avec leur genre.
import sqlite3
def livres_empruntes_periode(date_debut, date_fin):
"""
Retourne la liste des livres empruntés entre date_debut et date_fin
avec leur titre, auteur et nom du genre.
"""
conn = sqlite3.connect('bibliotheque.db')
curseur = conn.cursor()
requete = """
SELECT L.titre, L.auteur, G.nom_genre
FROM LIVRES L
JOIN GENRES G ON L.id_genre = G.id_genre
JOIN EMPRUNTS E ON L.id_livre = E.id_livre
WHERE E.date_emprunt BETWEEN ? AND ?
"""
curseur.execute(requete, (date_debut, date_fin))
resultats = curseur.fetchall()
conn.close()
return resultats
# Test de la fonction
print(livres_empruntes_periode('2024-01-01', '2024-03-31'))
-
Expliquez ce que fait la requête SQL dans la fonction, en détaillant chaque clause (SELECT, FROM, JOIN, WHERE).
-
Le développeur souhaite maintenant modifier la fonction pour qu'elle retourne également le nombre total d'emprunts par livre pendant la période. Proposez une solution algorithmique en expliquant votre démarche.
-
Complétez le code Python ci-dessous pour implémenter cette nouvelle fonctionnalité :
def livres_empruntes_avec_compteur(date_debut, date_fin):
"""
Retourne la liste des livres empruntés entre date_debut et date_fin
avec leur titre, auteur, nom du genre et nombre d'emprunts.
"""
conn = sqlite3.connect('bibliotheque.db')
curseur = conn.cursor()
# À compléter : écrire la requête SQL adaptée
requete = """
"""
curseur.execute(requete, (date_debut, date_fin))
resultats = curseur.fetchall()
conn.close()
return resultats
Methode
Pour résoudre cet exercice sur les bases de données en Python, il faut adopter une approche méthodique. Commencez par analyser soigneusement la structure des tables et leurs relations (clés primaires et étrangères). Pour la partie SQL, identifiez les données à extraire et les tables à joindre. La compréhension des jointures (INNER JOIN) est cruciale pour relier les informations. Pour les agrégations (comme le comptage), pensez à utiliser la clause GROUP BY et les fonctions d'agrégation (COUNT, SUM). En Python, suivez le pattern standard : connexion à la base, création d'un curseur, exécution d'une requête paramétrée (avec ? pour éviter les injections SQL), récupération des résultats et fermeture de la connexion. Testez toujours votre requête mentalement ou sur un exemple simple avant de l'écrire dans le code.
Points cles
- 1Compréhension des jointures SQL : La requête utilise JOIN pour combiner les tables LIVRES, GENRES et EMPRUNTS. L'INNER JOIN (implicite avec JOIN) assure que seuls les livres ayant un genre correspondant et un emprunt dans la période sont sélectionnés. La condition ON spécifie comment les tables sont liées (par les clés étrangères).
- 2Filtrage avec WHERE et BETWEEN : La clause WHERE restreint les résultats aux emprunts dont la date est comprise dans l'intervalle spécifié. BETWEEN ? AND ? est utilisé avec des paramètres pour une requête sécurisée. Les dates doivent être au format compatible avec le champ de la base (généralement 'YYYY-MM-DD').
- 3Agrégation avec GROUP BY et COUNT : Pour compter le nombre d'emprunts par livre, il faut regrouper les résultats par livre (ou par identifiant de livre) et utiliser la fonction d'agrégation COUNT sur les lignes du groupe. Cela nécessite d'ajouter GROUP BY à la requête.
- 4Sélection des colonnes dans SELECT : La clause SELECT liste les colonnes à retourner. Pour la version avec compteur, il faut ajouter COUNT(*) ou COUNT(E.id_emprunt) pour obtenir le nombre d'emprunts. Il est essentiel que toutes les colonnes non agrégées soient listées dans GROUP BY.
- 5Programmation Python avec sqlite3 : L'utilisation du module sqlite3 suit un pattern précis : connexion, curseur, exécution de requête avec paramètres (un tuple), fetchall() pour récupérer tous les résultats sous forme de liste de tuples, et fermeture de la connexion pour libérer les ressources.
