1. 你为什么值得花一下午做一个链表学生管理系统
先抛结论:这个项目虽然顶着“课程设计”的名头,但只要你真正动手写完,C语言里最难啃的指针、结构体、动态内存这三座大山,基本就能翻过去一大半。我见过太多人卡在链表这块,不是说看不懂代码,而是“能看懂但自己写不出来”。你让他讲原理,他能背出“链表是由一系列节点组成,节点之间通过指针相连”这种标准答案,但真要他手写一个节点的插入和删除,立马就露馅了。
这个学生信息管理系统正好治这个病。它是一个标准到不能再典型的链表应用场景:学生的学号、姓名、成绩,天然就是一组结构体数据;增删改查,正好对应链表的插入、删除、遍历、查找;加上排序和文件存取,链表的核心操作就全齐了。做完这个项目,你会深刻理解为什么很多底层系统和嵌入式开发至今还在大量使用C语言和链表——因为它够简单、够直接、够高效。
适合谁来写?两类人。一类是正在学数据结构、被链表折磨得够呛的大学生,你缺的不是更多理论讲解,而是亲手把一个完整项目跑通的经验;另一类是准备找工作、想快速回顾C语言核心功底的开发者,这个项目能在两三个小时内帮你把指针操作和内存管理的敏感度重新提起来。下面我把自己当时做这个项目时踩过的坑、琢磨过的细节、反复调试过的代码,全部摊开讲给你听。
2. 方案选型背后的真实考量
2.1 为什么是链表而不是数组
其实用数组做学生信息管理系统,代码写起来会简单得多——一个固定大小的结构体数组,配合下标运算,怎么都比指针操作直观。如果你只想“交个作业”,数组方案确实省事,一个晚上就能写完。但这里有个很现实的问题:数组的长度是死的,你定义100个元素的数组,最多只能存100个学生。哪天要加数据,得改代码重新编译;中间删掉一个学生,后面的所有数据都得往前挪;插入一个新学生,位置后面的元素也要整体后移。这些操作的时间复杂度是O(n)不说,代码逻辑里还全是边界判断,一不小心就数组越界。
链表恰恰把这些痛点全部解决了。节点是动态malloc出来的,系统有多少学生就占多少内存,理论上可以一直加下去;删除一个节点,只需要把前一个节点的next指针绕过它;中间插入节点,改两个指针就行。数据结构课上讲“链表适合频繁插入删除、长度不确定的场景”,这句话背一百遍都不如你在一个真实项目里体验一次来得深刻。
当然,链表也有自己的代价。查找第k个元素必须从头开始遍历,没法像数组那样直接下标定位;每个节点还要额外存储一个next指针,有内存开销;最麻烦的是,所有指针操作都必须自己负责,野指针、内存泄漏、段错误这些坑全都等着你呢。这正是做这个项目的价值所在——你用链表的劣势换来了宝贵的内存管理和指针调试经验。
2.2 为什么用C语言而不是C++或Java
很多同学会问,Java里有ArrayList,C++里有vector,底层链表都帮你封装好了,直接调用不就行了吗?这话没错,但你要明白,那些封装好的容器类,底层实现逻辑才是最值得学习的东西。你用Java的LinkedList用得再熟练,也不代表你懂链表——你只是懂API的调用而已。
C语言在这方面是最“裸”的。没有垃圾回收,你malloc出来的内存必须自己free,不然就泄露;没有类型安全机制,指针可以随意指向任何地址,一个误操作可能直接让程序崩溃;没有异常处理,错误只能靠返回值判断。正是这种“什么都要自己管”的特性,逼着你去理解链表每个操作背后的内存变化和指针指向。等你用C语言把链表写完一遍,再去用Java的LinkedList或者C++的list,你会发现自己能“看见”它们底层到底干了什么。
还有一层考虑是适用性。C语言是嵌入式、操作系统、数据库、网络协议栈这些领域的基石,链表在这些场景中无处不在——任务队列是链表,内存分配器的空闲块列表是链表,文件系统的目录项组织也常涉及链表。你早点用C把链表的每一根指针都摸透,后面学这些内容的时候会轻松很多。
2.3 功能模块拆解:一个“麻雀虽小五脏俱全”的系统
做任何项目,动手写代码前先把功能想清楚,否则写着写着就会变成一锅粥。我当时给这个系统规划了5个核心模块:
- 学生信息的增加:支持头部插入也行,但更合理的是插入时保持学号有序,或者插入到指定位置
- 学生信息的删除:按学号删除,这是最常见的需求
- 学生信息的修改:按学号查找,修改姓名和成绩字段
- 学生信息的查询:按学号精确查询、按姓名模糊搜索、按成绩范围筛选
- 学生信息的排序与展示:按学号排序、按成绩排序,正序倒序都要支持
- 文件读写:把链表存到磁盘文件,下次启动能读回来
功能看着不少,但落到链表操作上,其实都是那几种基本操作的组合:建表、遍历、插入、删除、查找。这也是这个项目最好的地方——用有限的功能把链表的核心操作全部覆盖到了,每一个功能都能加深你对链表某个方面的理解。
3. 链表核心细节与C语言的那几个坑
3.1 节点定义:一个结构体里藏着所有秘密
链表的节点结构体是整棵树的根,先把它定义明白。我当时写的时候一开始用的是双向链表还是单向链表纠结了好一会儿——因为学生管理系统里既要正着遍历,又经常要删一个节点后拿前一个节点,双向链表在删除的时候不需要找前驱,省事不少。但考虑到国内大部分教材的单链表课程设计要求,也为了让你把“找前驱”这个核心操作练扎实,我用的是带表头节点的单向链表。下面是节点定义:
typedef struct Student { int id; // 学号 char name[20]; // 姓名 float score; // 成绩 struct Student *next; // 指向下一个节点的指针 } Student;这里有几个细节值得说。首先是self-referential structure——结构体里包含一个指向自身类型的指针。很多初学者第一次看到这个会蒙:这结构体到底多大?我告诉你,next只是一个指针变量,固定占用4字节(32位系统)或8字节(64位系统),不会递归变大的。你可以把next理解成一个“门牌号”,它不存学生数据本身,只告诉你下一个学生的地址在哪儿。
再一个是姓名用定长字符数组而不是字符指针。用char *name当然也行,但那就得每个节点单独malloc一块内存来存字符串,还得记得在删除节点时先释放这块字符串内存再释放节点内存。为了保证“删除节点 = 释放一块内存”这个简单模型,我用定长数组。如果你确实想练练二级指针和字符串内存管理,可以自己改成字符指针版本,但建议先把基础版本跑通再说。
3.2 表头节点到底要不要,我的答案是必须
链表有两种建法:有表头节点(header node)的和没有表头节点的。所谓表头节点,就是链表最前面那个不存数据、只作为入口的节点,它的next指向真正的第一个数据节点。
我强烈建议你用带表头节点的版本。原因很朴素:没有表头节点时,插入第一个节点要改头指针本身,删除最后一个节点也要改头指针本身,这意味着你需要用到指针的指针(Student **head)或者在函数里返回新的头指针,代码逻辑特别容易绕晕。有了表头节点后,头指针head始终指向那个固定的表头节点,不管链表是空还是满,head永远不会变,插入和删除都只需要操作某个节点的next指针就行了,空表和非空表的操作也能统一处理。
用图来表示大概是这个样子:
head -> [表头节点] -> [学生1] -> [学生2] -> [学生3] -> NULL表头节点虽然不存数据,但它占用的内存同样是malloc出来的,记得在程序退出的时候也要free掉它。这是很多人容易漏掉的细节。
3.3 链表的增删改查:每个操作背后都是“改指针的顺序问题”
很多人代码能看懂但自己写不出来,根本原因是没把“改指针的顺序”想清楚。我一个个来说。
插入一个新节点newNode到节点prev的后面,核心操作是两步:
newNode->next = prev->next; // 第一步:新节点的next 先指向 prev 的原后继 prev->next = newNode; // 第二步:prev 的next 指向新节点这两步顺序绝不能反。你要是先执行prev->next = newNode,那么prev原来的后继节点就“丢失”了——它的地址没有被任何变量保存,链表就断掉了。可以类比成一个接力赛:你要让新人插入到两人中间,必须让新人先接住后面人递来的棒(第一步),再松手让前面人的手接到新人(第二步)。顺序反了,棒就掉地上了。
删除节点delNode时,链表不会自动“跳过”被删除的节点,必须手动把它前一个节点的next指向它的后一个节点:
prev->next = delNode->next; free(delNode);这里有一个关键问题:对于单向链表,我们没法知道一个节点的前驱是谁,除非从头遍历。所以删除操作必须带着“记录前一个节点”的遍历方式来做:
Student *prev = head; Student *cur = head->next; while (cur != NULL && cur->id != target_id) { prev = cur; cur = cur->next; } if (cur != NULL) { prev->next = cur->next; free(cur); }这才是单链表删除的完整形态。找到目标节点不是终点,能找到它的前驱才是重点。
至于“改”和“查”,本质上就是遍历过程中比较节点的id字段,找到匹配的节点后修修改改或者打印出来,没什么特别的难度。真正会出问题的地方,我放到下一节的代码实操里结合着讲。
3.4 内存管理:malloc和free必须成双成对
C语言程序员和段错误之间,只有一行代码的距离。链表项目里最常见的内存问题就是我前面反复提的:malloc了没free,漏了;free了还接着使用,炸了。
我给你一个铁律:每一个malloc都必须有对应的free,放内存操作要么不发,发了就必须让被释放的内存不再被任何代码引用。按这个来,项目里要清理内存的地方有几处:
- 销毁整个链表:遍历所有节点,挨个free,最后free表头节点
- 删除单个学生:找到节点,前驱先链接好,再free掉目标节点
- 程序正常退出之前,释放所有链表内存
还有一个很有用的习惯:free之后立刻把指针置为NULL。比如free(cur); cur = NULL;,这样就算后面不小心再访问cur->id,会立刻触发空指针崩溃,而不是更隐蔽的“访问已释放内存”的未定义行为。未定义行为最难排查,因为它可能随机表现为程序崩溃、数据错乱、甚至“看起来一切正常”,但过段时间换个场景就炸了。能立刻崩溃反而是好事,至少你能快速定位。
3.5 再看一眼:为什么这项目值得用笔记下每个细节
我觉得学链表最大的痛苦不是“概念不懂”,而是“代码看完觉得自己懂了,合上书本却一行都写不出来”。为什么会这样?因为链表的代码非常依赖“在脑子里模拟指针指向”。你得在纸上画一画:这个节点的next在操作前后分别指向谁?指针修改后还有没有被错误使用的旧值?
我给你的建议是:手写代码之前,一定要拿一张纸,画出一个完整的链表,然后模拟一遍插入和删除的指针变化过程。宁可慢,也要画明白。等你在纸上把连续几个节点画熟了,再上机写代码,你会发现思路清晰到飞起。这个习惯我从这个项目一直坚持到现在,写复杂数据结构时,画图永远比直接敲代码高效得多。
4. 系统落地实操:从空链表到一个能跑的学生信息管理系统
4.1 功能规划与菜单设计
动手写代码之前,我先在纸上把系统的菜单画了出来,确保每个功能都有明确的入口。最终规划是这样的:
-------------------- 学生信息管理系统 -------------------- 1. 添加学生信息 2. 删除学生信息(按学号) 3. 修改学生信息(按学号) 4. 查找学生信息(按学号/按姓名/按成绩区间) 5. 显示所有学生(按学号排序或按成绩排序) 6. 从文件加载数据 7. 保存数据到文件 0. 退出系统用户入口用主函数里一个死循环配合switch来实现,每次执行完一个功能后打印菜单等待下一次输入,退出时才跳出循环。这是所有控制台小系统最经典的结构,简单稳妥,不易出错。
4.2 初始化链表和添加学生:头插法还是尾插法
创建链表的第一步是分配表头节点并将其next置为空:
Student *initList() { Student *head = (Student *)malloc(sizeof(Student)); if (head == NULL) { printf("内存分配失败!\n"); exit(1); } head->next = NULL; return head; }这里malloc之后一定要判空是C语言的基本素养。系统内存紧张时malloc会返回NULL,你如果不检查直接往下操作,就是对空指针解引用,程序立刻崩溃。
添加学生有两种策略:头插法和尾插法。头插法代码简单,新节点永远插在表头的后面,但数据顺序会是反的,打印出来和输入顺序不一致。尾插法需要维护一个尾部指针,或者每次都从头遍历找到最后一个节点,代码相对复杂,但能保持输入顺序。为了展示效果更自然,我选择了尾插法,每次从表头遍历到NULL,然后挂上新节点:
void appendStudent(Student *head, Student *newNode) { Student *p = head; while (p->next != NULL) { p = p->next; } newNode->next = NULL; p->next = newNode; }有的同学会问每次都从头遍历,效率不高怎么办。这个问题问得好,但对于一个课程设计级别的学生信息管理系统来说,数据量充其量几百上千条,线性遍历的耗时完全感知不到。算法优化的前提是先有能跑的正确实现,过早优化在个人项目里纯属浪费时间。
4.3 核心函数逐个拆解:插入、删除、修改、查找、排序
先看插入。我上面那个系统里的“添加学生”,除了直接append到尾部之外,我还做了一个按学号有序插入的版本,这样系统里学生的数据能始终按学号排列,展示时更整齐。有序插入的核心逻辑是先找到插入位置:
void insertSorted(Student *head, Student *newNode) { Student *p = head; // 找到第一个学号比 newNode 大的节点,插在它前面 while (p->next != NULL && p->next->id < newNode->id) { p = p->next; } // 此时 p->next 指向第一个比 newNode->id 大的节点 newNode->next = p->next; p->next = newNode; }如果学生总数不多,这个版本够用。防线在这个函数里有两层:第一,如果链表为空,p就是表头节点,p->next为NULL,新节点直接挂在后面;第二,如果新节点的学号最大,循环会一直走到结尾,newNode->next为NULL,它会被挂在链表末尾。这两层边界情况都不需要额外判断,这就是带表头节点的好处。
删除函数的完整版本我在前面已经给过了。再补一个细节:如果你要实现按学号删除,但系统里不存在这个学号的学生,应该打印一句“未找到该学生”,而不是什么都不做。用户的每次操作都需要有明确的反馈,这是用户体感的一部分,别忽略。
修改函数比删除简单,因为不需要改链表结构,只需要改节点的数据字段:
void modifyStudent(Student *head, int id, char *newName, float newScore) { Student *p = head->next; while (p != NULL && p->id != id) { p = p->next; } if (p != NULL) { strcpy(p->name, newName); p->score = newScore; } else { printf("未找到学号为 %d 的学生\n", id); } }查找的逻辑也是遍历+比较。这里我可以分享一个实用变化:把查找拆成“按学号精确查找”和“按姓名模糊查找”。模糊查找的核心是C标准库的strstr函数,它能判断一个字符串是否是另一个字符串的子串,比如输入“张”,就能匹配所有姓张的学生,用户体验比必须输入完整姓名好很多。
// 按姓名模糊查找,匹配到就打印这条记录 void searchByName(Student *head, const char *name) { Student *p = head->next; while (p != NULL) { if (strstr(p->name, name) != NULL) { printf("%d %s %.2f\n", p->id, p->name, p->score); } p = p->next; } }排序这块,我先讲一个很多人忽略的问题:链表排序有两个思路,交换节点里的数据,或者交换节点之间的指针连接。交换数据实现简单,但代价是复制结构体,如果结构体很大,复制成本就高;交换指针更“像链表原生的做法”,但实现起来要处理多个前驱后继关系,很容易写错。对课程设计来说,交换数据已经完全够用,而且代码可读性强得多。我当时用的是冒泡排序,外层循环记录节点个数,内层循环交换两个相邻节点的数据字段:
void sortByScore(Student *head) { int count = getListLength(head); for (int i = 0; i < count - 1; i++) { Student *p = head->next; for (int j = 0; j < count - 1 - i; j++) { if (p->score < p->next->score) { // 交换两个节点的成绩数据 Student temp = *p; p->id = p->next->id; strcpy(p->name, p->next->name); p->score = p->next->score; p->next->id = temp.id; strcpy(p->next->name, temp.name); p->next->score = temp.score; } p = p->next; } } }这个函数交换的是整个节点的数据值,而不是拆链重接。直接对指针解引用拿到结构体然后整体赋值Student temp = *p,这是C语言结构体赋值的天然能力,用起来非常省事。如果你追求更高阶的“指针交换”排序,可以参考下面这个思路:记录当前节点的前驱,通过修改前驱的next来调换相邻节点顺序。这个方法涉及三个节点的指针修改,容易出错,但完成那一瞬间对链表的理解会上一个台阶。
4.4 文件存取:让数据活到下次启动
学生信息管理系统如果真的只是一个内存程序,关闭后数据全丢,那就太鸡肋了。文件存取是很多课程设计容易偷懒不做、但实际工作量并不大的功能,强烈建议你做上。它涉及的知识点是标准的C文件操作,但和链表结合之后能让你更理解“序列化”这件事——把内存中结构化的数据写到磁盘,再从磁盘恢复。
先看保存:
void saveToFile(Student *head, const char *filename) { FILE *fp = fopen(filename, "w"); if (fp == NULL) { printf("无法打开文件 %s\n", filename); return; } Student *p = head->next; while (p != NULL) { fprintf(fp, "%d %s %.2f\n", p->id, p->name, p->score); p = p->next; } fclose(fp); printf("数据已保存到 %s\n", filename); }再看加载。加载时每读一行就创建一个新节点,然后用前面写过的append函数挂到链表的尾部:
void loadFromFile(Student *head, const char *filename) { FILE *fp = fopen(filename, "r"); if (fp == NULL) { printf("文件 %s 不存在,跳过加载\n", filename); return; } int id; char name[20]; float score; while (fscanf(fp, "%d %s %f", &id, name, &score) == 3) { Student *newNode = (Student *)malloc(sizeof(Student)); newNode->id = id; strcpy(newNode->name, name); newNode->score = score; appendStudent(head, newNode); } fclose(fp); printf("已从文件 %s 加载数据\n", filename); }注意fscanf的返回值判断。fscanf返回的是成功匹配并赋值的参数个数,我期望每次读到3个值,所以判断== 3。如果读到文件末尾或者遇到格式错误的行,返回值就不是3,循环自然终止。这里如果不判返回值,程序就会在文件结束时崩溃或者读到一堆脏数据。
4.5 释放内存:优雅退出是C程序最后的体面
程序退出前,必须释放整个链表占用的内存。这是C语言程序“有头有尾”的体现,也是预防内存泄漏的必修课:
void destroyList(Student *head) { Student *p = head; while (p != NULL) { Student *tmp = p; p = p->next; free(tmp); } }这里的关键是“先存下一个节点的指针,再释放当前节点”。你不能先free掉当前节点再去访问它的next,因为free之后那块内存的内容已经不属于你了,访问它完全是未定义行为。先tmp = p、p = p->next、free(tmp)三步走,是链表遍历释放的标准范式,写熟它,后面学销毁二叉树、销毁图的时候都是同一套思路。
4.6 一个完整主控制流程示例
把所有功能拼在一起,主函数长这样:
int main() { Student *head = initList(); loadFromFile(head, "students.txt"); int choice; while (1) { printMenu(); printf("请输入你的选择:"); scanf("%d", &choice); switch (choice) { case 1: { int id; char name[20]; float score; printf("请输入学号、姓名、成绩:"); scanf("%d %s %f", &id, name, &score); Student *newNode = (Student *)malloc(sizeof(Student)); newNode->id = id; strcpy(newNode->name, name); newNode->score = score; insertSorted(head, newNode); break; } case 2: { int id; printf("请输入要删除的学号:"); scanf("%d", &id); deleteById(head, id); break; } // 其余 case 类似处理... case 0: saveToFile(head, "students.txt"); destroyList(head); printf("感谢使用,再见!\n"); return 0; default: printf("无效选项,请重新输入\n"); } } }有一点要注意:输入学号姓名成绩这种混合输入,scanf经常会把上一次输入留在缓冲区里的换行符一起吞掉或者卡住。更稳妥的办法是用getchar()消费掉每个输入后的残留换行,或者用完scanf后用while (getchar() != '\n');清理一次输入缓冲区。这个细节看起来微不足道,但很多同学第一次跑程序时,就是在这里遇到“为什么我什么都没输入程序就跳过了”的诡异情况。
5. 常见问题与排查技巧实录
5.1 段错误(Segmentation Fault)
段错误应该是我在写链表期间见过最多的问题。它的本质是程序访问了非法内存地址,常见的触发原因有三个:
- 对NULL指针解引用:比如链表是空的,你直接访问
head->next->id - 访问已经free掉的内存:比如删除节点后没有把指针置空,后续又用它来操作
- 指针未初始化:定义了
Student *p;就直接用,p里面是随机值,自然指向随机地址
我的排查思路是:先确认崩溃点的行号(用gdb或加打印),然后回头看那个位置访问的是哪个指针,再倒推这个指针指向的内存是否合法。90%的情况能在两分钟内定位到问题。另外,调试验证初始化很有用:写代码时每声明一个指针就立刻初始化为NULL或赋值,禁止使用“握着随机地址的野指针”。
5.2 死循环和内存泄漏
链表操作中死循环高发的场景有两处:一是在链表里找最后一个节点时,while (p != NULL)和while (p->next != NULL)用混了,导致永远走不到结尾;二是遍历时忘记把p = p->next写进循环体,p一直在原地打转。
内存泄漏则没有直观症状,你的程序照常运行,但内存越占越多。排查手段是用回收工具(比如Linux下的valgrind)跑一遍,它会明确报告哪一行分配的内存没有释放。养成习惯之后我基本不用工具也能靠代码审查自己追上。
valgrind --leak-check=full ./student_systemvalgrind的输出里会明确告诉你“在某个函数里malloc了X字节但从未释放”,照着提示逐个修就好。
5.3 输入缓冲区的坑
scanf这个函数看似简单,坑却不少。典型场景:连续两次scanf,第一次输入学号按了回车,第二次想输入姓名却发现程序直接“跳过”了,根本没等你打字。
原因就是第一次scanf从缓冲区读走了数字,但把换行符留在了缓冲区内。第二次scanf要读字符串时,它遇到了缓冲区里开头的换行符,%s会自动跳过空白字符(包括换行、空格、制表符),所以你应该能正常读到姓名。但如果你用的是%c读单个字符(比如菜单选项),这个换行符就会被%c直接读走,让你什么都输入不了。
解决方案有几种:在菜单输入用scanf(" %c", &choice),在%c前面加一个空格跳过空白字符;或者每次scanf后主动清空缓冲区。我推荐第一个方案,简洁有效。如果你用%d读数字,其实不用太担心换行符,因为%d会跳过前导空白。
5.4 排序结果不对,数据错乱
如果冒泡排序后打印的数据出现错乱,常见的错误来源是交换逻辑写得不完整。我前面的sortByScore里交换了三组数据:id、name、score。你如果只交换了score,那么别的字段对不上,就出现“张三的成绩变成李四的,但名字还是张三”这种尴尬场景。
更稳妥的写法是像前面那样用结构体临时变量就近交换:
Student temp = *p; *p = *(p->next); *(p->next) = temp;这三行直接完成整个结构体的交换,比逐个字段复制省心得多。注意*(p->next) = temp这行,它把临时变量中保存的原p节点数据写回到p->next指向的内存,天然避免了“先改p却影响到p->next”的连带错误。
5.5 文件读写的编码与格式问题
用fprintf保存时,我用了空格分隔字段。用fscanf读回时,也按空格分隔匹配。如果某一天你改用逗号分隔,记得读写两边要同步。另外,姓名如果包含中文,不同平台下的文件编码可能不一致,如果你用系统自带的记事本打开保存的文件看到乱码,不用慌,那是编码显示问题,程序内部读回来是正常的。真正的错误是文件里混入了非预期字符,导致fscanf匹配失败,循环提前终止,这时候就要检查数据文件的格式了。
6. 写在最后的几个实操心得
做完这个项目,我最大的体会是:链表不是你“看会的”,而是“写会的”。那些代码看得头头是道、一动手就卡壳的人,多半是缺少了在纸上画图的环节。强烈建议你拿到这个项目后,不要急着看别人的完整源码,而是先自己按功能画链表结构图,再动手写代码。写不出来的地方再去查、再看、再问,效果比直接抄一遍好十倍。
另一个心得是,这个项目做完后不要扔,继续往里加功能,是把它升级成双向链表,还是改成循环链表,是增加一个“按学院分组的二级链表索引”,还是把存储换成二进制文件,都能衍生出新的练习。我在做完单链表版本后,又花了一天把它改成了双向链表版本,体验了一下“有了前驱指针删除时到底多爽”。这种从“能跑”到“真正理解每个操作为什么这样写”的跨越,正是刷数据结构的乐趣所在。
按我个人经验,你只要把这个项目完完整整写一遍,再去面对考试里的链表算法题,或者面试中被问到的链表反转、环检测、合并有序链表这些题,都会有一种“不过如此”的感觉。因为那些题本质上都还是指针操作和边界条件的组合,你已经在学生信息管理系统的开发中锤炼过很多遍了。
如果写的过程中卡在某个问题上超过两小时还弄不明白,我的建议是:先关掉电脑,拿一张纸画一画指针的指向,画完继续来。这个方法解决了我自己当年90%的链表难题,希望你也能试试。