Pourquoi utilisons-nous des tableaux au lieu d'autres structures de données?

195

Pendant que je programmais, je n'ai pas vu d'instance où un tableau est meilleur pour stocker des informations qu'une autre forme de celles-ci. J'avais en effet pensé que les "fonctionnalités" ajoutées dans les langages de programmation s'étaient améliorées et que cela les avait remplacées. Je vois maintenant qu'ils ne sont pas remplacés mais plutôt donnés une nouvelle vie, pour ainsi dire.

Donc, en gros, quel est l'intérêt d'utiliser des tableaux?

Ce n'est pas tant pourquoi utilisons-nous des tableaux d'un point de vue informatique, mais plutôt pourquoi utiliserions-nous des tableaux d'un point de vue de programmation (une différence subtile). Ce que l'ordinateur fait avec la baie n'était pas le point de la question.

Xesaniel
la source
2
Pourquoi ne pas considérer ce que l'ordinateur fait avec la baie? Nous avons un système de numérotation des maisons parce que nous avons des rues DROITES . Il en va de même pour les tableaux.
lcn
Que voulez-vous dire par « autres structures de données » ou « autre forme »? Et dans quel but?
tevemadar

Réponses:

770

Il est temps de remonter le temps pour une leçon. Bien que nous ne pensions pas beaucoup à ces choses dans nos langages gérés de fantaisie aujourd'hui, ils sont construits sur la même base, alors regardons comment la mémoire est gérée en C.

Avant de plonger, une brève explication de la signification du terme " pointeur ". Un pointeur est simplement une variable qui "pointe" vers un emplacement en mémoire. Il ne contient pas la valeur réelle dans cette zone de mémoire, il contient l'adresse de mémoire qui lui est associée. Considérez un bloc de mémoire comme une boîte aux lettres. Le pointeur serait l'adresse de cette boîte aux lettres.

En C, un tableau est simplement un pointeur avec un décalage, le décalage spécifie la distance à parcourir en mémoire. Cela donne le temps d'accès O (1) .

  MyArray   [5]
     ^       ^
  Pointer  Offset

Toutes les autres structures de données s'appuient sur cela ou n'utilisent pas de mémoire adjacente pour le stockage, ce qui entraîne un mauvais temps de recherche d'accès aléatoire (bien qu'il y ait d'autres avantages à ne pas utiliser de mémoire séquentielle).

Par exemple, disons que nous avons un tableau avec 6 nombres (6,4,2,3,1,5), en mémoire cela ressemblerait à ceci:

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================

Dans un tableau, nous savons que chaque élément est côte à côte en mémoire. Le tableau AC (appelé MyArrayici) est simplement un pointeur vers le premier élément:

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================
   ^
MyArray

Si nous voulions rechercher MyArray[4], en interne, il serait accessible comme ceci:

   0     1     2     3     4 
=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================
                           ^
MyArray + 4 ---------------/
(Pointer + Offset)

Étant donné que nous pouvons accéder directement à n'importe quel élément du tableau en ajoutant l'offset au pointeur, nous pouvons rechercher n'importe quel élément dans le même laps de temps, quelle que soit la taille du tableau. Cela signifie que l'obtention MyArray[1000]prendrait le même temps que l'obtention MyArray[5].

Une autre structure de données est une liste chaînée. Il s'agit d'une liste linéaire de pointeurs, chacun pointant vers le nœud suivant

========    ========    ========    ========    ========
| Data |    | Data |    | Data |    | Data |    | Data |
|      | -> |      | -> |      | -> |      | -> |      | 
|  P1  |    |  P2  |    |  P3  |    |  P4  |    |  P5  |        
========    ========    ========    ========    ========

P(X) stands for Pointer to next node.

Notez que j'ai fait de chaque "nœud" son propre bloc. En effet, ils ne sont pas garantis (et ne seront probablement pas) adjacents en mémoire.

Si je veux accéder à P3, je ne peux pas y accéder directement, car je ne sais pas où il est en mémoire. Tout ce que je sais, c'est où se trouve la racine (P1), donc je dois commencer à P1 et suivre chaque pointeur jusqu'au nœud souhaité.

Il s'agit d'un temps de recherche O (N) (le coût de recherche augmente à mesure que chaque élément est ajouté). Il est beaucoup plus cher de se rendre au P1000 que de se rendre au P4.

Les structures de données de niveau supérieur, telles que les tables de hachage, les piles et les files d'attente, peuvent toutes utiliser un tableau (ou plusieurs tableaux) en interne, tandis que les listes liées et les arbres binaires utilisent généralement des nœuds et des pointeurs.

Vous vous demandez peut-être pourquoi quelqu'un utiliserait une structure de données qui nécessite une traversée linéaire pour rechercher une valeur au lieu de simplement utiliser un tableau, mais ils ont leur utilité.

Reprenez notre tableau. Cette fois, je veux trouver l'élément de tableau qui contient la valeur «5».

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================
   ^     ^     ^     ^     ^   FOUND!

Dans cette situation, je ne sais pas quel décalage ajouter au pointeur pour le trouver, je dois donc commencer à 0 et remonter jusqu'à ce que je le trouve. Cela signifie que je dois effectuer 6 vérifications.

Pour cette raison, la recherche d'une valeur dans un tableau est considérée comme O (N). Le coût de la recherche augmente à mesure que le tableau s'agrandit.

Rappelez-vous ci-dessus où j'ai dit que l'utilisation d'une structure de données non séquentielle peut parfois avoir des avantages? La recherche de données est l'un de ces avantages et l'un des meilleurs exemples est l'arbre binaire.

Un arbre binaire est une structure de données similaire à une liste chaînée, mais au lieu de se lier à un seul nœud, chaque nœud peut se lier à deux nœuds enfants.

         ==========
         |  Root  |         
         ==========
        /          \ 
  =========       =========
  | Child |       | Child |
  =========       =========
                  /       \
            =========    =========
            | Child |    | Child |
            =========    =========

 Assume that each connector is really a Pointer

Lorsque des données sont insérées dans un arbre binaire, elles utilisent plusieurs règles pour décider où placer le nouveau nœud. Le concept de base est que si la nouvelle valeur est supérieure aux parents, elle l'insère à gauche, si elle est inférieure, elle l'insère à droite.

Cela signifie que les valeurs dans un arbre binaire pourraient ressembler à ceci:

         ==========
         |   100  |         
         ==========
        /          \ 
  =========       =========
  |  200  |       |   50  |
  =========       =========
                  /       \
            =========    =========
            |   75  |    |   25  |
            =========    =========

Lors de la recherche d'un arbre binaire pour la valeur de 75, il suffit de visiter 3 nœuds (O (log N)) à cause de cette structure:

  • Est-ce que 75 est inférieur à 100? Regardez le nœud droit
  • 75 est-il supérieur à 50? Regardez le nœud gauche
  • Il y a le 75!

Même s'il y a 5 nœuds dans notre arbre, nous n'avons pas eu besoin de regarder les deux autres, car nous savions qu'ils (et leurs enfants) ne pouvaient pas contenir la valeur que nous recherchions. Cela nous donne un temps de recherche qui, dans le pire des cas, signifie que nous devons visiter chaque nœud, mais dans le meilleur des cas, nous n'avons qu'à visiter une petite partie des nœuds.

C'est là que les tableaux sont battus, ils fournissent un temps de recherche linéaire O (N), malgré le temps d'accès O (1).

Il s'agit d'un aperçu incroyablement élevé des structures de données en mémoire, ignorant de nombreux détails, mais nous espérons qu'il illustre la force et la faiblesse d'un tableau par rapport à d'autres structures de données.

FlySwat
la source
1
@Jonathan: Vous avez mis à jour le diagramme pour pointer vers le 5ème élément mais vous avez également changé MyArray [4] en MyArray [5] donc il est toujours incorrect, remettez l'index à 4 et gardez le diagramme tel quel et vous devriez être bon .
Robert Gamble,
54
C'est ce qui me
dérange à
8
Bonne réponse. Mais l'arbre que vous décrivez est un arbre de recherche binaire - un arbre binaire n'est qu'un arbre où chaque nœud a au plus deux enfants. Vous pouvez avoir un arbre binaire avec les éléments dans n'importe quel ordre. L'arbre de recherche binaire est organisé comme vous le décrivez.
gnud
1
Bonne explication, mais je ne peux pas m'empêcher de taper ... si vous êtes autorisé à réorganiser les éléments dans une arborescence de recherche binaire, pourquoi ne pouvez-vous pas réorganiser les éléments du tableau afin qu'une recherche binaire fonctionne également? Vous pourriez entrer dans plus de détails concernant l'insertion / suppression O (n) pour un arbre, mais O (n) pour un tableau.
commercialise le
2
La représentation arborescente binaire n'est-elle pas un O (log n) car le temps d'accès augmente logarithmiquement par rapport à la taille de l'ensemble de données?
Evan Plaice
73

Pour O (1) accès aléatoire, qui ne peut pas être battu.

Jason
la source
6
Sur quel point? Qu'est-ce que O (1)? Qu'est-ce que l'accès aléatoire? Pourquoi ne peut-il pas être battu? Un autre point?
jason
3
O (1) signifie temps constant, par exemple si vous voulez obtenir l'élément n-esim d'un tableau, vous y accédez directement via son indexeur (tableau [n-1]), avec une liste chaînée par exemple, vous avez pour trouver la tête, puis passer au noeud suivant séquentiellement n-1 fois qui est O (n), temps linéaire.
CMS
8
La notation Big-O décrit comment la vitesse d'un algorithme varie en fonction de la taille de son entrée. Un algorithme O (n) prendra deux fois plus de temps pour fonctionner avec deux fois plus d'éléments et 8 fois plus de temps pour fonctionner avec 8 fois plus d'éléments. En d'autres termes, la vitesse d'un algorithme O (n) varie avec le [suite ...]
Gareth
8
taille de son entrée. O (1) implique que la taille de l'entrée ('n') ne prend pas en compte la vitesse de l'algorithme, c'est une vitesse constante quelle que soit la taille de l'entrée
Gareth
9
Je vois votre O (1) et je vous élève O (0).
Chris Conway
23

Tous les programmes ne font pas la même chose ou ne s'exécutent pas sur le même matériel.

C'est généralement la réponse pour laquelle diverses fonctionnalités de langage existent. Les tableaux sont un concept informatique fondamental. Le remplacement de tableaux par des listes / matrices / vecteurs / quelle que soit la structure de données avancée aurait un impact sérieux sur les performances et serait carrément impraticable dans un certain nombre de systèmes. Il existe un certain nombre de cas où l'utilisation de l'un de ces objets de collecte de données "avancés" devrait être utilisée en raison du programme en question.

Dans la programmation d'entreprise (ce que la plupart d'entre nous font), nous pouvons cibler du matériel relativement puissant. Utiliser une liste en C # ou un vecteur en Java est le bon choix à faire dans ces situations car ces structures permettent au développeur d'atteindre les objectifs plus rapidement, ce qui permet à ce type de logiciel d'être plus en vedette.

Lors de l'écriture d'un logiciel intégré ou d'un système d'exploitation, une baie peut souvent être le meilleur choix. Alors qu'un tableau offre moins de fonctionnalités, il prend moins de RAM et le compilateur peut optimiser le code plus efficacement pour les recherches dans les tableaux.

Je suis sûr que je laisse de côté un certain nombre d'avantages pour ces cas, mais j'espère que vous comprendrez.

Jason Jackson
la source
4
Ironiquement, en Java, vous devez utiliser une ArrayList (ou une LinkedList) au lieu d'un Vector. Cela est lié à la synchronisation d'un vecteur, ce qui est généralement une surcharge inutile.
ashirley
0

Un moyen de regarder les avantages des tableaux est de voir où est la capacité d'accès O (1) des tableaux est nécessaire et donc capitalisée:

  1. Dans les tables de recherche de votre application (un tableau statique pour accéder à certaines réponses catégorielles)

  2. Mémorisation (résultats de fonctions complexes déjà calculés, de sorte que vous ne calculez plus la valeur de la fonction, par exemple log x)

  3. Applications de vision par ordinateur à haute vitesse nécessitant un traitement d'image ( https://en.wikipedia.org/wiki/Lookup_table#Lookup_tables_in_image_processing )

priya khokher
la source