INF3105 - Automne 2019

Examen mi-session / Partie A

Corrigé PDF.

#
Question
Explications
Q1
Cochez les mots qui sont des mots-clés (keywords ou mots réservés) du langage C/C++.
Il faut bien distinguer les parties «langage» et «bibliothèque standard».

Un langage de programmation est généralement défini par des règles lexicales et syntaxiques (grammaire). Les règles lexicales définissent des mots-clés (keyword) qui ont une signification particulière (types de base, instructions de contrôle, etc.). Les mots-clés sont aussi appelés «mots réservés», car ils ne peuvent pas être utilisés comme identificateur pour nommer une classe, une fonction ou un objet (variable). Voir diapo #15 de intro-c++.pdf.

Un langage de programmation est habituellement accompagné d'une bibliothèques de base. En C++, il s'agit de la «bibliothèque standard» (standard library). Voir diapos #39-41 de intro-c++.pdf.

Réponses:
  • Mots clés: do, delete, return, unsigned, char, while, short, new, switch.
  • Identificateurs pour des classes, fonctions ou objets de la bibliothèque standard C/C++ : ofstream, cout, string, sin, cin, sqrt, istream.
Q2
En C/C++, la signature d'une fonction est définie par ...
Seul le type de retour ne fait pas partie de la signature. Le type de retour ne peut pas faire partie de la signature, car l'appelant n'est pas obligé de récupérer la valeur retournée.

Exemple:
int f(int x){return 2;}
string f(int x){return string("allo");}
void f(int x, int y) { }
int main(){
    int a = f(2); // théoriquement, on pourrait déduire.
    string s = f(4); // théoriquement, on pourrait déduire.
    f(3); // serait ambiguë.
    f(2, 4); // correct
}
Q3
Cochez le ou les énoncés illégaux
(ayant au moins une erreur).
int p; int& q=p;
string n, *p=0; // un pointeur nul a une valeur de zéro (0)
double k; int* p = &k; // Erreur: p doit pointeur sur un entier et non un double
int *p, *q, **r=&q; // correct : r (double pointeur) pointe sur le pointeur d'entier q

Q4
Le programme progA.cpp laisse _____ objets de type int non libérés sur le tas (heap).
  • f1 : aucun objet perdu.
  • f2 : 2 objets Rectangle «perdus».
  • f3 : à la ligne 39, en réaffectant le pointeur r (r=new Rectangle[4];), on peut les 2 objets Rectangle alloués par le new à la ligne précédente.
  • Donc 4 objets Rectangle.
  •  4 Rectangle * 2 Point/Rectangle * 3 int/Point = 24 objets int.
Q5
Le programme progA.cpp affiche sur la première ligne (fonction f1) ...
Ici, il faut regarder la fonction f1().
Ligne 28: L'ordre de construction : p1 ensuite p2. Cela affiche «P12P34».
Ligne 29: L'objet r1 est construit. Rectangle n'hérite de rien. Les attributs hg sont bd initialisés avec le constructeur par copie Point::Point(const Point&) qui est non fourni donc généré par le compilateur. Enfin, «G_» est affiché.
Ligne 30: L'objet r2 est construit par copie (constructeur par copie Rectangle::Rectangle(const Rectangle&) non fourni, donc généré par le compilateur). Les points hg et bd sont aussi construits par copie.
Après Ligne 31. Tout ce qui a été construit doit être déconstruit. Par convention, on déconstruit dans l'ordre inverse de la construction.
On détruit r2. Cela affiche «r_» et appelle le destructeur pour les attributs bd, hg. Ces 2 derniers affichent «p34» et «p12».
Idem pour r1 qui affiche «r_», «p34» et «p12».
Enfin, p2 et p1 sont détruits, ce qui affiche encore «p34» et «p12».

Réponse: « P12P34G_r_p34p12r_p34p12p34p12 ».
Q6
Le programme progA.cpp affiche sur la deuxième ligne (fonction f2) ...
Ligne 33 : construction de 2 objets Rectangle avec le constructeur Rectangle::Rectangle(). Pour chacun de ces 2 rectangles, hg et bd sont construits avec Point::Point() qui équivaut à Point::Point(0, 0). Cela affiche «P00» et «P00». Enfin, le corps de Rectangle::Rectangle() affiche «R_».
Le tout deux fois : «P00P00R_P00P00R_».
Ligne 34 : l'expression «Rectangle(Point(1,2),Point(3,4))» construit des 3 objets «temporaire», 2 Point et 1 Rectangle. La construction du tout affiche «P34P12G_». La destruction du Rectangle affiche «r_p34p12». La destruction des 2 Point affiche «p12p34».
Enfin, le tableau sur lequel pointe tr n'est pas détruit, car pas de «delete[] tr».

Réponse: P00P00R_P00P00R_P34P12G_r_p34p12p12p34
Q7
L’exécution de ./progB <
test1.txt affiche sur la première ligne :

Analyse du programme

  • Ligne 8 : la boucle while itère tant que l'entrée est valide.
  • La variable position indique la position du mot courant dans la phrase. Cette variable est incrémenté de 1 à chaque mot et remise à zéro (0) après chaque point («.»).
  • Ligne 14
    • La première condition «position<=3» vérifie si on est dans les 3 premiers mots d'une phrase.
    • La deuxième condition «++compte[mot]>nbmax» est évaluée si seulement si la première est vérifiée.
      • meilleur mémorise le mot le plus fréquent en début de phrase (3 premiers mots).
      • nbmax mémorise la fréquent de ce mot;
      • k17 compte le nombre de fois que la ligne 17 (et 15-16) est exécutée, c-à-d le nombre de fois qu'on détecte un nouveau mot plus fréquent.

Dans texte 1, ce qui est intéressant à retenir, cest les 3 premiers mots par phrase:
a b c d e f g . b c d e f g h j a . c e g z x y a . c b a k a l d b b b b . d c b a k .

Compte:

  • a: 2
  • b : 4
  • c : 5
  • d : 2
  • e : 1
  • g : 1

Mot le plus fréquent : c.

Q8
L’exécution de ./progB <
test2.txt affiche sur la deuxième ligne :
test2.txt:
a b c
d e . e d c b a . a b b c c .

États des compteurs (* indique une nouveau meilleur):
a=1* b=1 c=1 e=1 d=1 c=2* a=2 b=2 b=3*

* k17 = 3 fois

Q9
Quelle est la complexité temporelle (pire cas) du programme progB.cpp ? Supposez n phrases, m mots par phrase et k mots différents.
Analyse:
  • Lignes 10-12 : on lira nm mots, dont au moins O(nm).
  • Ligne 13 : on aurat n phrases, donc O(n).
  • Ligne 14:
    • La première condition «position<=3» : O(nm).
    • La deuxième condition «++compte[mot]>nbmax» est évaluée 3n fois, donc O(n) fois.
    • L'expression «compte[mot]» fait une recherche dans dictionnaire. Chaque recherche coûte O(log k), car il y aura au lieu k mots différents. Donc total de 3n log k ===> O(n log k).
  • Réponse : O(nm + n log k).
  • Autre façon d'exprimer: O(n (m + log k) ).
La réponse O(nm log k) a mérité 1 point /2, car proche de la réponse .
Q10
Cochez les expressions de complexité grand-O qui sont simplifiées. Notez que k, m et n sont des variables indépendantes.
  • O(n+7) : non simplifié; il faut enlever le +7.
  • O(n² + 8m) : non simplifié; il faut enlever la constante 8 devant le m.
  • O(n² + m): forme la plus simple, car m et n indépendantes.
  • O(3n + 2m): non simplifié; il faut enlever les constantes 3 et 2.
  • O(n + k) : forme la plus simple, car n et k indépendantes.
  • O(n! 2^n) : forme la plus simple (ça aurait été autrement si n! et 2^n avaient été additionnés...).
Q11
Cochez les énoncés vrais. Les symboles < et > signifient moins et plus complexe que.
  • O(5n²+9n) < O(n³) : VRAI, car n³ croît plus rapidement que n².
  • O(3n) > O(2n) : FAUX, car même ordre de grandeur O(n).
  • O(2^n) > (n⁵) : VRAI, car 2^n croît plus rapidement que n⁵.
  • O(n!) > O(2^n) : VRAI, car n! croît plus rapidement que 2^n.
Q12
L'ajout à la fin d'un tableau dynamique a une complexité temporelle amortie de ____ lorsque la politique d'agressissement augmente la capacité de 1. À chaque ajout, on recopie tout le tableau.

Réponse: O(n).
Q13
L'ajout à la fin d'un tableau dynamique a une complexité temporelle amortie de ____ lorsque la politique d'agressissement double la capacité. Double la taille du tableau a un coût de O(n). Ce coût est cependant amorti sur les n ajouts suivants.

Réponse: O(1)
Q14
L’insertion dans un arbre binaire de recherche équilibré a une complexité temporelle de ____.
Réponse: O(log n)
Q15
Le test d'équivalence (operator ==) pour un arbre binaire de recherche équilibré a une complexité temporelle de ____. Supposez la meilleure implémentation possible de cet opérateur.
Que l'arbre soit équilibré ou non, il faut de toute façon parcourir l'arbre en entier pour faire la comparaison de tous les éléments. L'équilibre de l'arbre n'est pas un enjeu, car on ne fait pas de recherche. Le parcours en inordre a une complexité de O(n), et ce, peu importe la structure de l'arbre.

Réponse: O(n)

Commentaires:
  • Une implémentation naïve de la comparaison consiste à itérer sur l'un des deux arbres, et pour chaque élément rencontré, on le recherche dans l'autre arbre. Cette implémentation a une complexité temporelle de O(n log n) si l'arbre est équilibré, et de O(n²) sinon.
  • La «meilleure implémentation possible» consiste à parcourir simultanément les 2 arbres en inordre, tel que présenté en classe.
Q16
Le test d'équivalence (operator ==) pour un arbre binaire de recherche non équilibré a une complexité temporelle de ____. Supposez la meilleure implémentation possible de cet opérateur.
Q17
On ajoute, dans un ordre arbitraire, n entiers différents dans 2 arbres, l'un AVL et l'autre rouge-noir. Les 2 arbres ont une hauteur de h1 (AVL) et h2 (R-N). On peut conclure avec certitude...
  • h1 ≤ h2 : FAUX. C'est uniquement vrai pour le pire cas, c'est-à-dire que la hauteur maximum de l'arbre RN est plus grande que l'arbre AVL.
  • h2 ≤ h1 : FAUX.
  • h1 ≤ 1.5 log2 n + 1: VRAI. Dans les notes de cours, la hauteur maximum est plus précisiément définie par 1.44 log2 (n+2) -0.328. Dans les explications, on simplifiait parfois à 1.44 log2 n, car la différence devient négligeable lorsque n est grand.
  • h2 ≤ 1.5 log2 n + 1 : FAUX.
  • h1 ≤ 2 log2 n + 1: VRAI. Rappel: h ≤ 2 ⌈log2(n+1)⌉.
  • h2 ≤ 2 log2 n + 1: VRAI. Rappel: h ≤ 2 ⌈log2(n+1)⌉.
  • n < 2h1 : VRAI.
  • h2 ≥ ⌈log2(n)⌉ : VRAI.
Q18
La meilleure implémentation possible d'une copie profonde d'un arbre AVL (constructeur par copie et operator=) a une complexité temporelle de ____ dans le pire cas.
Voir explications des Q17 et Q18. Qu'un arbre binaire de recherche soit équilibré ou non, il faut parcourrir en inordre l'arbre en entier pour en faire une copie complète. Le fait qu'il soit équilibré AVL ou RN ne procure aucun avantage pour la copie.

Il serait même possible de faire une copie en O(n) d'une arbre binaire de recherche non équilibré vers un arbre équilibré.

Réponse: O(n)
Q19
La meilleure implémentation possible d'une copie profonde d'un arbre rouge-noir (constructeur par copie et operator=) a une complexité temporelle de ____ dans le pire cas.
Q20
L'insertion dans un arbre AVL a une complexité temporelle de ____ dans le pire cas.
Réponse: O(log n)
Q21
La meilleure implémentation possible de la suppression dans un arbre binaire de recherche (pas forcément équilibré) a une complexité temporelle de ____ dans le pire cas. Pour enlever un élément, il faut d'abord localiser le noeud correspondant. Si l'arbre n'est pas équilibré, sa hauteur peut être de l'ordre de O(n).

Réponse: O(n)
Q22
Durant l'insertion d'un élément dans un arbre AVL, combien de rotation(s) peut-on avoir dans le pire cas? Une double rotation compte pour 2 rotations. Supposez que l'arbre contient n éléments.
Maximum 2.

Une rotation est nécessaire lorsqu'il y a un déséquilibre. L'existence d'un déséquilibre implique qu'il y a de l'espace disponible dans un sous-arbre par rapport à l'autre sous-arbre. La simple ou double rotation vise justement à combler cet espace disponible. Ainsi, après la rotation, le sous-arbre retrouve sa hauteur originale. Puisque le sous-arbre conserve sa hauteur originale, il ne peut plus avoir d'autre rotation sur le chemin de retour en remontant jusqu'à la racine.
Q23
Durant l'enlèvement d'un élément dans un arbre AVL, combien de rotation(s) peut-on avoir dans le pire cas? Une double rotation compte pour 2 rotations. Supposez que l'arbre a une hauteur de h. Choisissez la réponse la plus proche.
Un enlèvement peut réduire la hauteur d'un sous-arbre. La réduction de la hauteur d'un sous-arbre peut provoquer le déséquilibre du sous-arbre parent. Le ré-équilibrage d'un sous-arbre peut réduire sa hauteur. Bref, il peut y avoir une cascade de (simple/double) rotations jusqu'à la racine. La figure ci-bas illustre les rotations provoquées après l'enlèvement du noeud rouge. Enfin, il y aura jusqu'à 2(h-1) rotations, où h est la hauteur initiale.
... ... ... #1 #2 #3 #4 #5 #6 #7 #8
Réponse la plus proche: 2h.
Q24
On insère les entiers 0 à 4, dans un ordre aléatoire, dans un arbre AVL. Combien d'arbres différents (structure) peut on obtenir
Si la racine est 1 ou 3, ses sous-arbres gauche et droite n'ont qu'une possibilités, donc 2 arbres.
Si racine est 2, existe 2 sous-arbres à gauche pour placer {0,1} et 2 autres à droite {2,3}, donc 4 arbres. Donc un total de 6 arbres différents.
2 1 0 3 4 0 3 1 4 2 0 2 1 3 4 0 2 1 4 3 2 3 4 1 2 0 4 3 1 0
Réponse: 6 arbres différents

Q25
On insère les entiers 0 à 63 (incl.), dans un ordre aléatoire, dans un arbre AVL. Quel est le plus petit entier pouvant se retrouver à la racine ?
Réponse : 12.

Similaire à l'exercice Examen de mi-session 2014A / Question 3 d.

À gauche, un arbre de hauteur 5 avec les entiers 0 à 11.

À droite, un arbre de hauteur 5 ne peut pas contenir les 51 entiers restants 13 à 63 (2⁵-1=31<51).

À droite, un arbre de hauteur 6 peut contenir les 51 entiers restants (2⁶-1=63>=51).
Q26
On insère les entiers 0 à 63 (incl.), dans un ordre aléatoire, dans un arbre R-N. Quel est le plus petit entier pouvant se retrouver à la racine ?

Supposons que l'entier X soit à la racine. Il faut donc placer (63 - X - 1) entiers à droite et X entiers à gauche. Afin de minimiser l'entier X, il faut mettre le plus d'entiers possibles à droite et le moins possible à gauche. Pour maximiser le nombre de noeuds à droite, on peut alterner les couleurs rouge et noir. Pour minimiser le nombre de noeuds à gauche, il faut éviter autant que possible les noeuds rouge à gauche (idéalement aucun).

Pour placer 64 noeuds, il faut une hauteur d'au moins log2 (64+1)⌉=7.

Si l'arbre a une hauteur minimale de 7, le sous-arbre de droite doit avoir une hauteur d'au moins 6. Avec une hauteur de 6, on peut placer jusqu'à 26-1=63 noeuds.

7 3 1 5 0 2 4 6 ... ... 8 ... ... hauteur=37 noeuds hauteur=756 noeuds

Réponse: 7.

Q27
Dans quel ordre doit-on insérer les nombres 1 à 3 dans un arbre AVL pour provoquer une double-rotation ?
Réponses: 1 3 2 et 3 1 2.
Q28
Dans quel ordre doit-on insérer les nombres 1 à 7 dans un arbre AVL pour maximiser la hauteur de cet arbre? La hauteur maximale d'un arbre AVL contenant 7 noeuds est de 4.
1 2 3 4 5 6 7 ==> hauteur de 3 (arbre plein).
4 2 5 1 3 6 7 ==> hauteur de 3 (arbre plein).
3 5 2 1 6 4 7 ==> hauteur de 4 (maximise la hauteur).
2 1 4 5 3 6 7 ==> hauteur de 3 (arbre plein).
2 4 3 5 7 1 6 ==> hauteur de 4 (maximise la hauteur).
2 3 4 5 4 1 7 ==> erreur d'édition dans le questionnaire, il n'y avait pas de 6. Les 2 réponses sont donc acceptées.
  • Par définition, un ensemble ne peut pas contenir deux fois le même élément. Le deuxième 4 ne devrait pas avoir d'effet. Donc, l'arbre aura une hauteur de 3.
  • Cependant, on peut généraliser la structure d'arbre pour permettre de stocker plusieurs fois des objets équivalents (voir std::multiset). Dans ce cas, l'arbre aurait une hauteur de 4.
  • Si on remplace le dernier 4 par un 6, cela donne le cas ci-dessous.
2 3 4 5 6 1 7 ==> hauteur de 4 (maximise la hauteur).
Q29
Dans un arbre rouge-noir, ____ (avant et après les opérations d'insertion et d'enlèvement).
Réponse: les sentinelles sont à profondeur noire égale.
Q30
Quelle est la profondeur noire des sentinelles après l’insertion des entiers 1 à 5 inclusivement dans un arbre Rouge-Noir?
5 4 3 1 2 1 3 2 1 2 1 3 2 1 4 3 2 1 4 3 2 1 4 3 2 1 3 5 4 2 1