50、链表操作:头插法、尾插法、删除节点
链表这东西,说简单也简单,说难也难。我见过不少写了三年代码的同事,一碰到链表删除节点就翻车。说白了,链表就是一堆节点串起来,每个节点里存着数据和指向下一个节点的指针。但就是这「指针」二字,坑了多少人。
今天咱们就把链表的三个基本操作——头插法、尾插法、删除节点——彻底讲透。嗯,我保证,看完这一章,你写链表代码的手不会再抖。
链表节点的定义
先看最基本的节点结构。我个人习惯用 typedef 把结构体重命名一下,这样写起来清爽很多。
typedef struct Node {
int data; // 数据域
struct Node *next; // 指针域,指向下一个节点
} Node;
这里要注意:struct Node *next 不能写成 Node *next,因为 typedef 还没生效呢。这个顺序问题,我当年刚学的时候也栽过跟头。
头插法
头插法,就是把新节点插到链表的最前面。每次插入的新节点都成为新的头节点。
为什么要用头插法?最常见的场景就是「逆序构建链表」。你想想看,如果你有一组数据,想用链表存起来,并且希望遍历时是逆序的,头插法就是最佳选择。
Node *headInsert(Node *head, int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
return head;
}
newNode->data = data;
newNode->next = head; // 新节点指向原来的头节点
head = newNode; // 更新头节点
return head;
}
核心就两行:newNode->next = head 和 head = newNode。顺序不能反。我曾经见过有人写成 head = newNode; newNode->next = head;——这等于新节点指向了自己,链表直接成环了。
尾插法
尾插法,就是把新节点加到链表末尾。这样构建出来的链表,顺序和输入顺序一致。
尾插法比头插法麻烦一点,因为你得先找到尾巴。如果每次都从头遍历到末尾,效率是 O(n)。我建议维护一个尾指针,这样插入就是 O(1)。
Node *tailInsert(Node *head, int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
return head;
}
newNode->data = data;
newNode->next = NULL;
if (head == NULL) {
// 链表为空,新节点就是头节点
return newNode;
}
Node *cur = head;
while (cur->next != NULL) {
cur = cur->next;
}
cur->next = newNode;
return head;
}
这里有个细节:如果链表为空,新节点既是头也是尾。很多新手会忘记这个判断,直接去遍历,结果 head->next 就段错误了。
typedef struct { Node *head; Node *tail; } LinkedList;。这样插入时直接操作 tail,不用每次都遍历。
删除节点
删除节点是链表操作里最容易出错的。为什么?因为你要处理指针的重新连接,还要释放内存。漏了任何一步,要么内存泄漏,要么野指针。
删除节点分三种情况:
- 删除头节点:直接让 head 指向第二个节点,然后释放原头节点。
- 删除中间节点:找到待删节点的前一个节点,让它跳过待删节点,指向待删节点的下一个节点。
- 删除尾节点:找到倒数第二个节点,把它的 next 置为 NULL,然后释放尾节点。
Node *deleteNode(Node *head, int target) {
if (head == NULL) {
return NULL;
}
// 删除头节点
if (head->data == target) {
Node *temp = head;
head = head->next;
free(temp);
return head;
}
// 删除非头节点
Node *prev = head;
Node *cur = head->next;
while (cur != NULL) {
if (cur->data == target) {
prev->next = cur->next;
free(cur);
break;
}
prev = cur;
cur = cur->next;
}
return head;
}
嗯,这里要注意:删除节点后,那个节点就不存在了。如果你之前有其他指针指向它,那些指针就成了野指针。我在项目中遇到过这样的 bug——一个全局指针指向了某个节点,节点被删了,全局指针还在用,结果程序随机崩溃,查了两天才找到原因。
三种操作对比
| 操作 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 头插法 | O(1) | O(1) | 逆序构建链表、栈 |
| 尾插法(无尾指针) | O(n) | O(1) | 顺序构建链表 |
| 尾插法(有尾指针) | O(1) | O(1) | 频繁尾部插入 |
| 删除节点 | O(n) | O(1) | 按值删除 |
核心逻辑流程图
下面这张图展示了链表三种操作的核心流程。我特意把容易出错的地方标红了,你写代码的时候可以对照着看。
完整示例:综合使用三种操作
下面这个例子,把三种操作串在一起。先尾插法构建链表,再头插法插入一个节点,最后删除指定节点。你可以跑一下看看效果。
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
void printList(Node *head) {
Node *cur = head;
while (cur != NULL) {
printf("%d -> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
int main() {
Node *head = NULL;
// 尾插法构建链表: 1 -> 2 -> 3
head = tailInsert(head, 1);
head = tailInsert(head, 2);
head = tailInsert(head, 3);
printf("尾插法构建: ");
printList(head);
// 头插法插入 0: 0 -> 1 -> 2 -> 3
head = headInsert(head, 0);
printf("头插法插入0: ");
printList(head);
// 删除节点 2: 0 -> 1 -> 3
head = deleteNode(head, 2);
printf("删除节点2: ");
printList(head);
// 释放剩余节点
Node *cur = head;
while (cur != NULL) {
Node *temp = cur;
cur = cur->next;
free(temp);
}
return 0;
}
- 头插法:新节点指向头,更新头指针。顺序不能错。
- 尾插法:找到尾巴再插入。空链表要单独处理。
- 删除节点:分头、中、尾三种情况。记得 free,记得处理指针。
- 所有操作都要检查 malloc 返回值。内存分配失败不是小事。
链表操作,说白了就是指针的重新指向。你只要画清楚每个节点之间的连接关系,代码自然就写对了。我每次写链表代码之前,都会在草稿纸上画一遍指针的指向变化——这个习惯帮我避免了很多低级错误。
嗯,链表这块就讲到这里。记住:多画图,少踩坑。
公众号:蓝海资料掘金营,微信deep3321