链表(一):链表的概念、单向链表的节点定义、链表的创建与遍历
数组用久了,你可能会遇到一个烦心事——它的大小是固定的。我刚开始学C语言那会儿,总觉得数组够用了,直到有一次做项目,需要动态存储用户输入的数据,数量不确定,数组要么开小了不够用,要么开大了浪费内存。这时候,链表就派上用场了。
什么是链表?
链表,说白了就是一种动态的数据结构。它不像数组那样在内存里连续存放,而是通过指针把一个个节点串起来。每个节点里存着数据,还存着下一个节点的地址。
你想想看,数组就像一列固定座位的火车,座位号是连续的。链表呢,就像一群人手拉手站成一排,每个人只知道自己后面是谁。想加人?随时拉一个进来,把手牵上就行。
核心特点:
- 动态分配内存,不需要预先知道数据总量
- 插入和删除操作效率高,不需要移动其他元素
- 访问元素必须从头开始遍历,不支持随机访问
单向链表的节点定义
单向链表是最简单的一种链表。每个节点包含两部分:数据域和指针域。数据域存你要存的东西,指针域指向下一个节点。
我习惯用结构体来定义节点,这样代码清晰又好维护。来看个例子:
// 定义一个学生信息节点
struct Student {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
struct Student *next; // 指向下一个节点的指针
};
这里有个关键点——next指针的类型是struct Student *,它指向的是同类型的结构体。这就是链表的精髓:自己指向自己这种类型。
小技巧:我建议给结构体起个别名,用typedef,这样写代码时能少敲几个字:
typedef struct Student {
int id;
char name[20];
float score;
struct Student *next;
} StudentNode;
链表的创建
创建链表,说白了就是一个个地创建节点,然后把它们串起来。我常用的方法是头插法和尾插法。先讲尾插法,因为它更符合直觉——新节点加在末尾。
来看这段代码:
// 创建链表,返回头节点指针
StudentNode* createList(int n) {
StudentNode *head = NULL;
StudentNode *tail = NULL;
for (int i = 0; i < n; i++) {
// 动态分配一个新节点
StudentNode *newNode = (StudentNode*)malloc(sizeof(StudentNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
return NULL;
}
// 给节点赋值
printf("请输入第%d个学生的学号、姓名、成绩:", i + 1);
scanf("%d %s %f", &newNode->id, newNode->name, &newNode->score);
newNode->next = NULL; // 新节点的next先指向NULL
// 如果是第一个节点
if (head == NULL) {
head = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
}
return head;
}
这段代码里,我用了malloc动态分配内存。嗯,这里要注意——每次malloc之后一定要检查返回值,我曾经因为忘了检查,程序跑着跑着就崩了,查了半天才发现是内存分配失败。
避坑指南:我曾经在项目里犯过一个低级错误——创建节点后忘了把next指针初始化为NULL。结果遍历链表时,最后一个节点的next是个野指针,程序直接段错误。记住:每次创建新节点,一定要把next置为NULL。
链表的遍历
遍历链表,就是从头节点开始,顺着next指针一个个往下走,直到遇到NULL。说白了就是"跟着指针走"。
// 遍历链表,打印所有节点信息
void traverseList(StudentNode *head) {
StudentNode *current = head;
int count = 0;
printf("\n===== 学生信息列表 =====\n");
while (current != NULL) {
count++;
printf("第%d个学生:学号=%d, 姓名=%s, 成绩=%.1f\n",
count, current->id, current->name, current->score);
current = current->next; // 移动到下一个节点
}
printf("共%d个学生\n", count);
}
这里有个细节——我用了current指针来遍历,而不是直接用head。为什么?因为如果直接移动head,遍历完链表后,头节点就丢了,再也找不回来了。我刚开始学的时候犯过这个错,后来就养成了用临时指针的习惯。
完整示例:创建并遍历一个学生链表
把上面的代码拼起来,就是一个完整的程序:
#include <stdio.h>
#include <stdlib.h>
typedef struct Student {
int id;
char name[20];
float score;
struct Student *next;
} StudentNode;
// 创建链表
StudentNode* createList(int n) {
StudentNode *head = NULL, *tail = NULL;
for (int i = 0; i < n; i++) {
StudentNode *newNode = (StudentNode*)malloc(sizeof(StudentNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
return NULL;
}
printf("请输入第%d个学生的学号、姓名、成绩:", i + 1);
scanf("%d %s %f", &newNode->id, newNode->name, &newNode->score);
newNode->next = NULL;
if (head == NULL) {
head = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
}
return head;
}
// 遍历链表
void traverseList(StudentNode *head) {
StudentNode *current = head;
int count = 0;
printf("\n===== 学生信息列表 =====\n");
while (current != NULL) {
count++;
printf("第%d个学生:学号=%d, 姓名=%s, 成绩=%.1f\n",
count, current->id, current->name, current->score);
current = current->next;
}
printf("共%d个学生\n", count);
}
int main() {
int n;
printf("请输入学生人数:");
scanf("%d", &n);
StudentNode *head = createList(n);
traverseList(head);
return 0;
}
链表结构示意图
光说可能不太好理解,我画张图给你看看链表长什么样:
链表 vs 数组:一张表看懂区别
| 对比项 | 数组 | 链表 |
|---|---|---|
| 内存分配 | 静态分配,大小固定 | 动态分配,大小可变 |
| 内存位置 | 连续内存 | 不连续,通过指针连接 |
| 访问方式 | 随机访问,通过下标O(1) | 顺序访问,必须遍历O(n) |
| 插入/删除 | 需要移动元素,效率低 | 只需修改指针,效率高 |
| 额外开销 | 无 | 每个节点需要额外存储指针 |
我的建议:如果你知道数据量大概有多少,而且经常要随机访问,用数组。如果数据量不确定,或者经常要插入删除,用链表。我在做嵌入式项目时,经常用链表来管理动态变化的传感器数据列表,灵活得很。
链表这东西,刚开始接触可能觉得有点绕,尤其是那个指针指来指去的。但只要你动手写几遍,画几张图,慢慢就理解了。记住一句话:链表就是"节点 + 指针"的组合,节点存数据,指针指方向。
公众号:蓝海资料掘金营,微信deep3321