C++单链表与双链表 —— 像火车车厢一样一节连一节
较难31C++单链表与双链表 —— 像火车车厢一样一节连一节
什么是链表?
链表是一种存储数据的方式,它不像数组那样把所有数据挨个排在一起,而是把数据分散存放,然后用“指针”串联起来。你可以把链表想象成一列火车:
- 每节车厢就是一个节点(Node),里面装着货物(数据)。
- 挂钩就是指针(Pointer),用来连接下一节车厢(下一个节点)。
- 整列火车由第一节车厢(头节点 head)开始,一直连到最后。
为什么需要链表?因为数组虽然可以随机访问(直接通过下标找第几个元素),但要在中间插入或删除一个元素时,需要移动大量元素,很麻烦。链表则不同:插入或删除只需改动几个挂钩,其他车厢不动,特别灵活。
链表分为两种:
- 单链表:每节车厢只有向后的挂钩,你只能从车头走到车尾,不能回头。
- 双链表:每节车厢前后都有挂钩,可以往前也可以往后走。
C++ 标准库中提供了 list(双链表)和 forward_list(单链表),但为了真正理解链表的工作原理,我们最好自己用结构体和指针来模拟。下面我们就从单链表开始学起。
一、单链表详解
1. 节点结构
每个节点包含两个部分:数据(比如一个整数)和指向下一个节点的指针。最后一个节点的指针指向空(NULL 或 nullptr),表示链表结束。
struct Node {
int data; // 数据,这里用整数举例
Node* next; // 指向下一个节点的指针
};
生活比喻:就像寻宝游戏——你手里拿着一张纸条,上面写着“宝藏藏在衣柜里”,这张纸条就是节点,纸条上的字(“藏在衣柜里”)是数据,而指向下一个地点的线索就是指针。你跟着线索找到衣柜,衣柜里又有一张纸条……直到最后一张说“宝藏就在这里”。
2. 创建节点并连接
要创建一个链表,首先要创建许多节点,然后用指针把它们串起来。下面我们手动创建三个节点,存数字 10、20、30,连成 10 -> 20 -> 30 -> NULL。
#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
};
int main() {
// 创建三个节点,用 new 在堆上分配内存
Node* head = new Node(); // 第一个节点(头节点)
head->data = 10;
Node* second = new Node(); // 第二个节点
second->data = 20;
Node* third = new Node(); // 第三个节点
third->data = 30;
// 把节点连起来:第一个节点的next指向第二个,第二个的next指向第三个
head->next = second;
second->next = third;
third->next = NULL; // 最后一个节点指向空
// 遍历输出链表
Node* current = head; // 从头部开始
while (current != NULL) {
cout << current->data << " -> ";
current = current->next; // 移动到下一个节点
}
cout << "NULL" << endl;
// 释放内存(实际程序中不要忘记)
delete head;
delete second;
delete third;
return 0;
}
运行结果:
10 -> 20 -> 30 -> NULL
3. 插入节点(中间插入)
要在链表中插入一个新节点,比如在 10 和 20 之间插入 15,只需要三步:
- 新节点的
next指向 20。 - 10 的
next指向新节点。 - 其他节点不用动。
下面是一个插入函数:
// 在指定节点之后插入一个新节点
void insertAfter(Node* prevNode, int newData) {
if (prevNode == NULL) {
cout << "前一个节点不能为空!" << endl;
return;
}
Node* newNode = new Node(); // 创建新节点
newNode->data = newData; // 存入数据
newNode->next = prevNode->next; // 新节点指向原来的下一个
prevNode->next = newNode; // 前一个节点指向新节点
}
生活例子:火车要加一节新车厢,只需在原有挂钩处拆开,挂上新车厢,再把新车厢的后挂钩连到原来的下一节车厢。不需要移动其他车厢。
4. 删除节点
删除一个节点也很简单:让前一个节点的 next 跳过当前节点,直接指向当前节点的下一个节点,然后释放当前节点的内存。
// 删除给定节点的下一个节点(注意:不能删除头节点本身)
void deleteAfter(Node* prevNode) {
if (prevNode == NULL || prevNode->next == NULL) return;
Node* toDelete = prevNode->next; // 要删除的节点
prevNode->next = toDelete->next; // 跳过它
delete toDelete; // 释放内存
}
注意:删除后要释放内存,否则会造成内存泄漏(就像垃圾堆里越堆越多的空车厢)。
二、双链表简介
双链表比单链表多了一个向前指针 prev,每个节点都知道前一个是谁。这样,你可以从后往前遍历,插入和删除也变得更方便,因为不需要再专门找前一个节点。
节点结构
struct DNode {
int data; // 数据
DNode* prev; // 指向前一个节点
DNode* next; // 指向后一个节点
};
双链表的创建和遍历
下面创建一个双链表 10 <-> 20 <-> 30,并双向遍历:
#include <iostream>
using namespace std;
struct DNode {
int data;
DNode* prev;
DNode* next;
};
int main() {
// 创建三个节点
DNode* head = new DNode();
head->data = 10;
head->prev = NULL; // 头节点的前驱为空
DNode* second = new DNode();
second->data = 20;
DNode* third = new DNode();
third->data = 30;
third->next = NULL; // 尾节点的后继为空
// 连接前后关系
head->next = second;
second->prev = head;
second->next = third;
third->prev = second;
// 正向遍历:从头到尾
cout << "正向:";
DNode* cur = head;
while (cur != NULL) {
cout << cur->data << " <-> ";
cur = cur->next;
}
cout << "NULL" << endl;
// 反向遍历:从尾到头
cout << "反向:";
cur = third; // 从尾部开始
while (cur != NULL) {
cout << cur->data << " <-> ";
cur = cur->prev;
}
cout << "NULL" << endl;
// 释放内存(略,实际应逐个 delete)
delete head;
delete second;
delete third;
return 0;
}
运行结果:
正向:10 <-> 20 <-> 30 <-> NULL
反向:30 <-> 20 <-> 10 <-> NULL
双链表的优势:如果你想删除某个节点,直接用它自己的
prev和next就能找到前后节点,不需要像单链表那样先找到前一个节点。
三、常见错误(新手别踩坑)
-
忘记初始化指针
Node* ptr;直接使用,没有指向任何地方,会导致程序崩溃。一定要先让指针指向有效节点或NULL。 -
忘记把最后一个节点的 next 设为 NULL
否则遍历时不知道何时停止,会一直访问到非法内存。 -
插入或删除时指针顺序搞错
例如:先改了前一个节点的next,再改新节点的next,就会丢失后面节点的连接。正确做法:先连新节点和后面,再连前面和新节点(参考插入函数)。 -
忘记释放内存
用new创建的节点必须用delete释放,否则程序会内存泄漏(占用的内存越来越多)。不过对于小练习,不释放也不影响运行,但好习惯要养成。 -
访问空指针的数据
比如current->data但current == NULL,会崩溃。所以遍历时一定要先判断current != NULL。
四、完整可运行示例(单链表:插入、删除、遍历)
下面是一个完整的单链表程序,包含以下功能:
- 在头部插入节点
- 在尾部插入节点
- 在指定位置插入节点
- 删除指定值的节点
- 打印链表
代码中每行变量都加了中文注释,方便你理解。
#include <iostream>
using namespace std;
// 定义节点结构
struct Node {
int data; // 数据,这里用整数
Node* next; // 指向下一个节点的指针
};
// 打印链表
void printList(Node* head) {
Node* cur = head; // 当前节点,从头部开始
while (cur != NULL) {
cout << cur->data << " -> ";
cur = cur->next; // 移动到下一个
}
cout << "NULL" << endl;
}
// 在头部插入新节点
void insertAtHead(Node*& head, int newData) {
Node* newNode = new Node(); // 创建新节点
newNode->data = newData; // 存入数据
newNode->next = head; // 新节点指向原头节点
head = newNode; // 让新节点成为头节点
}
// 在尾部插入新节点
void insertAtTail(Node*& head, int newData) {
Node* newNode = new Node();
newNode->data = newData;
newNode->next = NULL; // 新节点将成为尾节点,next 为空
if (head == NULL) { // 如果链表为空
head = newNode;
return;
}
Node* cur = head; // 找到最后一个节点
while (cur->next != NULL) {
cur = cur->next;
}
cur->next = newNode; // 最后一个节点的 next 指向新节点
}
// 删除第一个等于某个值的节点
void deleteValue(Node*& head, int target) {
if (head == NULL) return; // 空链表什么都不做
// 如果要删除的是头节点
if (head->data == target) {
Node* toDelete = head; // 保存要删除的节点
head = head->next; // 头节点后移
delete toDelete; // 释放内存
return;
}
// 查找要删除节点的前一个节点
Node* prev = head;
while (prev->next != NULL && prev->next->data != target) {
prev = prev->next;
}
if (prev->next == NULL) {
cout << "没有找到值为 " << target << " 的节点。" << endl;
return;
}
Node* toDelete = prev->next; // 要删除的节点
prev->next = toDelete->next; // 跳过它
delete toDelete;
}
int main() {
Node* head = NULL; // 初始链表为空
// 插入几个节点
insertAtTail(head, 10); // 尾部插入: 10
insertAtTail(head, 20); // 尾部插入: 10 -> 20
insertAtHead(head, 5); // 头部插入: 5 -> 10 -> 20
insertAtTail(head, 30); // 尾部插入: 5 -> 10 -> 20 -> 30
cout << "当前链表:";
printList(head);
// 删除值为20的节点
deleteValue(head, 20);
cout << "删除20之后:";
printList(head);
// 删除不存在的值
deleteValue(head, 99);
// 输出提示“没有找到”
// 释放所有节点内存(略,实际完整的程序需要逐个删除)
// 这里为了简单,不逐一释放
return 0;
}
运行结果:
当前链表:5 -> 10 -> 20 -> 30 -> NULL
删除20之后:5 -> 10 -> 30 -> NULL
没有找到值为 99 的节点。
五、相关指引
学完单链表和双链表,你还可以继续探索:
- 循环链表:把最后一个节点的 next 指向头节点,形成一个圈。就像游乐园的环形小火车,可以一直转下去。
- 使用 STL 容器:C++ 标准库中的
forward_list(单链表)和list(双链表)可以直接使用,免去手写节点管理的麻烦。但理解底层原理能帮助你更灵活地使用它们。 - 与数组对比:数组适合频繁读取(按下标),链表适合频繁插入删除。根据场景选择合适的工具。
链表是数据结构的基础,掌握了它,后面学习栈、队列、树都会轻松很多。加油,你一定可以像拼乐高一样自如地组装这些“数据车厢”!
例题精讲
以下哪个是C++单链表节点的正确结构体定义?
在双链表中,已知节点p(非头非尾),要在p之后插入一个新节点q(已分配),需要修改多少个指针?
在单链表中,删除头节点时,必须将头指针指向原头节点的下一个节点。
以下代码实现单链表的遍历输出,请补全循环条件。
struct Node {
int data;
Node* next;
};
void printList(Node* head) {
Node* cur = head;
while ( ___) {
cout << cur->data << " ";
cur = cur->next;
}
}以下代码在双链表中删除节点p(假设p不是头节点也不是尾节点),请补全两个缺失的语句。
struct Node {
int data;
Node* prev;
Node* next;
};
void deleteNode(Node* p) {
p->prev->next = p->next;
___(1)___;
___(2)___;
}