单链表尾插法 · 动画讲解

逐步演示 creat() 函数如何用 head、p1、p2 三个指针,把输入的学号成绩一条条挂到链表尾部。每一步都和右边的 C 源码同步高亮。

动态内存 malloc / free struct student 尾插建表 遍历输出 单步可回退

内存与指针变化

阶段:尚未开始
讲解
点击「播放」或「下一步」开始演示。
0 / 0
快捷键:← 上一步 · → 下一步 · 空格 播放/暂停 · R 重置
默认演示 3 条记录 + 结束标志 0。

看懂尾插法的 6 个关键点

配合上面的动画看,每一条都对应动画里的某个具体步骤。

1三个指针各管一件事

head —— 永远指向第一个结点,是链表的入口,一旦定下就不再变。

p1 —— 指向刚 malloc 出来的新结点,是个临时工。

p2 —— 永远指向当前最后一个结点,尾插全靠它定位。

没有 p2 也能尾插,但每次都要从 head 走到尾巴,建 n 个结点要 O(n²);有了 p2 直接 O(1) 挂上去,整体 O(n)。

2尾插的核心就是这两行

p2->next = p1; —— 让老尾巴指向新结点,链表接上了。

p2 = p1; —— 让尾指针跟过去,新结点变成新的尾巴。

两行顺序不能颠倒,也不能省第二行。省了 p2 = p1,下一轮还是往老尾巴上挂,新结点会把上一个直接覆盖掉。

3第一个结点为什么单独处理

链表为空时 head == NULL,根本没有"老尾巴"可以挂,所以必须让 head = p1 直接把入口定下来。

这也是 if (head == NULL) ... else ... 这个分支存在的唯一原因——它只在第一轮循环走 if,之后全部走 else。

判断依据是 head 而不是 n,所以此时 n++ 已经执行过了,用 n == 1 判断也一样成立。

4结束标志 0 为什么要 free

代码是先 malloc 再 scanf,所以读到 0 的时候,内存已经申请下来了。

这个结点不是有效数据,不能挂进链表,但也不能就这么扔掉——必须 free(p1) 还给系统。

不 free 就是内存泄漏。程序小看不出来,但这是面试和考试的高频考点。

5p1->next = NULL 不能省

malloc 拿到的内存里是随机垃圾值,next 域也一样。

置空之后它才是一个"合法的尾巴"。遍历 for (p = head; p != NULL; p = p->next) 就是靠这个 NULL 判断结束的。

漏掉这行,最后一个结点的 next 指向野地址,遍历时直接段错误(Segmentation Fault)。

6n 的自增时机

n++ 写在"判断是否为 0"之后、"挂进链表"之前,所以 n 只统计有效结点,结束标志不算。

n 是全局变量,creat() 里加、print() 里用,两个函数靠它共享结点数。

空链表(第一次就输入 0)时,head 返回 NULL,n = 0,print 的 for 循环一次都不执行。
对比项尾插法(本例)头插法
关键语句p2->next = p1; p2 = p1;p1->next = head; head = p1;
是否需要尾指针需要 p2,否则每次都要走到尾巴不需要,直接改 head
结点顺序和输入顺序一致(101→102→103)和输入顺序相反(103→102→101)
第一个结点特殊处理需要,要判断 head == NULL不需要,逻辑统一
建表时间复杂度O(n)O(n)
典型用途按录入顺序保存数据、读文件建表逆序输出、链表原地反转
容易写错的地方后果正确做法
忘记 p1->next = NULL;尾结点 next 是野值,遍历崩溃挂进链表前一定先置空
忘记 p2 = p1;新结点覆盖上一个,链表只剩 2 个结点每次尾插后立即更新尾指针
写成 p1->next = p2;指针方向反了,链表断裂成环外散链是"老的指向新的",不是反过来
结束标志不 free内存泄漏free(p1); break;
malloc 后不判空内存不足时对 NULL 解引用,程序崩溃保留 if (p1 == NULL) 分支
n++ 放在判 0 之前结束标志也被计数,n 多 1放在 if (p1->num == 0) 之后