1 引言
数组有一个明显的缺点:插入或删除元素时,需要移动大量数据。例如,在数组开头插入一个元素,需要将所有元素向后移动一位。
c
/* 数组在开头插入元素需要移动所有元素 */
for (int i = size; i > 0; i--) {
arr[i] = arr[i-1];
}
arr[0] = new_value;
链表通过指针连接各个节点,插入和删除只需修改指针,不需要移动数据。
c
/* 链表的节点结构 */
typedef struct Node {
int data; /* 数据域 */
struct Node *next; /* 指针域,指向下一个节点 */
} Node;
每个节点包含数据和指向下一个节点的指针,最后一个节点的指针为 NULL。这种结构就是单向链表。
2 链表节点的结构体定义
2.1 节点结构
单向链表节点包含两个部分:
-
数据域:存储实际数据(可以是任意类型)
-
指针域:指向下一个节点
c
typedef struct Node {
int data; /* 数据域 */
struct Node *next; /* 指针域 */
} Node;
注意 :在结构体内部,不能直接用 Node*,因为 Node 这个类型名还没有定义完成,必须用 struct Node*。
2.2 带头节点 vs 不带头节点
链表有两种常见形式:
| 类型 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| 带头节点 | 有一个不存储数据的头节点 | 操作统一,不需要单独处理空链表 | 多占用一个节点空间 |
| 不带头节点 | 头指针直接指向第一个数据节点 | 节省空间 | 插入删除需要单独处理头指针变化 |
本章使用不带头节点的方式,更直观地理解指针操作。
c
Node *head = NULL; /* 空链表,头指针指向 NULL */
2.3 头指针的作用
头指针指向链表的第一个节点,是整个链表的入口。
text
head
↓
┌─────┐ ┌─────┐ ┌─────┐
│ data│ → │ data│ → │ data│ → NULL
│ next│ │ next│ │ next│
└─────┘ └─────┘ └─────┘
3 动态创建节点
3.1 创建新节点
c
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
/* 创建新节点 */
Node* create_node(int value)
{
Node *new_node = (Node*)malloc(sizeof(Node));
if (new_node == NULL) {
printf("内存分配失败\n");
return NULL;
}
new_node->data = value;
new_node->next = NULL;
return new_node;
}
3.2 头插法
在链表头部插入新节点:
c
/* 头插法:新节点成为新的头节点 */
Node* insert_head(Node *head, int value)
{
Node *new_node = create_node(value);
if (new_node == NULL) return head;
new_node->next = head; /* 新节点指向原来的头节点 */
return new_node; /* 新节点成为新的头节点 */
}
执行过程:
text
原链表:head → [10] → [20] → NULL
插入 5:new_node → [5] → head (指向[10])
结果:head → [5] → [10] → [20] → NULL
3.3 尾插法
在链表尾部插入新节点:
c
/* 尾插法:新节点添加到末尾 */
Node* insert_tail(Node *head, int value)
{
Node *new_node = create_node(value);
if (new_node == NULL) return head;
/* 空链表特殊处理 */
if (head == NULL) {
return new_node;
}
/* 找到最后一个节点 */
Node *current = head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
return head;
}
3.4 在指定位置插入
c
/* 在指定位置插入(位置从0开始计数) */
Node* insert_at(Node *head, int pos, int value)
{
if (pos < 0) return head;
Node *new_node = create_node(value);
if (new_node == NULL) return head;
/* 插入头部 */
if (pos == 0) {
new_node->next = head;
return new_node;
}
/* 找到前一个节点 */
Node *prev = head;
for (int i = 0; i < pos - 1 && prev != NULL; i++) {
prev = prev->next;
}
/* 位置超出链表长度 */
if (prev == NULL) {
free(new_node);
printf("位置无效\n");
return head;
}
new_node->next = prev->next;
prev->next = new_node;
return head;
}
4 链表的遍历
4.1 遍历并打印
c
/* 遍历链表并打印所有元素 */
void print_list(Node *head)
{
Node *current = head;
printf("链表内容:");
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
4.2 获取链表长度
c
/* 计算链表长度 */
int list_length(Node *head)
{
int count = 0;
Node *current = head;
while (current != NULL) {
count++;
current = current->next;
}
return count;
}
4.3 查找元素
c
/* 查找值为value的节点,返回位置(从0开始),找不到返回-1 */
int find_node(Node *head, int value)
{
Node *current = head;
int pos = 0;
while (current != NULL) {
if (current->data == value) {
return pos;
}
current = current->next;
pos++;
}
return -1;
}
/* 获取指定位置的节点值 */
int get_at(Node *head, int pos, int *value)
{
Node *current = head;
int i = 0;
while (current != NULL && i < pos) {
current = current->next;
i++;
}
if (current == NULL) {
return -1; /* 位置无效 */
}
*value = current->data;
return 0;
}
5 链表的释放
5.1 释放整个链表
链表节点是动态分配的,使用完后必须释放,否则会造成内存泄漏。
c
/* 释放整个链表 */
void free_list(Node *head)
{
Node *current = head;
Node *next;
while (current != NULL) {
next = current->next; /* 先保存下一个节点地址 */
free(current); /* 释放当前节点 */
current = next; /* 移动到下一个节点 */
}
}
为什么不能直接 free(current) 再 current = current->next?
因为 free(current) 后,current->next 已经不能访问了。
5.2 删除指定节点
c
/* 删除第一个值为value的节点 */
Node* delete_node(Node *head, int value)
{
if (head == NULL) return NULL;
/* 删除头节点 */
if (head->data == value) {
Node *temp = head;
head = head->next;
free(temp);
return head;
}
/* 查找要删除的节点 */
Node *prev = head;
Node *current = head->next;
while (current != NULL && current->data != value) {
prev = current;
current = current->next;
}
/* 找到则删除 */
if (current != NULL) {
prev->next = current->next;
free(current);
}
return head;
}
6 完整示例:学生成绩链表
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 学生节点结构 */
typedef struct StudentNode {
char name[50];
int id;
float score;
struct StudentNode *next;
} StudentNode;
/* 创建学生节点 */
StudentNode* create_student(const char *name, int id, float score)
{
StudentNode *new_node = (StudentNode*)malloc(sizeof(StudentNode));
if (new_node == NULL) return NULL;
strcpy(new_node->name, name);
new_node->id = id;
new_node->score = score;
new_node->next = NULL;
return new_node;
}
/* 插入到头部 */
StudentNode* insert_head(StudentNode *head, const char *name, int id, float score)
{
StudentNode *new_node = create_student(name, id, score);
if (new_node == NULL) return head;
new_node->next = head;
return new_node;
}
/* 插入到尾部 */
StudentNode* insert_tail(StudentNode *head, const char *name, int id, float score)
{
StudentNode *new_node = create_student(name, id, score);
if (new_node == NULL) return head;
if (head == NULL) {
return new_node;
}
StudentNode *current = head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
return head;
}
/* 打印所有学生 */
void print_students(StudentNode *head)
{
StudentNode *current = head;
printf("\n学生列表:\n");
printf("姓名\t\t学号\t成绩\n");
printf("------------------------\n");
while (current != NULL) {
printf("%-15s %d\t%.1f\n",
current->name, current->id, current->score);
current = current->next;
}
printf("\n");
}
/* 计算平均分 */
float average_score(StudentNode *head)
{
if (head == NULL) return 0;
StudentNode *current = head;
float sum = 0;
int count = 0;
while (current != NULL) {
sum += current->score;
count++;
current = current->next;
}
return sum / count;
}
/* 查找学生 */
StudentNode* find_student(StudentNode *head, int id)
{
StudentNode *current = head;
while (current != NULL) {
if (current->id == id) {
return current;
}
current = current->next;
}
return NULL;
}
/* 删除学生 */
StudentNode* delete_student(StudentNode *head, int id)
{
if (head == NULL) return NULL;
if (head->id == id) {
StudentNode *temp = head;
head = head->next;
free(temp);
return head;
}
StudentNode *prev = head;
StudentNode *current = head->next;
while (current != NULL && current->id != id) {
prev = current;
current = current->next;
}
if (current != NULL) {
prev->next = current->next;
free(current);
}
return head;
}
/* 释放链表 */
void free_students(StudentNode *head)
{
StudentNode *current = head;
StudentNode *next;
while (current != NULL) {
next = current->next;
free(current);
current = next;
}
}
int main(void)
{
StudentNode *head = NULL;
/* 插入学生 */
head = insert_tail(head, "张三", 1001, 88.5);
head = insert_tail(head, "李四", 1002, 92.0);
head = insert_tail(head, "王五", 1003, 78.5);
head = insert_head(head, "赵六", 1000, 95.0);
/* 打印 */
print_students(head);
/* 平均分 */
printf("平均分:%.2f\n", average_score(head));
/* 查找 */
StudentNode *found = find_student(head, 1002);
if (found) {
printf("找到学生:%s 成绩:%.1f\n", found->name, found->score);
}
/* 删除 */
head = delete_student(head, 1002);
printf("删除学号1002后:\n");
print_students(head);
/* 释放 */
free_students(head);
return 0;
}
7 常见错误与注意事项
7.1 忘记检查 malloc 返回值
c
Node *new_node = (Node*)malloc(sizeof(Node));
new_node->data = value; /* 如果 malloc 失败,new_node 为 NULL,程序崩溃 */
7.2 修改链表时丢失节点
c
/* 错误:在删除节点时,直接 current = current->next 后释放 */
Node *temp = current;
current = current->next; /* 先移动指针 */
free(temp); /* 正确,但顺序要对 */
7.3 对空链表解引用
c
Node *current = head;
while (current->next != NULL) { /* 如果 head 为 NULL,崩溃 */
/* ... */
}
7.4 忘记释放链表
c
void func(void)
{
Node *head = create_list();
/* 使用链表... */
/* 忘记 free_list(head) → 内存泄漏 */
}
7.5 指针悬挂
c
Node *p = head;
free_list(head);
p->data = 10; /* 错误!p 指向已释放的内存 */
7.6 循环引用
c
/* 错误:节点指向自己或形成环 */
last->next = first; /* 形成循环链表,遍历时死循环 */
8 本章小结
本章系统介绍了单向链表的实现:
1. 链表节点结构
c
typedef struct Node {
int data;
struct Node *next;
} Node;
2. 创建节点
-
使用
malloc动态分配 -
设置数据域和指针域(next 置为 NULL)
3. 插入操作
-
头插法:新节点指向原头节点,头指针指向新节点
-
尾插法:找到最后一个节点,将其 next 指向新节点
-
中间插入:找到前一个节点,修改指针
4. 遍历操作
-
从头指针开始,依次访问每个节点
-
用
current = current->next移动指针 -
当
current == NULL时结束
5. 释放操作
-
遍历链表,逐个
free节点 -
释放前先保存下一个节点地址
6. 注意事项
-
总是检查
malloc返回值 -
修改指针前先保存下一个节点
-
空链表特殊处理
-
使用完后必须释放