链表的开头插入一个结点
搜索链表
从链表中删除结点
有序链表
课堂编码与小测
链表的优点之一是可以在任何位置添加结点, 但开头最容易插入.
上一节课堂编码已用过这两条语句, 这里回顾一下: 顺序不能交换.
第一步是修改新结点的成员next, 使其指向之前在链表开头的结点:
new_node->next = first;
第二步是使first指向新结点:
first = new_node;
重点: 即使链表为空, 这些语句也有效——first 为 NULL 时, 新结点的 next 变成 NULL, 它自然成为唯一的结点.
问题: 在链表中搜索整数 n——找到则返回指向该结点的指针, 否则返回空指针.
struct node *search_list(struct node *list, int n)
{
for (struct node *p = list; p != NULL; p = p->next)
if (p->value == n)
return p;
return NULL;
}
struct node *search_list(struct node *list, int n)
{
for (; list != NULL; list = list->next)
if (list->value == n)
return list;
return NULL;
}
虽然while循环也可以搜索链表, 但for语句通常是首选.
访问链表中结点的循环, 使用指针变量p来跟踪"当前"结点:
该函数的初始版本:
还有很多其他的方法来编写search_list.
一种替代方法是消除变量p, 用list本身来跟踪当前结点:
由于list是原始链表指针的副本, 因此在函数中更改它没有损害.
for (cur = list, prev = NULL;
cur != NULL && cur->value != n;
prev = cur, cur = cur->next)
;
链表的一大优势: 删除结点很方便.
删除结点分三步:
找到要删除的结点.
更改前一个结点, 使其"绕过"已删除的结点.
调用 free 回收被删除结点占用的空间.
第 1 步比看起来更难, 因为第 2 步需要更改前一个结点.
这个问题有多种解决方案.
"追踪指针"技术涉及保持指向前一个结点(prev)的指针以及指向当前结点(cur)的指针.
假设 list 指向要搜索的链表, n 是要删除的整数.
实现第 1 步的循环:
当循环终止时, 若找到 n, cur 指向要删除的结点, prev 指向前一个结点; 若 n 不在链表中, cur 为 NULL, 需先检查再删除.
把前面的循环与第 2、3 步合起来, 得到完整的删除函数:
struct node *delete_node(struct node *list, int n)
{
struct node *cur, *prev;
for (cur = list, prev = NULL;
cur != NULL && cur->value != n;
prev = cur, cur = cur->next)
;
if (cur == NULL) return list; /* n 不在链表中 */
if (prev == NULL) list = cur->next; /* 删的是头结点 */
else prev->next = cur->next; /* 绕过 cur */
free(cur); /* 回收内存 */
return list;
}
删除头结点要更新头指针; free 之后 cur 不能再解引用
删除操作只改变指针, 不移动结点; 查找是 O(n), 删除本身 O(1)
cur = list, prev = NULL 后:
结点按数据排序的链表称为有序链表.
插入更难: 新结点并不总是放在链表的开头.
搜索更快: 到达期望结点应该出现的位置后即可停止查找.
插入时先接后断: 先让 new_node->next = cur, 再让 prev->next = new_node; 顺序反过来会先丢掉 cur 之后的链表.
现场演示
三个课时的任务共用同一份 skeleton.c, 按注释分组, 只写函数体.
void print_list(struct node *list): 从头到尾打印每个结点的 value
struct node *append(struct node *list, int n): 把新结点接到末尾 (注意: 空链表时直接返回新结点)
提示: 先想清楚 prev 和 cur 两个指针怎么配合, 再动手
⚡ 在 VS Code 中打开 skeleton.csearch_list 在空链表上调用会返回什么?
free(cur) 之后, 又访问 cur->next 会怎样?