Liste doublement chaînée

Parcourez pas à pas une liste doublement chaînée C++ : insertion aux deux extrémités, suppression, inversion et règle des cinq.

Tout est simulé dans ce navigateur ; rien n'est envoyé à un serveur.

Opérations

Appel du constructeur

Appel du constructeur

l1Non construite
∅
  • Actuel
  • Pointeur

Étape 1 / 34

l1
l2
l3

Code

#include <iostream>
#include <utility>
class Nodo { // Un nœud contient la donnée et les pointeurs vers le précédent et le suivant
private: // Attributs privés : seules les méthodes y touchent
int dato_;
Nodo* anterior_;
Nodo* siguiente_;
public:
Nodo(int dato, Nodo* anterior = nullptr, Nodo* siguiente = nullptr) {
dato_ = dato;
anterior_ = anterior;
siguiente_ = siguiente;
}
int getDato() const { return dato_; }
void setDato(int dato) { dato_ = dato; }
Nodo* getAnterior() const { return anterior_; }
void setAnterior(Nodo* anterior) { anterior_ = anterior; }
Nodo* getSiguiente() const { return siguiente_; }
void setSiguiente(Nodo* siguiente) { siguiente_ = siguiente; }
};
class Lista {
private:
Nodo* cabeza_; // Premier nœud de la liste
Nodo* cola_; // Dernier nœud de la liste
int n_; // Nombre de nœuds
public:
Lista() { // Constructeur par défaut : crée une liste vide
cabeza_ = nullptr;
cola_ = nullptr;
n_ = 0;
}
~Lista() { // Destructeur : libère tous les nœuds
while (cabeza_ != nullptr) {
Nodo* borra = cabeza_;
cabeza_ = borra->getSiguiente();
delete borra;
}
cola_ = nullptr;
}
Lista(const Lista& origin) { // Constructeur de copie : fait une copie profonde
cabeza_ = nullptr;
cola_ = nullptr;
n_ = 0;
Nodo* aux = origin.cabeza_;
while (aux != nullptr) {
insertarAlFinal(aux->getDato()); // Insérer en queue conserve l'ordre d'origine
aux = aux->getSiguiente();
}
}
Lista& operator=(const Lista& origin) { // Affectation par copie : libère les anciens nœuds, puis copie
if (this != &origin) { // Protège contre l'auto-affectation
while (cabeza_ != nullptr) { // Libère les nœuds que la liste possède déjà
Nodo* borra = cabeza_;
cabeza_ = borra->getSiguiente();
delete borra;
}
cola_ = nullptr;
n_ = 0;
Nodo* aux = origin.cabeza_;
while (aux != nullptr) {
insertarAlFinal(aux->getDato());
aux = aux->getSiguiente();
}
}
return *this;
}
Lista(Lista&& origin) noexcept { // Constructeur de déplacement : prend les nœuds de la source
cabeza_ = origin.cabeza_;
cola_ = origin.cola_;
n_ = origin.n_;
origin.cabeza_ = nullptr; // Laisse la source vide
origin.cola_ = nullptr;
origin.n_ = 0;
}
Lista& operator=(Lista&& origin) noexcept { // Affectation par déplacement : libère les anciens nœuds, puis prend les nouveaux
if (this != &origin) { // Protège contre l'auto-affectation
while (cabeza_ != nullptr) { // Libère les nœuds que la liste possède déjà
Nodo* borra = cabeza_;
cabeza_ = borra->getSiguiente();
delete borra;
}
cola_ = nullptr;
cabeza_ = origin.cabeza_;
cola_ = origin.cola_;
n_ = origin.n_;
origin.cabeza_ = nullptr; // Laisse la source vide
origin.cola_ = nullptr;
origin.n_ = 0;
}
return *this;
}
int getTamano() const { return n_; }
Nodo* getCabeza() const { return cabeza_; }
Nodo* getCola() const { return cola_; }
void insertarElemento(int valor) { // Insertion en tête : O(1)
Nodo* nodo = new Nodo(valor);
n_ += 1;
nodo->setSiguiente(cabeza_);
if (cabeza_ != nullptr) {
cabeza_->setAnterior(nodo);
} else {
cola_ = nodo;
}
cabeza_ = nodo;
}
void insertarAlFinal(int valor) { // Insertion en queue : O(1) grâce à cola_
Nodo* nodo = new Nodo(valor);
n_ += 1;
nodo->setAnterior(cola_);
if (cola_ != nullptr) {
cola_->setSiguiente(nodo);
} else {
cabeza_ = nodo;
}
cola_ = nodo;
}
void imprimirLista() const { // Affiche de la tête à la queue
Nodo* actual = cabeza_;
while (actual != nullptr) {
std::cout << actual->getDato() << " ";
actual = actual->getSiguiente();
}
std::cout << std::endl;
}
void imprimirListaAtras() const { // Affiche de la queue à la tête
Nodo* actual = cola_;
while (actual != nullptr) {
std::cout << actual->getDato() << " ";
actual = actual->getAnterior();
}
std::cout << std::endl;
}
void eliminarNodo(int indice) { // Supprime le nœud à l'indice donné
if (indice < 0 || indice >= n_) return; // Indice invalide : rien ne se passe
Nodo* eliminar = cabeza_;
for (int i = 0; i < indice; i++) {
eliminar = eliminar->getSiguiente();
}
if (eliminar->getAnterior() != nullptr) {
eliminar->getAnterior()->setSiguiente(eliminar->getSiguiente());
} else {
cabeza_ = eliminar->getSiguiente();
}
if (eliminar->getSiguiente() != nullptr) {
eliminar->getSiguiente()->setAnterior(eliminar->getAnterior());
} else {
cola_ = eliminar->getAnterior();
}
delete eliminar;
n_--;
}
void invertirLista() { // Inverse la liste sur place : chaque nœud échange anterior_ et siguiente_
Nodo* actual = cabeza_;
Nodo* temporal = nullptr;
while (actual != nullptr) {
temporal = actual->getAnterior();
actual->setAnterior(actual->getSiguiente());
actual->setSiguiente(temporal);
actual = actual->getAnterior(); // anterior_ est maintenant l'ancien siguiente_
}
temporal = cabeza_; // Enfin, on échange la tête et la queue
cabeza_ = cola_;
cola_ = temporal;
}
};
int main() {
Lista l1;
l1.insertarElemento(10);
l1.insertarElemento(20);
l1.insertarAlFinal(5);
l1.insertarElemento(30);
Lista l2;
l2.insertarElemento(100);
l2 = l1; // Affectation par copie (sans swap)
l2.imprimirLista();
l2.imprimirListaAtras();
Lista l3;
l3 = std::move(l1); // Affectation par déplacement (sans swap)
l3.invertirLista();
l3.imprimirLista();
return 0;
}

Ce visualiseur de liste doublement chaînée montre comment chaque nœud C++ garde un pointeur vers le précédent et un autre vers le suivant, et comment la liste maintient cabeza_, cola_ et n_. Trois listes, l1, l2 et l3, partagent le canevas pour que vous puissiez insérer en tête ou en queue, supprimer par indice, inverser la liste et l'afficher en avant et en arrière. Essayez ensuite les cinq membres spéciaux de la règle des cinq : destructeur, constructeur de copie, affectation par copie, constructeur de déplacement et affectation par déplacement. Chaque opération se déroule pas à pas avec la ligne C++ surlignée, des adresses simulées et le nœud détaché pendant la reconnexion. Tout est simulé dans votre navigateur.

Comment l'utiliser

  1. Choisissez un exemple dans la liste ou commencez par le premier, qui construit une liste puis la détruit.
  2. Ajoutez des opérations à n'importe laquelle des trois listes : insérer en tête ou en queue, supprimer par indice, inverser ou afficher dans les deux sens.
  3. Choisissez la liste source à côté de chaque ligne pour les copies et les déplacements, et saisissez une valeur ou un indice si nécessaire.
  4. Appuyez sur Lecture ou avancez pas à pas et suivez la ligne C++ surlignée, les deux pointeurs de chaque nœud et leurs adresses.

Questions fréquentes

Qu'est-ce qu'une liste doublement chaînée ?
C'est une liste où chaque nœud stocke un pointeur vers le nœud précédent et un autre vers le suivant. On peut donc la parcourir dans les deux sens et, avec un pointeur vers la queue, insérer à la fin en O(1).
Quel est l'intérêt de garder cola_ ?
Avec cola_ on insère à la fin sans parcourir la liste, et on peut afficher à l'envers en partant du dernier nœud.
Quelle différence entre copier et déplacer une liste ?
Une copie crée de nouveaux nœuds avec de nouvelles adresses et laisse la source intacte. Un déplacement prend les nœuds de la source, conserve leurs adresses et laisse la source vide, avec cabeza_ et cola_ à nullptr.
Le code C++ est-il vraiment compilé ?
Non. La page simule chaque opération dans votre navigateur et surligne la ligne de code correspondante ; rien n'est envoyé à un serveur.

Outils associés