链表入门:从排队逻辑到带头结点单链表实战

发布时间:2026/11/4 14:10:56
链表入门:从排队逻辑到带头结点单链表实战 简介本资源是北京邮电大学数据结构课程首次实验的完整线性表实践报告面向计算机类本科生及算法初学者聚焦带头结点单链表的核心实现与深度剖析。内容涵盖存储结构原理、9大关键算法头/尾插法、析构、按位/按值查找、插入、删除、遍历、求长、复制构造、倒置的代码实现、时间复杂度分析及完整main()函数测试流程辅以清晰的内存示意图与分步执行逻辑有效支撑实验复现与概念内化。资源为1个6.3MB的Word文档.doc结构规范含实验要求、程序分析、运行结果与调试总结等模块便于直接阅读、打印或作为课程笔记参考。已有602人学习下载适合数据结构课堂巩固、实验预习复习及链表操作能力系统训练。1. 线性表不是“表格”而是数据排队的底层规则零基础也能看懂的链表入门第一课你有没有试过在超市结账时队伍突然从中间插进一个人或者前面有人临时改主意不买了整条队就得重新调整——线性表就是计算机里模拟这种「排队逻辑」的最基础模型。它不关心你存的是数字、名字还是照片只管一件事谁在谁前面、谁在谁后面怎么插队、怎么退队、怎么找人。北邮这份数结构实验报告里的「带头结点的单链表」就是用代码把这套排队规则写实了每个“人”叫结点自己带着一张小纸条叫指针上面写着下一个人站哪儿。没人规定大家必须挨着站所以内存可以零散但靠这张纸条就能顺着找到所有人。这不是高深算法而是所有APP、游戏、后台系统背后最底层的“秩序感”来源。如果你刚学编程看到NodeT* p front-next;就头皮发麻如果你连new NodeT是干啥的都拿不准如果你试过抄代码却卡在“程序一闪而过”“报错说类型没定义”——别慌这篇就是为你写的。它不讲大道理只带你亲手搭出第一个能跑通、能输入、能删改、还能倒着念的排队系统。接下来我们从“为什么非得用链表”开始一关一关拆解直到你亲手敲完、编译成功、看到屏幕上打出1 2 3 4 5那一刻。2. 为什么不用数组先搞懂链表存在的真实理由动态扩容、插入删除快、内存不浪费2.1 数组排队的“硬伤”固定长度像订死的座位号想象你去电影院买票售票员递给你一张印着“7排12座”的票——这就是数组。好处是你想找第5个人直接翻到票根上写“5”的位置秒开。但坏处也致命如果队伍突然加进来20个人座位不够了你得整个大厅重排搬椅子、贴新号、通知所有人——对应代码里就是int a[10]想变int a[30]得申请新内存、复制旧数据、释放旧空间慢且容易出错如果第3个人临时有事走了他空出的“3号座”永远空着后面的人不能往前挪否则编号全乱座位就白白浪费更糟的是你想在第2和第3个人中间插个新人那第3号及之后所有人得集体往后挪一个座位——1000个人的队就得动998次时间全耗在路上。提示这些不是理论假设。你在写学生管理系统时班级人数从30变成60写聊天软件时用户消息一条接一条涌进来写编辑器时光标在文字中间疯狂删改……数组的“固定座位”立刻变成拖慢速度的枷锁。2.2 链表排队的“活法”每人带张导航纸条队伍想长就长、想短就短链表换了一种思路不发固定座位票而是给每个人发一张小纸条上面只写“下一个人站哪儿”。第一个人叫头结点不存数据只负责指路后面每个人既存自己的数据比如数字5又写明下一个人的地址。这样加人在队尾或队中随便找个位置让新人站好把他的地址写进前一个人的纸条里再把自己的纸条填上“下一个是谁”——两步操作跟队伍多长无关删人让前一个人的纸条跳过他直接指向后一个人然后把这个人“清空回收”——也是两步找第i个人得从头结点出发按纸条一路问下去“你下一个是几号”“他下一个是几号”……问i次才能到——慢是慢了但胜在灵活。这就是北邮实验里强调“带头结点的单链表”的核心价值它用一点点“找人时间”换来了无限的“插队自由”和“内存省心”。不是它比数组高级而是它解决的是数组根本解决不了的问题。2.3 一个真实场景对比学生选课系统里的“增删查改”假设你开发一个课程报名系统初始有100个学生。用数组实现Student students[100]; // 固定容量 int count 0; // 插入新学生得检查count是否100满了就崩溃或手动扩容 // 删除某学生得把后面所有人往前拷贝count--否则留空洞 // 查找学号为1001的学生for循环遍历O(n)用链表实现北邮这份代码的雏形LinkListStudent courseList; // 容量自动伸缩 // 插入courseList.Insert(5, newStudent); // 在第5位插入内部只改2个指针 // 删除courseList.Delete(3); // 删第3个内部只改1个指针释放内存 // 查找courseList.Locate(1001); // 内部遍历但你不用操心内存够不够看到区别了吗数组让你时刻盯着“容量红线”链表让你专注“业务逻辑”。北邮实验没让你造火箭它让你亲手把“排队自由”这个概念焊进自己的肌肉记忆里。3. 第一步从零搭建你的第一个链表——照着做5分钟跑通打印1 2 3 4 53.1 先理解两个最关键零件结点Node和链表类LinkList别被templateclass T吓住它只是说“这个东西能装任何类型的数据数字、字符、甚至你自己定义的结构体都行。”我们先锁定最简单的int整数。结点Node就是排队的“每个人”。它有两个口袋data口袋装数据比如数字5next口袋装一张纸条写着“下一个人的地址”类型是NodeT*指向另一个Node的指针。代码长这样摘自北邮源码templateclass T struct Node { T data; // 装数据的口袋 struct NodeT* next; // 装“下一个人地址”的纸条 };白话解释struct NodeT* next;不是“下一个Node”而是“下一个Node住在哪栋楼、哪个房间号”。就像你朋友说“我家在朝阳区建国路8号302室”他没把你拉过去只给了地址。链表类LinkList就是管理整支队伍的“队长”。它有个秘密武器——头结点front一个不存数据、只负责指路的“领队”。它的next口袋永远指着队伍里第一个人。templateclass T class LinkList { public: LinkList() { front new NodeT; front-next NULL; } // 无参构造先建领队让他举着“空纸条” private: NodeT* front; // 队长永远站在最前面 };关键细节front-next NULL意思是“领队目前没看到任何人”队伍为空。这是链表安全的起点漏掉这句后续所有操作都会崩。3.2 动手用尾插法创建含5个数字的链表并打印北邮实验提供了两种建队方式头插法新人总插队到最前面、尾插法新人老实排在队尾。我们选更符合直觉的尾插法一步步敲第一步准备数据const int n 5; int a[n] {1, 2, 3, 4, 5}; // 要排队的5个数字第二步创建链表对象调用尾插法构造函数LinkListint myList(a, n); // 这行会触发下面的构造函数第三步看懂尾插法构造函数重点逐行注释templateclass T LinkListT::LinkList(T a[], int n) { // 参数数据数组a长度n front new NodeT; // 1. 先建领队头结点 front-next NULL; // 2. 领队举着“空纸条”队伍初始为空 NodeT* r front; // 3. r是“尾巴指针”当前正指着领队因为队空领队就是尾巴 for (int i 0; i n; i) { // 4. 对每个数字a[i]执行 NodeT* s new NodeT; // 4a. 给新人s分配一个“身体”内存 s-data a[i]; // 4b. 把数字a[i]塞进s的data口袋 r-next s; // 4c. 让当前尾巴r的纸条指向新人ss排到队尾 r s; // 4d. 更新尾巴r现在s成了新尾巴 } r-next NULL; // 5. 最后新尾巴s的纸条写“空”表示队尾结束 }为什么需要r指针因为front是固定的领队不能乱动。r就像一根“尾巴绳”始终拴在最后一个人身上确保新人永远准确接在队尾。第四步打印整个队伍遍历templateclass T void LinkListT::PrintList() { NodeT* p front; // 1. p从领队开始 while (p-next ! NULL) { // 2. 只要领队纸条没写“空”说明后面有人 p p-next; // 3. p走到下一个人顺着纸条走 cout p-data ; // 4. 打印这个人data口袋里的数字 } }注意p front后第一次p p-next就跳过了领队直接到第一个人。所以打印不会出现乱码。第五步在main函数里串起来int main() { const int n 5; int a[n] {1, 2, 3, 4, 5}; LinkListint myList(a, n); // 创建链表 cout 打印链表; myList.PrintList(); // 打印1 2 3 4 5 cout endl; system(pause); // 防止窗口一闪而过北邮实验的血泪经验 return 0; }✅ 现在你已经完成了北邮实验要求的第一步构造链表 打印。把它完整敲进Dev-C或Code::Blocks推荐编译运行屏幕上一定会跳出1 2 3 4 5。这不是魔法是你亲手用指针纸条搭起的第一支数字队伍。4. 避坑指南新手必踩的5个“程序一闪而过”“报错看不懂”陷阱4.1 现象程序编译成功双击运行后黑窗口一闪就消失原因程序执行完main()最后一行就立刻退出Windows系统自动关闭控制台窗口。你根本来不及看清输出结果。解决在main()函数末尾加一句暂停命令。北邮实验用了system(pause);这是最简单粗暴的方法需#include stdlib.h。更规范的做法是加cin.get();需#include iostream等待你按回车才退出。血泪经验我第一次写的时候反复编译十几次以为代码错了其实只是没看到输出。后来养成习惯只要main()里有cout结尾必加system(pause);或cin.get();。4.2 现象编译报错error: LinkList is not a template或error: missing template arguments before ...原因模板类LinkList必须明确告诉编译器“里面装什么类型”。比如LinkListint表示装整数LinkListstring表示装字符串。如果只写LinkList myList;编译器一脸懵“你让我管什么类型”解决所有LinkList对象声明必须带上尖括号类型如LinkListint myList;。北邮实验里LinkListintexample(a, n);就是正确示范。漏掉int是新手最高频错误。提示templateclass T是定义时的“通用模具”LinkListint才是用模具造出的具体“零件”。4.3 现象运行时报错Segmentation fault段错误或直接崩溃原因指针p指向了一个无效地址比如NULL你还强行用p-data去读取。常见于Get(i)查找第i个元素时i超出了链表长度p最终变成NULL但代码没检查就直接p-dataDelete(i)删除时i0或i大于长度p Get(i-1)返回NULL后续p-next就崩了。解决所有涉及p-xxx的操作前先加安全检查if (p NULL) { cout 位置错误超出范围 endl; return; // 或返回默认值 } // 然后再用 p-data北邮源码里Insert和Delete函数开头都有if (p)判断这就是防御式编程。4.4 现象打印链表时开头多出一个奇怪的数字比如0 1 2 3 4 5原因误把头结点front当作了有效数据节点。头结点front的data是未初始化的垃圾值可能是0而PrintList()里如果写成p front; do { cout p-data; p p-next; } while(p ! NULL);就会先打头结点的垃圾值。解决严格遵循北邮代码的遍历逻辑——p front; while (p-next ! NULL) { p p-next; cout p-data; }。关键在while (p-next ! NULL)它保证了p每次进入循环体时p本身已经是有效节点因为p-next存在绝不会访问头结点的data。4.5 现象插入或删除后链表内容错乱比如1 2 3插入99到位置2后变成1 99 99 3原因指针操作顺序错了。典型错误是// ❌ 错误先改p-next再改s-next导致s的next指向了错误位置 p-next s; s-next p-next; // 此时p-next已是ss-next s自己指向自己解决牢记链表操作的黄金顺序——先连新线再断旧线。正确插入在p后插入ss-next p-next; // 1. 新人s先认清楚“下一个是原来p后面那个人” p-next s; // 2. 再让p改纸条写上“下一个是s”类比你要把新人C插到A和B之间。正确做法C先记住B的位置s-next p-next然后A把纸条从“B”改成“C”p-next s。如果A先改纸条C就找不到B了。5. 从“能跑通”到“真理解”动手改三个小功能吃透链表的呼吸感5.1 功能一把“用户输入数组”从硬编码变成交互式北邮实验下一步改进点北邮原代码用int a[n] {1,2,3,4,5};是教学简化实际项目必须让用户自己输。我们来升级修改main函数加入动态输入int main() { int n; cout 请输入学生人数; cin n; int* a new int[n]; // 动态申请数组大小由用户定 cout 请依次输入 n 个数字; for (int i 0; i n; i) { cin a[i]; } LinkListint myList(a, n); // 传给链表构造函数 cout 打印链表; myList.PrintList(); cout endl; delete[] a; // 重要释放动态数组内存 system(pause); return 0; }关键点int* a new int[n]和delete[] a必须配对。不释放会导致内存泄漏——就像租房子不退钥匙房东永远以为房子被占着。5.2 功能二增加“按值删除所有匹配项”不止删第一个北邮Delete(int i)只能按位置删但实际需求常是“删掉所有数字5”。我们加一个新函数在LinkList类中添加成员函数templateclass T void LinkListT::DeleteAll(T x) { // 删除所有值为x的结点 NodeT* p front; // 从领队开始 while (p-next ! NULL) { // 只要p后面还有人 if (p-next-data x) { // 检查p后面那个人是不是目标 NodeT* q p-next; // q是待删者 p-next q-next; // p的纸条跳过q指向q的下一个人 delete q; // 释放q的内存 } else { p p-next; // p往后走继续检查下一个人 } } }在main中测试myList.DeleteAll(3); // 删除所有3 myList.PrintList(); // 原来1 2 3 4 3 5 → 变成1 2 4 5为什么用p-next检查因为要删除p-next必须先拿到p前驱这是单链表的限制。双链表可以前后双向单链表只能“借前驱之手”。5.3 功能三实现“合并两个有序链表”练熟指针游走这是面试高频题。假设有两个升序链表list1: 1 3 5list2: 2 4 6合并成1 2 3 4 5 6。核心思想用两个指针p1、p2分别扫描两个链表谁小谁先接进新链表。写一个独立函数不修改原类便于理解templateclass T LinkListT Merge(LinkListT list1, LinkListT list2) { LinkListT result; // 新建空链表存结果 NodeT* p1 list1.front-next; // list1第一个有效结点 NodeT* p2 list2.front-next; // list2第一个有效结点 NodeT* r result.front; // r是result的尾巴指针 // 当两个链表都没走完时 while (p1 ! NULL p2 ! NULL) { if (p1-data p2-data) { r-next p1; // 把p1接到result尾巴 p1 p1-next; // p1往下走 } else { r-next p2; // 把p2接到result尾巴 p2 p2-next; // p2往下走 } r r-next; // result尾巴更新 } // 把剩余的链表全部接上最多一个非空 if (p1 ! NULL) r-next p1; if (p2 ! NULL) r-next p2; return result; }测试LinkListint list1(a1, 3); // 1 3 5 LinkListint list2(a2, 3); // 2 4 6 LinkListint merged Merge(list1, list2); merged.PrintList(); // 输出1 2 3 4 5 6这个函数没有new和delete因为它只是“重排指针”不申请新内存——高效的关键。6. 下一步学什么一份拒绝焦虑的渐进路线图从链表到真实项目能力6.1 别急着跳坑先吃透这3个“链表思维”习惯很多新手学完链表转身写项目还是懵不是代码不会而是思维没切换。我带过的A同学第一次独立写图书管理系统卡在“借书”功能三天最后发现他还在用数组思维——试图给每本书编号1~100然后维护一个bool borrowed[100]。直到我把北邮链表的Insert和Delete函数往他眼前一放“你看借书从‘在库’链表删一个加到‘已借’链表还书反过来。根本不用管编号”——他当场顿悟。所以先固化这三个本能遇到“可能增删”的数据条件反射想链表用户列表、消息队列、购物车商品凡是数量不确定、频繁变动的优先考虑链表或现代语言的vector/list操作前必问“前驱在哪”单链表无法直接访问前一个结点所以删除、插入位置i本质都是先找到位置i-1的结点即前驱。北邮Delete(int i)里p Get(i-1)就是这个逻辑指针操作后必画图哪怕再简单的s-next p-next; p-next s;也花10秒在纸上画两个方块p和s和箭头。我至今保留这个习惯每次重构复杂链表逻辑第一件事就是摊开草稿纸画节点、标指针、涂掉旧线、画新线。图比代码诚实。6.2 路线图用3周时间把链表变成你的“数据搬运工”不要追求“学完所有数据结构”先让链表为你干活。按周规划周次目标关键动作北邮资源如何用第1周链表熟练工能独立写出增删查改、倒置、合并不查资料每天手写1个函数如Insert不看代码写完立刻画图验证用北邮的main()测试框架把所有函数都跑一遍把北邮main()里的cin部分全改成你自己的测试数据比如Insert(1,99)连续插5次观察指针变化第2周链表调试员能快速定位段错误、空指针、内存泄漏故意制造错误删掉if(p)检查看崩溃位置用cout p p endl;打印指针地址学习用Dev-C的调试器单步执行观察p和p-next的值北邮实验报告第4页“调试过程出现的问题”就是现成的踩坑清单照着复现一遍再修复第3周链表应用家用链表解决1个真实小问题选一个① 学生成绩管理增删学生、按分数排序② 简易浏览器历史记录前进/后退用双向链表③ 文本编辑器光标移动字符用链表存光标是Node* cursor北邮的LinkListT是完美骨架你只需替换T为Student或char重载operator支持打印即可6.3 一个真实教训从那以后我每次写指针都强制走一遍“三问检查”我第一次用链表写文件解析器处理上万行日志时程序随机崩溃。花了两天才发现是在Delete函数里忘了delete q;内存越积越多最后爆掉。从那以后我给自己定了铁律一问生命周期“这个new出来的指针对应的delete写在哪了如果函数提前return会不会漏掉”二问空值“p此刻可能是NULL吗p-next能安全访问吗所有-操作前加if(p)或assert(p)。”三问所有权“这段内存是我申请的还是别人传进来的释放权在我这还是该交给调用者”北邮实验里析构函数~LinkList()完整遍历释放就是所有权意识的体现。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询