逐步演示 creat() 函数如何用 head、p1、p2 三个指针,把输入的学号成绩一条条挂到链表尾部。每一步都和右边的 C 源码同步高亮。
配合上面的动画看,每一条都对应动画里的某个具体步骤。
head —— 永远指向第一个结点,是链表的入口,一旦定下就不再变。
p1 —— 指向刚 malloc 出来的新结点,是个临时工。
p2 —— 永远指向当前最后一个结点,尾插全靠它定位。
p2 也能尾插,但每次都要从 head 走到尾巴,建 n 个结点要 O(n²);有了 p2 直接 O(1) 挂上去,整体 O(n)。p2->next = p1; —— 让老尾巴指向新结点,链表接上了。
p2 = p1; —— 让尾指针跟过去,新结点变成新的尾巴。
p2 = p1,下一轮还是往老尾巴上挂,新结点会把上一个直接覆盖掉。链表为空时 head == NULL,根本没有"老尾巴"可以挂,所以必须让 head = p1 直接把入口定下来。
这也是 if (head == NULL) ... else ... 这个分支存在的唯一原因——它只在第一轮循环走 if,之后全部走 else。
head 而不是 n,所以此时 n++ 已经执行过了,用 n == 1 判断也一样成立。代码是先 malloc 再 scanf,所以读到 0 的时候,内存已经申请下来了。
这个结点不是有效数据,不能挂进链表,但也不能就这么扔掉——必须 free(p1) 还给系统。
malloc 拿到的内存里是随机垃圾值,next 域也一样。
置空之后它才是一个"合法的尾巴"。遍历 for (p = head; p != NULL; p = p->next) 就是靠这个 NULL 判断结束的。
n++ 写在"判断是否为 0"之后、"挂进链表"之前,所以 n 只统计有效结点,结束标志不算。
n 是全局变量,creat() 里加、print() 里用,两个函数靠它共享结点数。
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) 之后 |