单向链表的实现(C++)
单向链表的基本概念
单向链表是一种常见的线性数据结构,其中的节点通过指针相互连接形成一个序列3。每个节点包含两部分:一部分用于存储实际的数据,另一部分是一个指向下一个节点的指针。这种设计使得单向链表能够动态地管理内存空间,在需要时分配新的节点。
链表的特点
相比于数组,单向链表具有以下特点:
- 灵活性:可以在任意位置插入或删除节点,而不需要移动其他元素。
- 动态大小:可以根据需求调整链表的长度,无需预先定义固定容量。
- 缺点:无法随机访问特定索引处的节点,必须从头节点依次遍历至目标节点1。
数据结构组成
-
节点结构体
MyStudent
:-
数据域:
-
int id
:存储学生的学号。 -
string name
:存储学生的姓名。
-
-
指针域:
-
MyStudent* next
:指向下一个节点的指针。
-
-
构造函数:初始化
id
、name
和next
,支持直接指定下一个节点(默认值为nullptr
)。struct MyStudent { string name;//姓名 int id;//id MyStudent* next;//指向下一个节点的指针 MyStudent(int Id, string named, MyStudent* next1 = nullptr) :id(Id), name(named), next(next1) {} };
-
-
链表类
student
:-
头指针:
MyStudent* head
,指向链表的第一个节点。 -
功能方法:插入、删除、遍历等操作。
-
功能实现详解
1. 头插法插入节点:push(int id, string name)
-
实现逻辑:
-
创建新节点
newdata
,传入id
和name
。 -
若链表为空(
head == nullptr
),直接将head
指向新节点。 -
若链表非空:
-
将新节点的
next
指向原头节点。 -
更新
head
指向新节点。
-
-
-
时间复杂度:O(1)。
void student::push(int id, string name)//头插法插入 { MyStudent* newdata = new MyStudent(id,name); if (head==nullptr) { //如果newdata的next指针是空的,代表这是第一个节点,把头指针指向这个节点 head = newdata; } else { newdata->next = head; head = newdata; } }
2. 删除头节点:pop()
-
实现逻辑:
-
检查链表是否为空(
isempty()
)。 -
若链表非空:
-
保存当前头节点到临时指针
temp
。 -
将
head
指向下一个节点。 -
释放
temp
的内存。
-
-
-
时间复杂度:O(1)。
void student::pop() { if (!isempty()) { MyStudent* temp = head; head = head->next; delete temp; } }
3. 按学号删除节点:pop(int num)
-
链表删除的上下文
假设链表结构为:
prev节点
→curr节点
→curr->next节点
→ ...
我们要删除中间的curr节点
。 -
类比解释
想象一列火车车厢:
-
初始状态:车厢 A(prev)→ 车厢 B(curr)→ 车厢 C(curr->next)
-
目标:移除车厢 B。
-
操作:将车厢 A 的挂钩直接连接到车厢 C。
-
结果:车厢 A → 车厢 C,车厢 B 被移除。
-
实现逻辑:
-
检查链表是否为空。
-
使用双指针
preptr
(前驱节点)和cur
(当前节点)遍历链表。 -
找到
id == num
的节点后:-
头节点匹配:直接更新
head
指向下一个节点。 -
中间或尾部匹配:调整前驱节点的
next
指针,跳过当前节点。 -
释放
cur
的内存,并立即退出函数。
-
-
若未找到匹配节点,输出提示信息。
-
-
时间复杂度:O(n)。
void student::pop(int num) { if (isempty()) { cout << "链表为空"<<endl; return; } MyStudent* preptr=nullptr;//cur的上一个节点,如果cur是头节点就为nullptr MyStudent* cur=head;//cur初始化指向链表头部,从头部向后比对移动 while (cur!=nullptr)//如果链表不是空的进入循环 { if (cur->id==num) { //如果第一个节点就满足了,直接把头指针指向第二个节点 if (preptr == nullptr) { head = cur->next; } //如果后面的节点满足 else { preptr->next = cur->next; } //找到节点满足后,删除临时指针 delete cur; //delete preptr; return; } //如果不满足就往后挪 preptr = cur; cur = cur->next; } cout << "未找到节点" << endl; }
4. 遍历链表:listTraverse()
-
实现逻辑:
-
检查链表是否为空。
-
从头节点开始遍历,依次输出每个节点的
id
和name
。 -
最后输出
NULL
表示链表结束。
-
-
时间复杂度:O(n)。
void student::listTraverse() { if (isempty()) { cout << "链表为空" << endl; return; } MyStudent* x = head; while (x!=nullptr) { cout << x->id << ":" << x->name << "->" ; x = x->next; } cout << "NULL"<<endl; }
5. 析构函数:~student()
-
实现逻辑:
-
循环调用
pop()
删除头节点,直到链表为空。 -
关键改进:由于
pop()
已正确释放内存,析构函数能安全释放所有节点。student::~student() { while (isempty()==false) { pop(); } }
-
示例运行流程
int main() {
student stu;
stu.push(10, "aa"); // 头插法插入 10:aa
stu.push(50, "bb"); // 头插法插入 50:bb → 10:aa
stu.push(90, "cc"); // 头插法插入 90:cc → 50:bb → 10:aa
stu.listTraverse(); // 输出: 90:cc->50:bb->10:aa->NULL
stu.pop(50); // 删除学号 50 的节点 → 链表变为 90:cc→10:aa
stu.listTraverse(); // 输出: 90:cc->10:aa->NULL
stu.pop(0); // 未找到学号 0,输出提示
stu.listTraverse(); // 输出: 90:cc->10:aa->NULL
return 0;
}
STL对单向链表的支持
C++ 标准模板库(STL)提供了对单向链表的直接支持,具体通过 std::forward_list
容器实现。std::forward_list
是一个单向链表,每个节点仅包含指向下一个元素的指针,因此它在内存占用和某些操作效率上优于双向链表 std::list
。以下是关于 std::forward_list
的详细介绍:
1. std::forward_list
的核心特性
-
单向性:只能从头部向尾部单向遍历,无法反向遍历。
-
内存高效:每个节点仅存储一个指向下一个节点的指针,内存占用更小。
-
操作限制:没有直接访问尾部元素的方法(如
back()
),插入和删除操作需通过迭代器实现。 -
头插法优化:在链表头部插入或删除元素的时间复杂度为 O(1)。
2. 基本用法
(1) 头文件与声明
#include <forward_list>
// 声明一个存储整型的单向链表
std::forward_list<int> flist;
(2) 常用操作
操作 | 示例 | 说明 |
---|---|---|
插入元素(头插法) | flist.push_front(10); | 在链表头部插入元素 10 |
删除头部元素 | flist.pop_front(); | 删除链表头部元素 |
遍历链表 | for (auto& num : flist) { ... } | 使用范围 for 循环遍历 |
插入元素到指定位置 | flist.insert_after(it, 20); | 在迭代器 it 指向的位置后插入 20 |
删除指定位置的元素 | flist.erase_after(it); | 删除迭代器 it 指向位置的下一个元素 |
判断链表是否为空 | if (flist.empty()) { ... } | 返回布尔值表示链表是否为空 |
清空链表 | flist.clear(); | 移除所有元素 |
3. 示例代码
#include <iostream>
#include <forward_list>
int main() {
std::forward_list<int> flist;
// 头插法插入元素
flist.push_front(30);
flist.push_front(20);
flist.push_front(10);
// 遍历并输出链表
std::cout << "链表内容: ";
for (const auto& num : flist) {
std::cout << num << " -> ";
}
std::cout << "NULL" << std::endl;
// 在第二个元素后插入 25
auto it = flist.begin();
std::advance(it, 1); // 移动迭代器到第二个位置(索引从 0 开始)
flist.insert_after(it, 25);
// 删除第三个元素
it = flist.begin();
std::advance(it, 2);
flist.erase_after(it);
// 再次遍历输出
std::cout << "修改后的链表: ";
for (const auto& num : flist) {
std::cout << num << " -> ";
}
std::cout << "NULL" << std::endl;
return 0;
}
输出:
链表内容: 10 -> 20 -> 30 -> NULL
修改后的链表: 10 -> 20 -> 25 -> NULL
4. std::forward_list
与 std::list
的对比
特性 | std::forward_list | std::list |
---|---|---|
遍历方向 | 单向(仅向前) | 双向(向前或向后) |
内存占用 | 每个节点 1 个指针(更小) | 每个节点 2 个指针(更大) |
插入/删除头部元素 | O(1) | O(1) |
插入/删除尾部元素 | 需要遍历到尾部(O(n)) | 直接操作(O(1)) |
随机访问支持 | 不支持 | 不支持 |
适用场景 | 内存敏感、频繁头插/删、无需反向遍历 | 需要双向操作或频繁中间插入/删除 |
5. 使用建议
-
选择
std::forward_list
的场景:-
需要极简的内存占用。
-
频繁在链表头部插入或删除元素。
-
不需要反向遍历或尾部操作。
-
-
避免使用
std::forward_list
的场景:-
需要频繁访问尾部元素。
-
需要双向遍历或随机插入删除。
-
需要直接获取链表大小(
std::forward_list
没有size()
方法,需手动计算)。
-
总结
该程序实现了一个单向链表,支持头插法插入、删除头节点、按学号删除节点、遍历链表等功能。代码通过面向对象的方式封装了链表操作,避免了内存泄漏,并优化了边界条件处理。