Algorithmique et structures de données

sdefelice(at)up8.edu


1. Rappels de C

Pointeurs

Un pointeur sur un type a de taille n octet est une variable qui contient l’adresse d’une variable de type a (adresse sur a).

Arithmétique du pointeur

Si a est un type de taille m et b est une valeur de type a*, alors, en mémoire, b+i pointe sur le i^{ème} groupe de n octet à “droite”.

Les zones mémoires d’un processus

Un processus a accès à 3 “zones de mémoire”.

Les fonctions

Structures

/

2. Domination de suite

Cf. TD1reponses

Le nombre d’opérations d’un algorithme pour résoudre un problème dépend des données du problème. Afin de caractériser l’efficacité d’un algorithme, on étudie la suite qui lie la taille des données du problème avec le nombre d’opérations de l’algorithme.

Définition (suite numérique)

Une suite numérique est une application de \N dans \R.

Exemple

Vocabulaire

On peut décrire la suite avec une formule explicite ou avec une formule récursive.

Exemple

Formules explicite :
\ u : n \mapsto\ \ 3 : 3, 3, 3, 3
\ v : n \mapsto\ \ n : 0, 1, 2, 3
w : n \mapsto 2n : 0, 2, 4, 6, 8
\ r : n \mapsto 2^n\ : 1, 2, 4, 8, 16, 32

V_{n} = \begin{cases} & 0 \text{ si } n = 0 \\\\ & V_{n-1} \text{ si } n \neq 0 \end{cases}

W_n = \begin{cases} & 0 \text{ si } n = 0 \\\\ & W_{n-1}+2 \text{ si } n \neq 0 \end{cases}

R_n = \begin{cases} & \text{ si } n = 0 \\\\ & R_{n-1} \text{ si } n \neq 0 \end{cases}

Définition (Domination)

Soient U et R deux suites numérique, on dit que U domine R si :
\exists\ M > 0, M \in \R, \exists\ n \in \N, \forall_n > n_0, |R_n| \leq M.|U_n|

On note R = O(u) ou R \ll U (celle qui domine est celle qui croît le plus vite)
On note \Omega(U) l’ensemble des suites qui domine U.
U \in O(D)

Propriété

Si U, V, W sont des suites et m, w des nombres réels, alors :

Remarque

Si U, F, W sont des suites, alors :

Notation

Si U et V sont des suites et que on a U\ll V et V\ll U on note U = \Theta(U) (ou V =\Theta(U)) -> c’est une relation d’équivalence.

Exemple

3. Terminaison, correction et complexité d’un algorithme

Cf. TD1reponses

Exemple de tri “à bulle”

Entrée : Tableau d’entiers T de taille m
Sortie : Tableau d’entiers G triés selon l’ordre croissant. Il faut que G contienne les “mêmes” entiers que T.

void trie_a_bulle(int * T, int m) {
    int bool1 = 0;
    for(int i = 0; i < m; i++) {
        if (T[i] > T[i + 1]) {
            bool1 = 1;
            int tmp = T[i + 1];
            T[i + 1] = T[i];
            T[i] = tmp;
        }
    }
    if (bool1 != 0) return trie_a_bulle(T, m--);
}

4. Techniques de structuration de la mémoire

Exemple de données

Les données doivent être organisées dans la mémoire machine qui n’est qu’un grand tableau contenant des cases de 8 bits associées à des adresses.

Illustration

illustration

Afin de structurer ses données dans la mémoire on utilise deux techniques (souvent simultanément).

0,0 1,0 2,0 3,0 4,0 5,0 6,0 7,0 8,0
0,1 1,1 2,1 3,1 4,1 5,1 6,1 7,1 8,1
0,2 1,2 2,2 3,2 4,2 5,2 6,2 7,2 8,2
0,3 1,3 2,3 3,3 4,3 5,3 6,3 7,3 8,3
0,4 1,4 2,4 3,4 4,4 5,4 6,4 7,4 8,4
0,5 1,5 2,5 3,5 4,5 5,5 6,5 7,5 8,5
0,6 1,6 2,6 3,6 4,6 5,6 6,6 7,6 8,6
0,7 1,7 2,7 3,7 4,7 5,7 6,7 7,7 8,7
0,8 1,8 2,8 3,8 4,8 5,8 6,8 7,8 8,8

Donne :

0,0 1,0 2,0 8,0 0,1 8,8
case(i, j) = tableau[i + (9 * j)]
tableau[k] = case(k % 9, k / 9)

schema labyrinthe

5. Liste chaînée

Une liste chaînée est composée de cellule avec deux champs.

Illustrations

illustration_listeChainees

#define <stdlib.h>

typedef struct cel {
    int valeur;
    struct cel * suivant;
} Cellule;

typedef Cellule * Liste; // liste est synonyme de cellule*


Cellule * creerCellule(int a) {
    cellule * p = malloc(sizeof(cellule));
    if(p == NULL) return exit(1);
    (*p).valeur = a; // p -> suivant = a est équivalent
    (*p).suivant = NULL; // ne pointe sur rien

    return p; // on retourne l'adresse
}

exemple en image du code

Ajouter une liste

void ajouterListe(Liste * l, int a) {
    cellule * c = creerCellule(a);
    c -> suivant = *l;
    *l = c;
}

Supprimer une liste

Pour cela on prend le premier élément de la liste et on le supprime et on le rend NULL jusqu’à mettre le dernier élément = NULL.

void suppListe(Liste * l) { // version récursive - sans boucle
    if(*l == NULL) return;
    suppListe(l(*l) -> suivant);
    free(*l);
    *l = NULL;
}

Rechercher dans une liste

int chercheListe(Liste l, int val) {
    if(l == NULL) return 0; // cas où la liste est vide
    if(l -> valeur == val) return 1;
    chercheListe(l -> suivant, val);
}

6. Arbres : parcours en profondeurs

Cf. TD2

Un arbre enraciné (ou arborescence) est une structure formelle que l’on rencontre dans de nombreuse situations. Les arbres sont souvent représenté avec la racine en haut et les feuilles en bas.

Illustrations

graph TD;
/-->home & proc & dev & foot;
home-->login1 & login2 & key;
login1-->docs & vinte;

Arbre binaire étiqueté par des nombre :

graph TD;
3-->2 & 5;
2-->1;
5-->6 & 20;
20-->7;

Arbre binaire non étiqueté :
ArbreBNE_exemple_explication

On peut caractériser un arbre par une définition récursive :

Définition (arbre binaire non étiqueté, ABNE)

Un ABNE est :

Exemple

exemple_arbre_BNE

Définition (plus symbolique et équivalente) :

Arbre BNE := arbreVide | ArbreBNE * ArbreBNE

Définition (Arbre Binaire étiqueté par des entiers)

Arbre BE := arbreVide | ArbreBE * ArbreBE * ℤ

Exemple

exemple_arbre_BE

En C

typedef struct noeud {
    int etiquette;
    struct noeud * arbreGauche;
    struct noeud * arbreDroit;
} Noeud;


typedef Noeud * Arbre;

Arbre: a = NULL; // a est l'arbre vide

schema_C_arbreBE

Parcours d’arbre

Pour représenter un arbre par une liste de mots (ou de nombres).

Parcours en préfixé

On peut *(re)*fabriquer un arbre à partir de son parcours en préfixé.

Parcours en infixé :

Exemple

exemple_parcours_arbre_infixe
Un parcours infixé de cet arbre donne : \perp 2 \perp 1 \perp

exemple_parcours_arbre_infixe2
Un parcours infixé de cet arbre donne : \perp 2 \perp 1 \perp
==> Deux arbre différents peuvent donner le même parcours infixé.

Parcours suffixe

7. Pile, File, Liste chainée circulaire, Parcours en largeur d’arbres

Cf. TD3

Une pile (ou une file) est un conteneur d’objet qui mémorise l’ordre dans lequel les objets ont été déposés.

Lorsque l’on retire un objet d’une pile on retire le dernier objet qui a été déposé

Exemple en C

void empiler(Pile * p, int i); // ajoute i à la pile
int depiler(Pile * p); // renvoie le dernier élément de la pile
int estVideP(Pile p); // renvoie 1 si la liste est vide
/*
    Aussi probablement une fonction qui initialise
    une pile et une autre qui libère la pile.
*/
Pile p:
  // TODO: instructions initialisation de p
  int a;
  // ici -> état 0
  empiler(&p, 1); // -> état 1
  empiler(&p, 2); // -> état 2
  a = depiler(&p); // -> état 3 | a = 2
  a = depiler(&p); // -> état 4 | a = 1 et la pile est vide
  // TODO: instructions détruire la pile

exemple_pile

Lorsque l’on retire un objet d’une file on retire le premier objet qui a été déposé

Exemple en C

void enfile(File * f, int i); // ajoute i à la file
int defile(File * f); // renvoie la tête de liste
int estVideF(file f); // renvoie 1 si la liste est vide
/*
    Aussi probablement une fonction qui initialise
    une file et une autre qui libère la file.
*/
File f:
  // TODO: instructions initialisation de p
  int a;
  // ici -> état 0
  enfiler(&p, 1); // -> état 1
  enfiler(&p, 2); // -> état 2
  a = defiler(&p); // -> état 3 | a = 1
  a = defiler(&p); // -> état 4 | a = 2
  // TODO: instructions détruire la pile

exemple_file

Exemple d’un arbre

flowchart TD
1([1])---2([1]) & 3([3])
2---4([1])---7([4])
3---5([2]) & 6([7])
6---8([1])---9([2])

Parcours en largeur : 7, 1, 3, 1, 2, 7, 4, 1, 2

Exemple en C pour l’utilisation d’une file dans un arbre

// définition de Arbre précédemment dans le cours
void parcours_en_largeur(Arbre a) {
    File_Arbre f; // instanciation de la file
    initialiser(&f); // initialisation de la file
    enfiler(&f, a); // enfiler(File_Arbre * f, Arbre a); | on enfile des arbres
    while(!estVideF(f)) { // tant que la file n'est pas vide
        a = defiler(&f);
        if(a != NULL) {
            printf("%d ", a); // affichage de l'étiquette a
            enfiler(&f, a -> arbreGauche); // on enfile le fils gauche
            enfiler(&f, a -> arbreDroit); // on enfile le fils droit
        }
    }
    detruireFile(&f); // on détruit la file
}

Implantation en C

Manière 1

Manière 2

typedef struct cela {
    Arbre a;
    struct cel * suivant;
} CelluleA;


typedef struct cela * ListeA;
ListeA a = NULL;

int estVideF(FileA f) {
    return f == NULL;
}

void enfiler(FileA * f, Arbre a) {
    CelluleA * c = (celluleA*)malloc(sizeof(CelluleA));
    c -> a = a; // ici la cellule pointe sur elle-même
    if(*f == NULL) {
        c -> suivant = c;
        *f = c;
        return;
    }
    temp = (*f) -> suivant;
    (*f) -> suivant = c;
    c -> suivant = temp;
    (*f) = c;
}

int defiler(FileA * f);

File_arbre.c :

#include<stdlib.h>

// on suppose que le type Arbre est défini

typedef struct cellulA {
   Arbre a;
   struct cellulA* suivant;
} CelluleA;

typedef CelluleA* FileA;

int estVideFA(FileA f) {
   return NULL == f;
}
void enfiler(FileA* f,Arbre a) {
   CelluleA * c = malloc(sizeof(CelluleA));
   c -> a = a;
   if(c == NULL) exit(EXIT_FAILURE);
   if(*f == NULL) {
      c - >suivant=  c;
      *f = c;
   } else {
      c -> suivant = (*f) -> suivant;
      (*f) -> suivant = c;
      *f = c;
   }
}

int main() {
    return 0;
}

8. Ensemble et dictionnaire

Valeur ou objet

Une valeur est quelque chose de théorique, alors qu’un objet existe en mémoire.
Plusieurs objets différents peuvent avoir une même valeure.

Exemple

int a;
// ...
a = 3; // a ici est l'objet
c = a * 2; // a ici est la valeure

Arbre a = ass(anul(), anul(), 3);
Arbre b = ass (a, a, 4);

représentation mémoire

Ensemble

Un ensemble est un conteneur qui mémorise des valeurs.

Exemple (ensemble d’entiers)

Ensemble * e = initialiser();
ajouter(e, 4);
ajouter(e, 3);
estDans(e, 3); // doit renvoyer 1
estDans(e, 5); // doit renvoyer 0
supprimer(e, 3);
estDans(e, 3); // doit renvoyer 0
detuire(e);

Implémentation (d’ensemble)

“Dictionnaire”

En plus d’un ensemble, chaque valeure mémorisée est associée à un objet.

9. ABR (Arbre binaire de recherche)

Cf. TD4

Définition (ABR)

Soit un arbre binaire étiqueté par des valeurs qui peuvent être toujours comparées entre elles (i.e les valeurs sont donc des éléments d’un ensemble totalement ordonnés, par exemple les entiers).

On dit que c’est un arbre binaire de recherche si il vérifie :

Exemple

8 3 16 17 23 2 5 3 1 9 7
En partant d’un arbre vide, on veut ajouter la liste de nombres.
On commence par 8, on l’ajoute (ass(anul(), anul(), 8)
Ensuite on ajoute 3, on l’ajoute en suivant la définition de l’ABR, donc on l’ajoute sur la gauche.
Ensuite pour le 16, en comparant 8 et 16 on se rend compe que 16 est plus grand que 8 donc on va a droite et on le met sur la case vide.
Pour 17, on va a droite car 8 < 17 et 16 < 17.
Etc, on procède récursivement jusqu’à atteindre cet arbre :
exemple ABR

Remarque

Selon l’ordre d’ajout des éléments on peut obtenir des arbres différents, par exemple des arbres “file de fer”
exemple file de fer

Définition (profondeur d’un nœud)

Dans un arbre, la profondeur d’un noeud est le nombre de branche parcourues par un chemin entre la racine et ce nœud.

Définition (hauteur d’un arbre)

La hauteur d’un arbre est le maximum des profondeurs de ses nœuds.
exemple ABR profondeur
Le nombre maximale de nœuds d’un arbre de hauteur h est : 2^{h+1}-1

hauteur nb de nœud
0 1
1 3
2 7
h 2^{\ hauteur+1}-1

Implantation en C

typedef struct noeudABR {
    int entier;
    struct noeudABR * g;
    struct noeudABR * d;
    void * objet; // utile pour les dictionnaires
    int hauteur; // utile pour équilibrer les arbres (voir chapitre 10)
} NoeudABR;
typedef NoeudABR * ABR;

void ajouter(ABR * arbre; int val) {
    if (*arbre == NULL) {
        *arbre = (NoeudABR*)malloc(sizeof(NoeudABR));
        if (*arbre == NULL) exit(1);
        (*arbre) -> entier = val;
        (*arbre) -> g = NULL;
        (*arbre) -> d = NULL;
    } else {
        if ((*arbre) -> entier < val) {
            ajouter(&(*arbre) -> d, val);
        }
        if ((*arbre) -> entier > val) {
            ajouter(&(*arbre) -> g, val);
        }
        /*
            Dans cet exemple, on ne fait rien si on a une égalité
            entre `(*arbre) -> entier` et `val`.
        */
    }
}

Pour retirer une racine, on doit mettre à la place de l’arbre qu’on retire quelque chose :

-> On récupère la valeur maximale de l’arbre et on met cette dernière à la place de la racine (dans ce cas là)
explication extraction d’une racine

NoeudABR * ExtraireMax(ABR * a);
void       retirer(ABR * a, int val);

10. AVL (ABR équilibré)

Cf. TD5

On souhaite améliorer nos ABR et éviter d’avoir des arbres trop “allongés” (genre file de fer) -> on veut des arbres avec une hauteur pas trop grande pour une taille donnée.

Définition

Taille d’un arbre

La taille d’un arbre est le nombre de noeud

Arbre binaire équilibré

On dit qu’un arbre binaire équilibré si :

Théorème

La hauteur d’un arbre équilibré de taille n est inférieure à 2\times log_e(n).


On souhaite utiliser et maintenir des ABR équilibrés. Pour cela on conserve dans chaque noeud A la hauteur du sous-arbre dont la racine est le noeud A. À chaque ajout ou suppression d’un noeud, s’il provoque un déséquilibre, on effectuera une “rotation d’arbre” pour le rendre équilibré.

Définition

AVL

Un AVL (nom des concepteurs) est un ABR (Arbre Binaire de Recherche) équilibré.

Après un ajout simple ou une suppression simple dans un AVL, s’il y a déséquilibre la différence de hauteur ne peut-être plus de 2.

Rotation

Rotation

Remarques

Une rotation conserves :

Sur la hauteur

Hauteur

Remarques

Chaque ajout ou suppression entraînera soit :

Amélioration

La hauteur mémorisé peut-être remplacé par la différence de hauteur entre le sous-arbre droit et celui de gauche. (-1, 0, -1, soit 2 bits).

Preuves

Arbre complet

Définition

Un arbre complet de hauteur h est un arbre qui contient le maximum de noeud pour sa hauteur.
Arbre Complet

Propriété

La taille d’un arbre complet de hauteur h est 2^{h+1}-1

Hauteur complète

Définition

Soit un arbre A sa hauteur complète la hauteur de son plus grand sous-arbre :

Exemple

Exemple hauteur complète

Lemme

La hauteur complète d’un arbre équilibré de hauteur h est supérieur à \dfrac{h}{2}

Preuve du théorème

Soit A un arbre équilibré de taille n et de hauteur h, alors selon le lemme :
2^{\frac{h}{2}+1} \leq n \leq 2^{h+1}
\frac{h}{2}+1\leq log_2(n) \leq h+1
h+2 \leq 2log_2(n) \leq 2h+2
=> Donc la hauteur de A est inférieure à 2\times log_2(n)

Preuve du lemme

Par réccurence sur la hauteur
Si h = 1, on suppose la propriété vrai jusqu’à h \leq n
Preuve
Soit C un hauteur n+1 :

11. Graphe

graph LR;
1-->2 & 3 & 5 & 8;
2-->9;
3-->5;
4-->3;
5-->6 & 7 & 8;
6-->4 & 7;
7-->6 & 7;
8-->1 & 7 & 9;
9-->7 & 9;

En mémoire, on peut représenter un graphe par une matrice d’adjacence :

\nearrow 1 2 3 4 5 6 7 8 9
1 0 1 1 0 1 0 0 1 0
2 0 0 0 0 0 0 0 0 1
3 0 0 0 0 1 0 0 0 0
4 0 0 1 0 0 0 0 0 0
5 0 0 0 0 0 1 1 1 0
6 0 0 0 1 0 0 1 0 0
7 0 0 0 0 0 1 1 0 0
8 1 0 0 0 0 0 1 0 1
9 0 0 0 0 0 0 1 0 1

En C :

#define MAX 100
typedef struct gra {
    int graphe[MAX][MAX];
    int nbSommets;
} Graphe;

On peut représenter en mémoire un graphe par des listes d’adjacences :
1: 2, 3, 5, 8
2: 9
3: 5
Utile lorsque le nombre d’arcs du graphe est \ll n^2 (n le nombre de sommets).

Remarques

Le schéma précédent illustre un graphe orienté (arc avec un sens). On rencontre aussi des graphes non-orientés (arrêtes).

Vocabulaire

Chemin dans un graphe : Longueur d’un chemin.
Circuit : Dans le graphe ci-dessous, 2 et 3 forment un circuit (je crois?).

graph LR
1-->2
2-->3
3-->2 & 5

Parcours en profondeur dans un Graphe

graph LR;
1-->2 & 7;
2-->9;
3-->1 & 5;
4-->3;
5-->6 & 7;
6-->4 & 7;
7-->8;
8-->1 & 7 & 9;
9-->7 & 9;
// On suppose la structure en C précédente

void estAccessible(Graphe * g, int s, int * tab);

/*
`sd` Sommet de Départ et `sa` Sommet d'Arrivé
On prend *g et pas g pour éviter de recopié le graphe (car il est grand)
donc on prend son adresse.
*/
int chemin(Graphe * g, int sd, int sa) {
    int T[g->nbSommets]; // indique les sommets visités
    for(int i = 0; i < g->nbSommets; i++)
        T[i] = 0;
    estAccessible(g, sd, T);

    return T[sa];
}

/*
`tab` est un tableau contenant au départ que des cases vides à 0
et la fonction met à 1 les cases dont les indices sont les sommets
accessibles par un chemin partant du sommet `s`.
*/
void estAccessible(Graphe * g, int s, int * tab) {
    if(tab[s] == 1) // Si `tab[s]` à déjà été visité
        return;
    tab[s] = 1;
    // Ensuite on parcours tout les successeurs possibles
    for(int i = 0; i < g->nbSommets; i++)
        if(g->graphe[s][i] == 1)
            return estAccessible(g, i, tab); // on continue le parcours à partir de `i`
}

Remarque

On pourrait améliorer la fonction en la quittant lorsque le sommet d’arrivé sa est atteint.

Trouver les circuits

graph TD
A[ ]-->B & C;
B[ ]-->D & E;
C[ ]-->E;
D[ ]-->F;
E[ ]-->D & F;
F[ ]
graph TD
A[ ]-->B & C;
B[ ]-->C & D;
C[ ]-->E;
D[ ]-->C & F;
E[ ]-->D & F;
F[ ]

Définition de pré-visite, post-visité et revisité

Lors d’un parcours en profondeur, chaque sommet peut-être soit :

Graphe (étiqueté)

Un graphe étiqueté est un graphe où chaque arc possède une étiquette (un nombre, une lettre…)

Vocabulaire (longueur d’un chemin d’un graphe étiqueté)

Par convention, la longueur d’un chemin dans un graphe étiqueté par des nombres est la somme des étiquettes des arcs empruntés par le chemin. La longueur du chemin vide est nulle.

Exemple

graph LR
1==2==>2
1--1-->3
2==3==>4
3--6-->2
4==2==>5

La longueur de 1, 2\rightarrow 2, 2, 2\rightarrow4, 4, 4\rightarrow5, 4, 4\rightarrow5, 5 est 2+3+2=7

Définition (plus court chemin et distance)

Soit un graphe étiqueté par des nombres et deux sommets d et a du graphe.
S’il existe un chemin allant de d à a qui possède la plus petite longueur parmis les chemins allant de d à a alors ce chemin est appelé le plus “court” chemin allant de d à a et sa longueur est appelée la distance de d à a.

Exemple

graph LR
1==1==>2
1--9-->3
2==1==>3

Ici la distance de 1 à 3 vaut 2 et le plus court chemin est 1, 2, 3

Contre-Exemple

graph LR
1--1-->2
1-- -1 -->1
graph TD
3
4

Dans ces 2 cas il n’y a pas de plus court chemin.
On pourrait dire que :

Problème algorithmique

Entrée: graphe `g`, `d`, `a` deux sommets de `g`
Sortie: plus court chemin (s'il existe) allant de `d` vers `a`
Illustration

illustration algorithme

Propriété

Soit un graphe G étiqueté par des nombres positifs ou nuls et d’un sommet
Si pour un sommet a on appelle C_a le plus court chemin allant de d vers a, alors :
C_{a} = C_{p} \circ p\rightarrow a
avec p une puissance de a et C_p le plus court chemin allant de d vers p
b(C_{p)} + l(p\rightarrow a) = \min(l(C_{s)} + l(s\rightarrow a),\ t_{q})\ \text{$s$ prédésseur de $a$}
avec l(C_{p}) longueur du chemin C_{p} et l(p\rightarrow a) étiquette de l’arc allant de p\rightarrow a. Si de plus d = a il faut prendre en compte le chemin vide.

Dorénavant les étiquettes seront positives ou nulles.

Algorithme de Dijkstra

Entrée: graphe `g` étiquetté, `d` sommet
Sortie: Pour chaque sommet accessible à partir de `d` : le plus court chemin allant de `d` à `i` et la distance de `d` à `i`
Illustration

illustration algorithme dijkstra

Comment traité un sommet pas encore traité ?

Exemple

exemple algorithme