双向链表

逐步查看 C++ 双向链表:两端插入、删除、反转和五法则。

所有内容均在此浏览器中模拟,不会发送到任何服务器。

操作

调用构造函数

调用构造函数

l1未构造
∅
  • 当前
  • 指针

第 1 / 34 步

l1
l2
l3

代码

#include <iostream>
#include <utility>
class Nodo { // 节点保存数据以及指向前一个和后一个节点的指针
private: // 私有属性:只有方法可以访问
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_; // 链表的第一个节点
Nodo* cola_; // 链表的最后一个节点
int n_; // 节点数量
public:
Lista() { // 默认构造函数:创建一个空链表
cabeza_ = nullptr;
cola_ = nullptr;
n_ = 0;
}
~Lista() { // 析构函数:释放所有节点
while (cabeza_ != nullptr) {
Nodo* borra = cabeza_;
cabeza_ = borra->getSiguiente();
delete borra;
}
cola_ = nullptr;
}
Lista(const Lista& origin) { // 拷贝构造函数:进行深拷贝
cabeza_ = nullptr;
cola_ = nullptr;
n_ = 0;
Nodo* aux = origin.cabeza_;
while (aux != nullptr) {
insertarAlFinal(aux->getDato()); // 尾部插入可保持原有顺序
aux = aux->getSiguiente();
}
}
Lista& operator=(const Lista& origin) { // 拷贝赋值:先释放旧节点,再进行拷贝
if (this != &origin) { // 防止自我赋值
while (cabeza_ != nullptr) { // 释放链表已有的节点
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 { // 移动构造函数:夺取源链表的节点
cabeza_ = origin.cabeza_;
cola_ = origin.cola_;
n_ = origin.n_;
origin.cabeza_ = nullptr; // 让源链表保持为空
origin.cola_ = nullptr;
origin.n_ = 0;
}
Lista& operator=(Lista&& origin) noexcept { // 移动赋值:先释放旧节点,再夺取新节点
if (this != &origin) { // 防止自我赋值
while (cabeza_ != nullptr) { // 释放链表已有的节点
Nodo* borra = cabeza_;
cabeza_ = borra->getSiguiente();
delete borra;
}
cola_ = nullptr;
cabeza_ = origin.cabeza_;
cola_ = origin.cola_;
n_ = origin.n_;
origin.cabeza_ = nullptr; // 让源链表保持为空
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) { // 头部插入: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) { // 尾部插入:借助 cola_ 为 O(1)
Nodo* nodo = new Nodo(valor);
n_ += 1;
nodo->setAnterior(cola_);
if (cola_ != nullptr) {
cola_->setSiguiente(nodo);
} else {
cabeza_ = nodo;
}
cola_ = nodo;
}
void imprimirLista() const { // 从头到尾打印
Nodo* actual = cabeza_;
while (actual != nullptr) {
std::cout << actual->getDato() << " ";
actual = actual->getSiguiente();
}
std::cout << std::endl;
}
void imprimirListaAtras() const { // 从尾到头打印
Nodo* actual = cola_;
while (actual != nullptr) {
std::cout << actual->getDato() << " ";
actual = actual->getAnterior();
}
std::cout << std::endl;
}
void eliminarNodo(int indice) { // 删除指定位置的节点
if (indice < 0 || indice >= n_) return; // 索引无效:什么也不做
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() { // 原地反转链表:每个节点交换 anterior_ 和 siguiente_
Nodo* actual = cabeza_;
Nodo* temporal = nullptr;
while (actual != nullptr) {
temporal = actual->getAnterior();
actual->setAnterior(actual->getSiguiente());
actual->setSiguiente(temporal);
actual = actual->getAnterior(); // anterior_ 现在是原来的 siguiente_
}
temporal = cabeza_; // 最后交换头节点和尾节点
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; // 拷贝赋值(不使用 swap)
l2.imprimirLista();
l2.imprimirListaAtras();
Lista l3;
l3 = std::move(l1); // 移动赋值(不使用 swap)
l3.invertirLista();
l3.imprimirLista();
return 0;
}

这个双向链表可视化工具展示了每个 C++ 节点如何同时保存指向前一个节点和后一个节点的指针,以及链表如何维护 cabeza_、cola_ 和 n_。画布上同时有三个链表 l1、l2 和 l3,你可以在头部或尾部插入、按索引删除、反转链表,并正向或反向打印。然后试试五法则的五个特殊成员:析构函数、拷贝构造函数、拷贝赋值、移动构造函数和移动赋值。每个操作都会逐步播放,高亮对应的 C++ 代码行,显示模拟地址,并在两个指针重新连接时显示游离节点。一切都在你的浏览器中模拟。

使用方法

  1. 从列表中选择一个示例,或从第一个开始,它会构造一个链表然后销毁它。
  2. 为三个链表中的任意一个添加操作:头部或尾部插入、按索引删除、反转,或向两个方向打印。
  3. 为拷贝和移动在每一行旁选择源链表,需要时输入值或索引。
  4. 点击播放或逐步前进,并跟随高亮的 C++ 代码行、每个节点的两个指针及其地址。

常见问题

什么是双向链表?
双向链表中的每个节点都保存指向前一个节点和后一个节点的指针。这样可以双向遍历,并且有了指向尾节点的指针,就能在 O(1) 时间内从末尾插入。
保存 cola_ 有什么好处?
有了 cola_,无需遍历链表就能在末尾插入,也可以从最后一个节点开始反向打印。
拷贝和移动链表有什么区别?
拷贝会创建带有新地址的新节点,源链表保持不变。移动会接管源链表的节点并保留它们的地址,源链表变为空,cabeza_ 和 cola_ 都为 nullptr。
C++ 代码真的会被编译吗?
不会。页面在你的浏览器中模拟每个操作并高亮对应的代码行,不会向任何服务器发送数据。

相关工具