当前位置: 首页 > news >正文

单向链表的实现(C++)

单向链表的基本概念

单向链表是一种常见的线性数据结构,其中的节点通过指针相互连接形成一个序列3。每个节点包含两部分:一部分用于存储实际的数据,另一部分是一个指向下一个节点的指针。这种设计使得单向链表能够动态地管理内存空间,在需要时分配新的节点。

链表的特点

相比于数组,单向链表具有以下特点:

  • 灵活性:可以在任意位置插入或删除节点,而不需要移动其他元素。
  • 动态大小:可以根据需求调整链表的长度,无需预先定义固定容量。
  • 缺点:无法随机访问特定索引处的节点,必须从头节点依次遍历至目标节点1

数据结构组成

  1. 节点结构体 MyStudent

    • 数据域

      • int id:存储学生的学号。

      • string name:存储学生的姓名。

    • 指针域

      • MyStudent* next:指向下一个节点的指针。

    • 构造函数:初始化 idname 和 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) {}
      };

  2. 链表类 student

    • 头指针MyStudent* head,指向链表的第一个节点。

    • 功能方法:插入、删除、遍历等操作。

功能实现详解

1. 头插法插入节点:push(int id, string name)
  • 实现逻辑

    1. 创建新节点 newdata,传入 id 和 name

    2. 若链表为空(head == nullptr),直接将 head 指向新节点。

    3. 若链表非空:

      • 将新节点的 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()
  • 实现逻辑

    1. 检查链表是否为空(isempty())。

    2. 若链表非空:

      • 保存当前头节点到临时指针 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 被移除。

  • 实现逻辑

    1. 检查链表是否为空。

    2. 使用双指针 preptr(前驱节点)和 cur(当前节点)遍历链表。

    3. 找到 id == num 的节点后:

      • 头节点匹配:直接更新 head 指向下一个节点。

      • 中间或尾部匹配:调整前驱节点的 next 指针,跳过当前节点。

      • 释放 cur 的内存,并立即退出函数。

    4. 若未找到匹配节点,输出提示信息。

  • 时间复杂度: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()
  • 实现逻辑

    1. 检查链表是否为空。

    2. 从头节点开始遍历,依次输出每个节点的 id 和 name

    3. 最后输出 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()
  • 实现逻辑

    1. 循环调用 pop() 删除头节点,直到链表为空。

    2. 关键改进:由于 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_liststd::list
遍历方向单向(仅向前)双向(向前或向后)
内存占用每个节点 1 个指针(更小)每个节点 2 个指针(更大)
插入/删除头部元素O(1)O(1)
插入/删除尾部元素需要遍历到尾部(O(n))直接操作(O(1))
随机访问支持不支持不支持
适用场景内存敏感、频繁头插/删、无需反向遍历需要双向操作或频繁中间插入/删除

 

5. 使用建议
  • 选择 std::forward_list 的场景

    • 需要极简的内存占用。

    • 频繁在链表头部插入或删除元素。

    • 不需要反向遍历或尾部操作。

  • 避免使用 std::forward_list 的场景

    • 需要频繁访问尾部元素。

    • 需要双向遍历或随机插入删除。

    • 需要直接获取链表大小(std::forward_list 没有 size() 方法,需手动计算)。

总结

该程序实现了一个单向链表,支持头插法插入、删除头节点、按学号删除节点、遍历链表等功能。代码通过面向对象的方式封装了链表操作,避免了内存泄漏,并优化了边界条件处理。

相关文章:

  • 基于贝叶斯估计的多传感器数据融合算法matlab仿真
  • SQL 中的 NULL 处理
  • 7.0 实际案例1-1:读取图片并显示
  • 编程思想——FP、OOP、FRP、AOP、IOC、DI、MVC、DTO、DAO
  • HarmonyOS中的多线程并发机制
  • Docker引擎、Docker守护进程和Docker客户端
  • RocketMQ 中的 MessageStore 组件:消息存储的核心枢纽
  • 不同数据库的注入报错信息
  • ubuntu 2204 安装 vcs 2018
  • L1-5 吉老师的回归
  • Python赋能量子计算:算法创新与应用拓展
  • 浏览器发起调用到服务器的全过程解析
  • Mybatis的简单介绍
  • 记一次Agora-RTSALite编译遇到的问题
  • SuperPoint论文及源码解读
  • 使用Lombok无法生成Getter()与Setter()和toString()方法的解决方案
  • RocketMQ 中 DefaultMessageStore 的 AllocateMappedFileService 属性详解
  • 【Linux】Linux 权限:数字背后的神秘 “门禁卡” 系统
  • 剖析Spring中的设计模式(一) | 工厂观察者
  • 【零基础玩转多模态AI:Gemma3 27B开源视觉模型本地部署与远程访问】
  • 比特币价格重返10万美元,哪些因素使然?
  • 2025上海科技节将于5月17日开幕,拟设6大板块专题活动
  • 复旦设立新文科发展基金,校友曹国伟、王长田联合捐赠1亿助力人文学科与社会科学创新
  • 上海:5月8日起5年以上首套个人住房公积金贷款利率下调至2.6%
  • 谢晖不再担任中超长春亚泰队主教练:战绩不佳主动请辞
  • 习近平在俄罗斯媒体发表署名文章