Passer au contenu

Pensée computationnelle et résolution de problèmes

Informatique A-Level · Sujet 19

Entrainer
Leçon vidéo pour ce sujet Ouvrir la page vidéo
15:33

Recherche & Tri

Un annuaire téléphonique avec un million de noms. Si vous les vérifiez un par un, vous pouvez faire un million de comparaisons. Mais vous connaissez déjà l'astuce : ouvrez-le dans le…

Narration en anglais · Sous-titres anglais + 中文 incrustés

19.1

Algorithmes de recherche

Programme
Les candidats doivent être capables de : Notes et orientations
Montrer la compréhension des méthodes de recherche linéaire et de recherche binaire Écrire un algorithme pour implémenter une recherche linéaire Écrire un algorithme pour implémenter une recherche binaire Les conditions nécessaires à l'utilisation d'une recherche binaire Comment la performance d'une recherche binaire varie selon le nombre d'éléments de données
Montrer la compréhension des méthodes de tri par insertion et de tri à bulles Écrire un algorithme pour implémenter un tri par insertion Écrire un algorithme pour implémenter un tri à bulles La performance d'une routine de tri peut dépendre de l'ordre initial des données et du nombre d'éléments de données
Montrer la compréhension et utiliser les Types de Données Abstraits (ADT) Écrire des algorithmes pour trouver un élément dans chacun des suivants : liste chaînée, arbre binaire Écrire des algorithmes pour insérer un élément dans chacun des suivants : pile, file d'attente, liste chaînée, arbre binaire Écrire des algorithmes pour supprimer un élément de chacun des suivants : pile, file d'attente, liste chaînée Montrer la compréhension qu'un graphe est un exemple d'ADT. Décrire les caractéristiques principales d'un graphe et justifier son utilisation pour une situation donnée. Les candidats ne seront pas tenus d'écrire du code pour une structure de graphe
Montrer comment il est possible d'implémenter des ADTs à partir d'un autre ADT Décrire les ADTs suivants et démontrer comment ils peuvent être implémentés à partir de types intégrés appropriés ou d'autres ADTs : pile, file d'attente, liste chaînée, dictionnaire, arbre binaire
Montrer la compréhension que différents algorithmes effectuant la même tâche peuvent être comparés en utilisant des critères (p. ex., temps nécessaire pour terminer la tâche et mémoire utilisée) Incluant l'utilisation de la notation Big O pour spécifier la complexité temporelle et spatiale

Source : Programme Cambridge International

Big O : comment les algorithmes évoluent
Tri par insertion : glisser chaque carte à sa place
Tri à bulles, passe par passe
Recherche binaire : diviser par deux et vaincre

Une recherche trouve une valeur cible dans un ensemble (souvent un tableau 数组) et retourne sa position, ou "non trouvé".

Un annuaire téléphonique ouvert
Rechercher dans une liste triée, comme un annuaire téléphonique, est bien plus rapide que vérifier chaque entrée une par une

Recherche linéaire

Une recherche linéaire 线性查找 parcourt du début à la fin, comparant chaque élément avec la cible :

FOR i ← 1 TO n
    IF A[i] = target THEN
        RETURN i
    ENDIF
NEXT i
RETURN -1   // not found

Aucune préparation n'est nécessaire, donc cela fonctionne sur n'importe quelle liste. Cas le plus défavorable O($n$) (cible à la fin ou absente) ; meilleur cas 1 comparaison. Utilisez-le sur des données non triées ou de petites listes. (La ⟨-1⟩ retournée est une valeur sentinelle — une position impossible qui signifie "non trouvé" ; l'appelant teste IF result = -1.)

La version de l'examen. Paper 3 vous demande de compléter une recherche linéaire écrite avec un indicateur et une boucle WHILE, et Paper 4 d'écrire une fonction qui retourne l'index ou un compteur. Tous deux ressemblent à ceci :

FUNCTION LinearSearch(Data : ARRAY OF INTEGER, Target : INTEGER) RETURNS INTEGER
    DECLARE Index, Count : INTEGER
    Count ← 0
    FOR Index ← 1 TO 100
        IF Data[Index] = Target THEN
            Count ← Count + 1
        ENDIF
    NEXT Index
    RETURN Count          // how many times Target occurs; 0 means not found
ENDFUNCTION

Pour s'arrêter à la première correspondance au lieu de cela, utilisez une boucle WHILE Index <= 100 AND NOT Found qui définit Found ← TRUE et mémorise l'index. Les points sont pour la boucle sur chaque élément, la comparaison, et ce qui est retourné lorsque la valeur est absente.

Une rangée de cellules alphabétiques A à Z ; les cellules A à V sont ombrées comme cochées et W est mis en évidence comme la correspondance, avec un pointeur sous W
La recherche linéaire vérifie chaque lettre à tour de rôle — 23 comparaisons pour trouver W

Recherche binaire

Une recherche binaire 二分查找 nécessite que les données soient triées. Regardez l'élément central ; s'il est la cible, c'est fini ; si la cible est plus petite, cherchez dans la moitié gauche, sinon dans la moitié droite — divisant la plage par deux à chaque fois :

low ← 1
high ← n
WHILE low <= high DO
    mid ← (low + high) DIV 2
    IF A[mid] = target THEN
        RETURN mid
    ENDIF
    IF A[mid] < target THEN
        low ← mid + 1
    ELSE
        high ← mid - 1
    ENDIF
ENDWHILE
RETURN -1

Cas le plus défavorable O($\log_{2} n$) — pour un million d'éléments, environ 20 comparaisons. Beaucoup plus rapide que la recherche linéaire sur de grands tableaux triés, mais vous devez d'abord trier (un coût ponctuel O($n \log n$)), ce qui vaut le coup si vous recherchez de nombreuses fois.

"Énoncez la condition nécessaire pour une recherche binaire." Les données doivent être ordonnées (triées, croissantes ou décroissantes, sur la clé recherchée). "Décrivez comment effectuer une recherche binaire" (trois points) : (1) trouvez l'élément central de la liste (ou de la plage actuelle) et comparez-le avec la cible ; (2) s'il correspond, la recherche se termine ; si la cible est plus petite, répétez sur la moitié inférieure, si plus grande, sur la moitié supérieure ; (3) continuez à diviser par deux la plage jusqu'à ce que l'élément soit trouvé ou que la plage soit vide, ce qui signifie qu'il n'est pas présent.

La version de l'examen, avec les bornes et un indicateur, est celle à reproduire lorsque l'on vous demande de compléter l'algorithme :

DECLARE Lower, Upper, Mid : INTEGER
DECLARE Found : BOOLEAN
Lower ← 0
Upper ← 99
Found ← FALSE
WHILE Lower <= Upper AND NOT Found
    Mid ← (Lower + Upper) DIV 2
    IF Names[Mid] = Target THEN
        Found ← TRUE
    ELSE
        IF Names[Mid] < Target THEN
            Lower ← Mid + 1
        ELSE
            Upper ← Mid - 1
        ENDIF
    ENDIF
ENDWHILE
IF Found THEN
    OUTPUT Mid
ELSE
    OUTPUT "Not found"
ENDIF

"Expliquez comment les performances varient avec le nombre d'éléments." Chaque comparaison réduit de moitié le nombre d'éléments restants, donc le nombre maximum de comparaisons est environ $\log_{2} n$ : doubler la taille de la liste ajoute seulement non comparaison de plus. C'est O($\log n$). "Comparez la recherche linéaire et binaire" : une recherche linéaire nécessite jusqu'à $n$ comparaisons (O($n$)) et, en moyenne, la moitié, mais fonctionne sur des données non triées ; une recherche binaire nécessite au maximum $\log_{2} n$ (O($\log n$)) et est beaucoup plus rapide pour de grandes listes, mais les données doivent d'abord être triées et elle doit permettre l'accès direct à l'élément central (un tableau, pas une liste chaînée). Pour $1000$ éléments : $1000$ contre $10$ comparaisons.

Trois lignes montrant une recherche binaire sur l'alphabet trié ; la plage active de bas à haut se réduit de moitié à chaque étape lorsque la lettre centrale M, puis T, puis W est comparée avec W
La recherche binaire réduit la plage de moitié à chaque étape (bas / milieu / haut) — juste 3 comparaisons pour trouver W
Un catalogue de cartes de bibliothèque : un mur de petits tiroirs en bois, dont un est tiré pour montrer les cartes classées par ordre
Un catalogue de cartes : ce sont les enregistrements triés qui rendent possible une recherche binaire — réduisez de moitié, regardez, réduisez de nouveau
Explorer

Recherche linéaire vs binaire

Recherchez une valeur. La recherche dichotomique divise la liste par deux à chaque étape (uniquement sur des données triées) ; la recherche linéaire vérifie un par un.

Vocabulaire Entrainer
Anglais Chinois Pinyin
insertion sort/ɪnˈsɜːʃn sɔːt/ 插入排序 chā rù pái xù
bubble sort/ˈbʌbl sɔːt/ 冒泡排序 mào pào pái xù
binary search/ˈbaɪnəri sɜːtʃ/ 二分查找 èr fēn chá zhǎo
array/əˈreɪ/ 数组 shù zǔ
linear search/ˈlɪnɪə sɜːtʃ/ 线性查找 xiàn xìng chá zhǎo
19.1

Algorithmes de tri

Tri à bulles

Un tri à bulles 冒泡排序 parcourt répétément le tableau, échangeant les paires adjacentes qui ne sont pas dans l'ordre, de sorte que le plus grand élément "bulle" vers la fin à chaque passage :

FOR pass ← 1 TO n - 1
    swapped ← FALSE
    FOR i ← 1 TO n - pass
        IF A[i] > A[i + 1] THEN
            temp ← A[i]
            A[i] ← A[i + 1]
            A[i + 1] ← temp
            swapped ← TRUE
        ENDIF
    NEXT i
    IF swapped = FALSE THEN      // already sorted
        EXIT FOR
    ENDIF
NEXT pass

Cas optimal O($n$) (déjà trié, avec sortie anticipée) ; moyen/mauvais cas O($n^{2}$). Simple mais lent pour de grands $n$.

Tri par insertion

Un tri par insertion 插入排序 construit un préfixe trié à partir de la gauche, insérant chaque nouvel élément à sa place en décalant les éléments plus grands vers la droite :

FOR i ← 2 TO n
    key ← A[i]
    j ← i - 1
    WHILE j >= 1 AND A[j] > key DO
        A[j + 1] ← A[j]
        j ← j - 1
    ENDWHILE
    A[j + 1] ← key
NEXT i

Cas optimal O($n$) (déjà trié) ; pire cas O($n^{2}$). Bon pour les tableaux petits ou presque triés. Il trie in place 原地 et est stable 稳定 (conserve l'ordre des éléments égaux).

Suivi d'un tri

Une tâche courante consiste à montrer le tableau après chaque passe externe. Pour [D, T, H, R] avec le tri par insertion : passe 1 (clé T) pas de changement ; passe 2 (clé H) → [D, H, T, R] ; passe 3 (clé R) → [D, H, R, T].

Écrire un tri de zéro. "Écrivez un pseudocode pour trier DataArray[1:1000] par ordre croissant" est répondu par un tri à bulles complet avec le indicateur de sortie anticipée, ou un tri par insertion, déclaré et indenté ; les deux obtiennent la note maximale s'ils fonctionnent pour toute entrée :

DECLARE Pass, Index, Temp : INTEGER
DECLARE Swapped : BOOLEAN
Pass ← 1
REPEAT
    Swapped ← FALSE
    FOR Index ← 1 TO 1000 - Pass
        IF DataArray[Index] > DataArray[Index + 1] THEN
            Temp ← DataArray[Index]
            DataArray[Index] ← DataArray[Index + 1]
            DataArray[Index + 1] ← Temp
            Swapped ← TRUE
        ENDIF
    NEXT Index
    Pass ← Pass + 1
UNTIL Swapped = FALSE OR Pass = 1000

Pour un ordre décroissant, remplacez > par < ; pour trier des enregistrements ou un tableau 2D par un champ, comparez ce champ mais échangez l'enregistrement entier (ou toutes les colonnes). On vous demande d'écrire un tri par insertion "qui effectue la même tâche" qu'un tri à bulles donné : gardez le même nom de tableau et la même direction et reproduisez le tri par insertion ci-dessus avec la comparaison inversée si l'ordre est décroissant.

"Décrivez deux façons dont les performances d'un tri sont affectées par les données" (deux points). (1) Le nombre d'éléments : un tri $O(n^{2})$ prend quatre fois plus longtemps pour deux fois plus d'éléments. (2) Dans quelle mesure les données sont déjà en ordre : un tri à bulles avec un indicateur, ou un tri par insertion, se termine en une seule passe sur des données déjà triées ($O(n)$) et fait le plus de travail sur des données en ordre inverse ; le nombre d'échanges dépend du nombre de paires désordonnées. (Aussi accepté : la plage ou le nombre de valeurs dupliquées, et si les éléments sont de grands enregistrements coûteux à déplacer.) Les tris à bulles et par insertion sont tous deux O($n^{2}$) dans les pire et cas moyens et O($n$) au mieux ; les tris rapides et par fusion sont O($n \log n$), c'est pourquoi ils sont utilisés pour les grandes données.

Lignes suivant un tri par insertion de D, T, H, R sur trois passages ; le préfixe trié est hachuré et des flèches montrent chaque élément plus grand se décalant vers la droite pour laisser la clé descendre
Un tri par insertion de [D, T, H, R], déplaçant chaque clé à sa place passage par passage
Explorer

Regardez un tri s'exécuter

Parcourez un tri et observez les barres se ranger dans l'ordre — comment fonctionne un algorithme de tri, étape par étape.

Vocabulaire Entrainer
Anglais Chinois Pinyin
in place/ɪn pleɪs/ 原地 yuán dì
stable/ˈsteɪbl/ 稳定 wěn dìng
19.1

ADT dans les algorithmes

Les Types de Données Abstraits (ADT) du Topic 10 apparaissent à l'intérieur de nombreux algorithmes : une pile 栈 guide la traversal en profondeur et l'undo ; une fichier 队列 guide la traversal en largeur et l'ordre d'impression ; une liste chaînée 链表 permet aux données de croître et de diminuer.

Les ADT peuvent être construits à partir d'autres ADT, pas seulement à partir de tableaux : une file à partir de deux piles ; une pile à partir d'une liste chaînée (push = ajouter une tête nœud 节点) ; une file à partir d'une liste chaînée avec des pointeurs de tête et de queue pointers 指针 ; un arbre binaire 二叉树 à partir de nœuds avec deux pointeurs enfants ; un dictionnaire 字典 stocke des paires clé→valeur (souvent sur une table de hachage). Empiler ainsi sépare les préoccupations — l'algorithme utilisant l'ADT n'a pas besoin de savoir comment il est construit.

Les ADT que l'examen vous demande de décrire et d'implémenter

Pile (last in, first out) : les éléments sont ajoutés (pushed) et retirés (popped) à la même extrémité, le sommet ; un pointeur TopOfStack garde l'index de l'élément du sommet. Implémenté avec un tableau et ce seul pointeur : push vérifie que la pile n'est pas pleine, incrémente le pointeur et stocke l'élément ; pop vérifie qu'elle n'est pas vide, retourne l'élément du sommet et décrémente le pointeur.

FUNCTION Push(Item : INTEGER) RETURNS BOOLEAN
    IF TopOfStack = 9 THEN      // full (array 0 to 9)
        RETURN FALSE
    ENDIF
    TopOfStack ← TopOfStack + 1
    StackData[TopOfStack] ← Item
    RETURN TRUE
ENDFUNCTION
FUNCTION Pop() RETURNS INTEGER
    IF TopOfStack = -1 THEN      // empty
        RETURN -1
    ENDIF
    TopOfStack ← TopOfStack - 1
    RETURN StackData[TopOfStack + 1]
ENDFUNCTION

File (first in, first out) : les éléments rejoignent à l'arrière (enfiler) et partent de l'avant (défiler) ; deux pointeurs et un compteur. Dans une file linéaire, le pointeur avant glisse le long du tableau jusqu'à ce que l'espace au début soit gaspillé ; une file circulaire 循环队列 fait tourner les deux pointeurs autour avec MOD, de sorte que chaque cellule est réutilisée.

Une file circulaire de six cellules de tableau contenant trois éléments dans les cellules 3 à 5, avec le pointeur avant à 3 et l'arrière à 5, et une flèche en pointillés montrant que le prochain élément fait le tour dans la cellule 0
Une file circulaire : les pointeurs arrière et avant avancent avec MOD, de sorte que les premières cellules du tableau sont réutilisées une fois leurs éléments partis
FUNCTION Enqueue(Item : STRING) RETURNS BOOLEAN
    IF Count = 6 THEN      // full
        RETURN FALSE
    ENDIF
    Rear ← (Rear + 1) MOD 6
    QueueArray[Rear] ← Item
    Count ← Count + 1
    RETURN TRUE
ENDFUNCTION
FUNCTION Dequeue() RETURNS STRING
    IF Count = 0 THEN      // empty
        RETURN ""
    ENDIF
    DECLARE Item : STRING
    Item ← QueueArray[Front]
    Front ← (Front + 1) MOD 6
    Count ← Count - 1
    RETURN Item
ENDFUNCTION

Liste chaînée : une séquence de nœuds, chacun contenant un élément de données et un pointeur vers le nœud suivant ; un pointeur de départ donne le premier nœud et un pointeur nul (0 ou $-1$) termine la liste. Dans une implémentation par tableau, deux tableaux parallèles contiennent les données et les pointeurs, et les cellules inutilisées sont enchaînées dans une liste libre 空闲列表 de sorte qu'une insertion sait où placer le nouveau nœud.

Deux tableaux parallèles Data et Pointer implémentant une liste chaînée des noms Ann, Ben et Dan : le pointeur de départ est 1, les pointeurs enchaînent 1 à 3 à 2 à 0, et les cellules inutilisées 4, 5 et 6 forment la liste libre
Une liste chaînée dans deux tableaux : l'ordre de la liste est dans les pointeurs, pas dans les positions ; insérer un nom signifie prendre une cellule de la liste libre et relier deux pointeurs
FUNCTION FindInList(Target : STRING) RETURNS INTEGER   // index, or 0 if absent
    DECLARE Current : INTEGER
    Current ← Start
    WHILE Current <> 0
        IF Data[Current] = Target THEN
            RETURN Current
        ENDIF
        Current ← Pointer[Current]
    ENDWHILE
    RETURN 0
ENDFUNCTION

Pour insérer dans une liste ordonnée : prenez la première cellule libre (NewNode ← FreeList, FreeList ← Pointer[FreeList]), stockez l'élément, puis parcourez la liste avec un pointeur Previous et Current jusqu'à Data[Current] > Item ou la fin ; définissez Pointer[NewNode] ← Current et Pointer[Previous] ← NewNode (ou Start ← NewNode si cela va en premier). Pour supprimer, relier le nœud précédent au-delà de celui supprimé et retourner la cellule à la liste libre.

Arbre binaire : un nœud racine, chaque nœud contenant des données, un pointeur gauche vers un sous-arbre de valeurs plus petites et un pointeur droit vers un sous-arbre de valeurs plus grandes. Implémenté comme un tableau 2D (ou trois tableaux 1D) Tree[Index, 0..2] pour le pointeur gauche, les données, le pointeur droit, avec un pointeur racine et un pointeur next-free.

FUNCTION FindInTree(Target : INTEGER) RETURNS INTEGER   // index, or -1
    DECLARE Current : INTEGER
    Current ← Root
    WHILE Current <> -1
        IF Tree[Current, 1] = Target THEN
            RETURN Current
        ENDIF
        IF Target < Tree[Current, 1] THEN
            Current ← Tree[Current, 0]      // go left
        ELSE
            Current ← Tree[Current, 2]      // go right
        ENDIF
    ENDWHILE
    RETURN -1
ENDFUNCTION

Pour insérer : stockez l'élément dans le prochain nœud libre avec les deux pointeurs $-1$ ; si l'arbre est vide, faites-en la racine ; sinon, descendez depuis la racine, allez à gauche ou à droite par comparaison, jusqu'à ce que le pointeur que vous suivriez soit $-1$, et définissez ce pointeur vers le nouveau nœud. Un ADT à partir d'un autre ADT : une pile est une liste chaînée où push et pop fonctionnent tous deux au début ; une file est une liste chaînée avec un pointeur de départ et un pointeur de fin ; une file peut être faite à partir de deux piles (push sur l'une, pop de l'autre, tout déplacer quand la seconde est vide) ; les nœuds d'un arbre binaire sont des enregistrements ou objets liés par des pointeurs, donc il est construit à partir d'une structure de liste de nœuds. Dites quelles opérations du nouveau ADT correspondent à quelles opérations de l'ancien.

Un arbre binaire avec racine 27, un sous-arbre gauche de 19, 16, 21 et 17, et un sous-arbre droit de 36, 42, 89 et 55, avec la racine, les pointeurs gauche et droit, et un nœud feuille étiqueté
Un arbre binaire : chaque nœud a jusqu'à deux nœuds enfants
Un arbre de recherche binaire avec racine 4 (sous-arbre gauche 2 sur 1 et 3, sous-arbre droit 6 sur 5 et 7) ; pré-ordre visite 4 2 1 3 6 5 7, in-ordre 1 2 3 4 5 6 7 (trié), post-ordre 1 3 2 5 7 6 4
Trois traversals en profondeur d'un arbre binaire : pré-ordre, in-ordre (ordre trié) et post-ordre
Vocabulaire Entrainer
Anglais Chinois Pinyin
linked list/lɪŋkt lɪst/ 链表 liàn biǎo
stack/stæk/ 栈 zhàn
queue/kjuː/ 队列 duì liè
node/nəʊd/ 节点 jié diǎn
pointers/ˈpɔɪntəz/ 指针 zhǐ zhēn
binary tree/ˈbaɪnəri triː/ 二叉树 èr chā shù
dictionary/ˈdɪkʃənəri/ 字典 zì diǎn
circular queue/ˈsɜːkjʊlə kjuː/ 循环队列 xún huán duì liè
free list/friː lɪst/ 空闲列表 kòng xián liè biǎo
time complexity/taɪm kəmˈpleksɪti/ 时间复杂度 shí jiān fù zá dù
Big-O notation/bɪɡ əʊ nəʊˈteɪʃn/ 大O表示法 dà O biǎo shì fǎ
space complexity/speɪs kəmˈpleksɪti/ 空间复杂度 kōng jiān fù zá dù
19.1

Comparaison des algorithmes

Complexité temporelle

La complexité temporelle 时间复杂度 est la façon dont le temps d'exécution augmente avec la taille de l'entrée $n$, écrite en notation Big-O 大O表示法 (le terme dominant) : O(1) constant, O($\log n$) recherche binaire, O($n$) recherche linéaire, O($n \log n$) bons tris, O($n^{2}$) tri à bulles/tri par insertion. Un ordre inférieur est meilleur à grande échelle, même si un autre algorithme est plus rapide pour de petites $n$.

Pour rendre cela concret : pour trier un million d'éléments, un tri $O(n \log n)$ se termine en une fraction de seconde, tandis qu'un tri $O(n^{2})$ peut prendre des minutes.

Exemple résolu. Une liste triée contient $1000$ éléments. Combien de comparaisons chaque recherche nécessite-t-elle dans le pire cas ?

Une recherche linéaire vérifie les éléments un par un, donc elle peut nécessiter jusqu'à $1000$ comparaisons — c'est $O(n)$. Une recherche binaire réduit la liste de moitié à chaque étape, donc elle nécessite au maximum $\lceil \log_2 1000 \rceil = 10$ comparaisons — c'est $O(\log n)$. Doubler la liste à $2000$ éléments ajoute seulement non comparaison à la recherche binaire, mais jusqu'à un autre $1000$ à la recherche linéaire — c'est pourquoi l'ordre de croissance, pas la vitesse brute, décide du gagnant à grande échelle.

Décrire un ordre. O(1): le temps est constante, indépendant du nombre d'éléments (ajout dans une pile, lecture d'un élément de tableau). O($\log n$): le temps croît avec le logarithme du nombre d'éléments, donc doubler les données n'ajoute qu'une étape fixe supplémentaire (recherche binaire). O($n$): le temps croît proportionnellement au nombre d'éléments (recherche linéaire, un seul parcours d'une liste). O($n \log n$): un peu pire que linéaire (tri efficaces). O($n^{2}$): le temps croît avec le carré du nombre d'éléments, donc doubler les données quadruple le temps (tri à bulles et tri par insertion). "Donner la complexité Big O d'une recherche binaire de Names[0:99]" est répondu $O(\log n)$, et "décrire sa signification" comme ci-dessus ; la notation Big O mesure comment le temps ou la mémoire s'échelle, pas le temps réel.

Un graphique du temps d'exécution contre la taille de l'entrée n pour les ordres communs : O(1) et O(log n) restent presque plats, O(n) monte doucement, O(n log n) plus raide, et O(n squared) monte le plus vite
Comment les ordres de croissance communs se comparent : un ordre plus petit gagne à grande échelle
Un graphique linéaire du temps d'exécution contre le nombre d'éléments n : le tri à bulles et le tri par insertion montent raide en tant que O(n squared), tandis que quick sort reste bas en tant que O(n log n)
Comment le temps de tri croît avec le nombre d'éléments $n$ : $O(n^2)$ tris s'éloignent d'un $O(n\log n)$ tri

Complexité spatiale

Complexité spatiale 空间复杂度 est la mémoire supplémentaire nécessaire. Le tri à bulles et le tri par insertion utilisent O(1) d'espace supplémentaire (en place) ; le tri fusion utilise O($n$) ; la récursion utilise de la mémoire de pile proportionnelle à sa profondeur. Il existe souvent un compromis temps-mémoire.

Autres critères

Simplicité (plus facile à coder et à maintenir), stabilité et adaptativité (plus rapide sur des données presque triées). Le bon algorithme dépend des données et des contraintes.

Explorer

La croissance du temps d'exécution avec n

Faites glisser n vers le haut et comparez les courbes : O(1) et O(log n) restent presque plats, O(n) monte régulièrement, O(n²) explose. C'est pourquoi le Big-O — et non un chronomètre — est la manière dont nous comparons les algorithmes sur de grandes entrées.

Explorer

Croissance en notation Big-O

Changez la taille de l'entrée n et comparez la vitesse à laquelle le travail de chaque algorithme augmente — l'idée derrière la complexité temporelle.

19.2

Récursivité

Programme
Les candidats doivent être capables de : Notes et orientations
Montrer la compréhension de la récursivité Caractéristiques essentielles de la récursivité Comment la récursivité est exprimée dans un langage de programmation Écrire et tracer des algorithmes récursifs Quand l'utilisation de la récursivité est bénéfique
Avoir conscience de ce qu'un compilateur doit faire pour traduire du code de programmation récursif Utilisation des piles et du déroulement

Source : Programme Cambridge International

Récursivité : la pile d'appels s'enroule et se déroule

Les algorithmes récursifs utilisent la récursivité 递归 : une routine s'appelle elle-même avec une version plus petite du même problème, jusqu'à ce qu'un cas de base 基本情形 arrête la chaîne. Elle comporte deux parties : le cas de base (assez petit pour être résolu directement — sans lui, la récursivité ne s'arrête jamais) et le cas récursif 递归情形 (réduire l'entrée et s'appeler elle-même).

Factorielle 阶乘:

FUNCTION Factorial(n : INTEGER) RETURNS INTEGER
    IF n = 0 OR n = 1 THEN
        RETURN 1
    ELSE
        RETURN n * Factorial(n - 1)
    ENDIF
ENDFUNCTION

La récursivité est naturelle pour les problèmes auto-similaires : arbres, diviser pour régner 分治 (recherche binaire, tri fusion) et structures de données imbriquées. Lorsqu'elle est mal adaptée, une boucle est généralement plus propre.

"Décrivez ce que signifie la récursivité" (deux points). Une fonction ou une procédure définie en termes d'elle-même : elle s'appelle elle-même depuis son propre corps, avec une version plus petite du problème à chaque fois, jusqu'à ce qu'un cas de base soit atteint. "Énoncez trois caractéristiques essentielles de la récursivité" : (1) un cas de base (condition d'arrêt) qui retourne une valeur sans appel ultérieur ; (2) un cas général 一般情形 dans lequel la routine s'appelle elle-même ; (3) chaque appel rapproche le problème du cas de base (le paramètre est réduit), afin que la récursivité se termine. Certains schémas ajoutent : les valeurs sont retournées lorsque les appels se déroulent.

"Décrivez quand l'utilisation de la récursivité est bénéfique, et donnez un exemple." Lorsque le problème est défini naturellement en termes de versions plus petites de lui-même, de sorte que la solution récursive est plus courte, plus claire et plus proche de la définition mathématique qu'une boucle ne pourrait l'être : factorielle ou nombre de Fibonacci, recherche binaire, parcours d'un arbre binaire, tri fusion ou quicksort, et traitement de structures imbriquées telles que des dossiers contenant des dossiers. C'est un mauvais choix lorsque la profondeur est grande (la pile peut déborder) ou lorsqu'un sous-problème identique est calculé de nombreuses fois (Fibonacci naïf).

Tracé d'un appel récursif

Pour Factorial(4) : les appels descendent jusqu'à Factorial(1)=1, puis le déroulement multiplie vers le haut : 2*1=2, 3*2=6, 4*6=24. Résultat final 24. Suivez chaque appel en attente sur une pile.

Exemple corrigé. La fonction ci-dessous est donnée sans explication. Tracez Unknown(3, 5) et indiquez sa sortie et sa valeur de retour.

FUNCTION Unknown(BYVAL X, BYVAL Y : INTEGER) RETURNS INTEGER
    IF X < Y THEN
        OUTPUT X + Y
        RETURN Unknown(X + 1, Y - 1) + 1
    ELSE
        RETURN 0
    ENDIF
ENDFUNCTION

Appel 1 : $X = 3, Y = 5$ : $3 < 5$, sortie 8, appel Unknown(4, 4). Appel 2 : $4 < 4$ est faux, retour 0. Déroulement : l'appel 1 retourne $0 + 1 = 1$. Sortie 8, valeur de retour 1. Écrivez le tracé sous forme de tableau avec une ligne par appel (paramètres, condition, sortie, ce qu'il retourne), et effectuez les retours depuis l'appel le plus profond vers le haut : c'est-à-dire le déroulement que cherche le barème.

Exemple corrigé (Fibonacci). Fib(n) retourne n lorsque n < 2, sinon Fib(n - 1) + Fib(n - 2). Trouvez Fib(5).

Fib(5) = Fib(4) + Fib(3) ; Fib(4) = Fib(3) + Fib(2) ; Fib(3) = Fib(2) + Fib(1) ; Fib(2) = Fib(1) + Fib(0) = 1 + 0 = 1. Donc Fib(3) = 1 + 1 = 2, Fib(4) = 2 + 1 = 3, Fib(5) = 3 + 2 = 5. Le cas de base est atteint beaucoup de fois (Fib(2) est calculé trois fois), c'est pourquoi cette version est lente : elle effectue 15 appels pour $n = 5$ et double approximativement le nombre d'appels pour chaque augmentation de $n$.

Conversion de la récursivité en itération. Chaque routine récursive peut être réécrite avec une boucle, ce qui utilise moins de mémoire et est plus rapide : conservez un résultat accumulé et boulez du cas de base vers le haut. Factorielle sous forme de boucle :

FUNCTION Factorial(N : INTEGER) RETURNS INTEGER
    DECLARE Result, Count : INTEGER
    Result ← 1
    FOR Count ← 2 TO N
        Result ← Result * Count
    NEXT Count
    RETURN Result
ENDFUNCTION

Interpellé pour convertir un tri par insertion ou une recherche récursive en une version itérative, remplacez l'appel à soi-même par une boucle sur l'index que la récursivité parcourait, et transformez le cas de base en condition de sortie de boucle.

La pile d'appels pour Factoriel(4) : chaque appel pousse une frame jusqu'au cas de base Factoriel(1)=1, puis la pile se dépile, renvoyant 2 = 2 fois 1, 6 = 3 fois 2 et 24 = 4 fois 6
La récursivité utilise la pile d'appels : les appels poussent des trames vers le bas jusqu'au cas de base, puis les retours se déroulent vers le haut

Risques

  • récursivité infinie si le cas de base est manqué — plantage avec une débordement de pile 栈溢出.
  • forte consommation de mémoire pour une récursivité profonde.
  • lent si cela répète des travaux (Fibonacci naïf est exponentiel — utilisez une boucle ou la mémoïsaion 记忆化).
Explorer

La récursivité se déroule depuis les feuilles vers le haut

Parcourir fib(4) dans l'ordre où les appels se terminent réellement : les feuilles (cas de base) se résolvent d'abord, puis chaque parent combine ses enfants. Remarquez que fib(2) est calculé deux fois — ce travail répété explique pourquoi la récursivité naïve est lente.

Vocabulaire Entrainer
Anglais Chinois Pinyin
recursion/rɪˈkɜːʃn/ 递归 dì guī
call stack/kɔːl stæk/ 调用栈 diào yòng zhàn
base case/beɪs keɪs/ 基本情形 jī běn qíng xíng
recursive case/rɪˈkɜːsɪv keɪs/ 递归情形 dì guī qíng xíng
factorial/fækˈtɔːrɪəl/ 阶乘 jiē chéng
divide-and-conquer/dɪˈvaɪd ænd ˈkɒŋkə/ 分治 fēn zhì
general case/ˈdʒenərəl keɪs/ 一般情形 yì bān qíng xíng
parameters/pəˈræmɪtəz/ 参数 cān shù
stack overflow/stæk ˌəʊvəˈfləʊ/ 栈溢出 zhàn yì chū
memoisation/ˌmeməʊaɪˈzeɪʃn/ 记忆化 jì yì huà
local variables/ˈləʊkl ˈveərɪəblz/ 局部变量 jú bù biàn liàng
stack frame/stæk freɪm/ 栈帧 zhàn zhēn
return address/rɪˈtɜːn əˈdres/ 返回地址 fǎn huí dì zhǐ
19.2

Ce que fait le compilateur pour le code récursif

La récursivité nécessite que chaque appel dispose de sa propre copie de ses paramètres 参数 et variables locales 局部变量. Le compilateur conserve ces éléments sur la pile d'appels 调用栈. Pour chaque appel, il pousse une trame de pile 栈帧 contenant les paramètres, les variables locales et l'adresse de retour 返回地址 (où reprendre chez l'appelant). Quand la fonction retourne, la valeur de retour est rendue, la trame est retirée, et le contrôle reprend à l'adresse de retour.

Parce que chaque appel a sa propre trame, les appels récursifs ne s'écrasent pas mutuellement leurs variables. La pile peut devenir grande pour une récursivité profonde, ce qui explique pourquoi une récursivité très profonde peut la faire déborder. C'est le même mécanisme d'appel et de retour utilisé pour les appels ordinaires (non récursifs) — il n'y a pas de "mécanisme spécial de récursivité".

"Expliquez pourquoi une pile est appropriée pour implémenter la récursivité" (trois points). Chaque appel récursif doit sauvegarder son adresse de retour, ses paramètres et ses variables locales, et les appels sont terminés dans l'ordre inverse de celui dans lequel ils ont été faits (le dernier appel fait est le premier à terminer), ce qui correspond exactement au comportement dernier entré, premier sorti d'une pile : chaque nouvel appel pousse une trame, et chaque retour retire la trame la plus récente, restaurant l'état de l'appelant et lui indiquant où continuer. C'est le travail du compilateur lorsqu'il traduit le code récursif : il génère la poussée d'une trame de pile à chaque appel et le retrait à chaque retour, et les trames se déroulent alors que les résultats reviennent.

19.2

Définitions acceptées par l'examinateur

Une question de définition est notée selon un libellé fixe. Apprenez-les exactement et ne donnez qu'une seule réponse.

Terme Définition
recherche linéaire vérifier chaque item à tour de rôle depuis le début jusqu'à ce que la cible soit trouvée ou la fin atteinte
recherche binaire comparer répétitivement la cible avec l'élément central d'une liste triée et éliminer la moitié qui ne peut pas la contenir
tri à bulles passer répétitivement à travers la liste, échanger les éléments adjacents qui sont dans le mauvais ordre, jusqu'à ce qu'un passage ne fasse aucun échange
tri par insertion prendre chaque élément successivement et l'insérer à sa place correcte parmi les éléments déjà triés
type de données abstrait un ensemble de données et les opérations qui peuvent y être effectuées, défini indépendamment de la manière dont il est stocké
pile une structure dernier entré, premier sorti avec push et pop en haut
fichier une structure premier entré, premier sorti avec des éléments ajoutés à l'arrière et retirés à l'avant
liste chaînée une séquence de nœuds, chacun contenant des données et un pointeur vers le nœud suivant, avec un pointeur de départ
arbre binaire des nœuds contenant chacun des données et des pointeurs vers un sous-arbre gauche de valeurs inférieures et un sous-arbre droit de valeurs supérieures
notation Big O une façon de classifier le temps (ou la mémoire) nécessaire par un algorithme selon sa croissance avec la taille de l'entrée
récursivité une routine qui s'appelle elle-même avec une version plus petite du problème jusqu'à ce qu'un cas de base arrête les appels
cas de base la condition selon laquelle une routine récursive retourne sans s'appeler elle-même
déroulement les retours d'une chaîne d'appels récursifs, du dernier appel au premier, alors que les trames de pile sont retirées
19.2

Conseils d'examen

  • Recherches : la recherche linéaire ne nécessite aucun ordre et O($n$) ; la recherche binaire nécessite un tableau trié, divise par deux à chaque étape et est en O($\log n$). Connaître les deux algorithmes par cœur, y compris les bornes et le drapeau.
  • Tries : tri à bulles avec indicateur d'échange, tri par insertion avec une clé qui décale les éléments plus grands vers la droite ; tous deux en O($n^{2}$) dans le pire des cas, O($n$) sur des données déjà triées. Les performances dépendent du nombre d'éléments et de leur degré de tri.
  • Les implémentations de TDA sont une gestion de pointeurs : un pointeur supérieur ; avant, arrière et compteur avec MOD ; début, pointeurs et liste libre ; racine avec pointeurs gauche et droit. Toujours vérifier les états plein et vide.
  • Big O concerne l'échelle : constant, logarithmique, linéaire, carré. Dire "doublester les données ajoute une comparaison" pour une recherche binaire.
  • Récursivité : cas de base, cas général, progression vers le cas de base ; bénéfique lorsque le problème est défini en termes de lui-même ; une pile contient les adresses de retour et les variables car les appels retournent dans l'ordre inverse. Tracez avec un tableau et déroulez depuis l'appel le plus profond.

Erreurs courantes

  • Utiliser une recherche binaire sur des données non triées, ou sur une liste chaînée ; et définir Lower ← Mid au lieu de Mid + 1, ce qui boucle indéfiniment.
  • Une boucle intérieure de tri à bulles qui parcourt toute le tableau à chaque passe, ou un échange sans variable temporaire.
  • Un push ou enqueue qui ne teste pas l'état plein, ou un pop ou dequeue qui ne teste pas l'état vide.
  • Déplacer le pointeur avant de la file sans MOD dans une file circulaire, ou considérer front = rear comme signifiant toujours vide.
  • Insérer dans une liste chaînée en décalant le contenu du tableau ; seuls les pointeurs changent.
  • Une fonction récursive sans cas de base, ou dont l'appel récursif ne rend pas le problème plus petit.
  • Tracer un appel récursif mais oublier d'ajouter le travail en attente sur le chemin retour.
  • Répondre "pourquoi une pile" par "parce que c'est rapide" ; la raison est l'ordre dernier entré, premier sorti des retours.

Leçons interactives sur ce sujet

Traversez-le étape par étape, avec des exercices à vérification instantanée.

Épreuves Passées

Plus de sujets dans Informatique A-Level

Se connecter ou créer un compte

IGCSE, A-Level & AP