一、什么是链表?
链表(Linked List)是一种非连续、非顺序 的线性数据结构,它通过指针 将一组零散的内存块串联起来,每个内存块被称为节点(Node)。
简单来说,链表就像一串 "糖葫芦":
- 每个 "山楂" 是一个节点,包含数据和下一个节点的地址;
- 通过 "竹签"(指针)将所有节点串联起来,形成一个完整的链条。
二、链表的核心结构:节点
每个链表节点由两部分组成:
- 数据域:存储节点的具体数据;
- 指针域:存储下一个节点的地址。
对应的 C 语言代码定义如下:
// 链表节点结构体
typedef struct Node {
int data; // 数据域:存储节点数据
struct Node* next; // 指针域:指向next节点
} Node;
三、链表的基本操作
1. 头插法:在链表头部插入节点
头插法是将新节点插入到链表的最前端,步骤如下:
- 让新节点的
next指向当前的头节点; - 更新头节点为新节点。
代码实现:
// 头插法:在链表头部插入新节点
void headInsert(Node** head, int value) {
// 1. 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
// 2. 新节点的next指向当前头节点
newNode->next = *head;
// 3. 更新头节点为新节点
*head = newNode;
}
2. 尾插法:在链表尾部插入节点
尾插法是将新节点插入到链表的最后端,步骤如下:
- 遍历链表找到最后一个节点;
- 让最后一个节点的
next指向新节点; - 新节点的
next指向NULL。
代码实现:
// 尾插法:在链表尾部插入新节点
void tailInsert(Node** head, int value) {
// 1. 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = NULL;
// 2. 如果链表为空,直接让头节点指向新节点
if (*head == NULL) {
*head = newNode;
return;
}
// 3. 遍历找到最后一个节点
Node* p = *head;
while (p->next != NULL) {
p = p->next;
}
// 4. 最后一个节点的next指向新节点
p->next = newNode;
}
3. 遍历链表
遍历链表是从头部开始,依次访问每个节点,直到遇到NULL结束,时间复杂度为O(n)。
代码实现:
// 遍历链表并打印所有节点
void traverseList(Node* head) {
Node* p = head;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
4. 指定位置插入节点
在指定位置插入节点,需要先找到该位置的前驱节点,步骤如下:
- 遍历找到前驱节点
p; - 让新节点的
next指向p的next; - 让
p的next指向新节点。
代码实现:
// 指定位置插入节点:在第pos个位置插入新节点(pos从0开始)
void insertAt(Node** head, int pos, int value) {
// 1. 处理头插情况(pos=0)
if (pos == 0) {
headInsert(head, value);
return;
}
// 2. 找到前驱节点
Node* p = *head;
for (int i = 0; i < pos - 1 && p != NULL; i++) {
p = p->next;
}
// 3. 如果前驱节点不存在,直接返回
if (p == NULL) {
printf("插入位置无效!\n");
return;
}
// 4. 创建新节点并插入
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = p->next;
p->next = newNode;
}
5. 指定位置删除节点
删除指定位置的节点,需要先找到该位置的前驱节点,步骤如下:
- 遍历找到前驱节点
p; - 保存要删除的节点
tmp = p->next; - 让
p的next指向tmp的next; - 释放
tmp的内存。
代码实现:
// 指定位置删除节点:删除第pos个位置的节点(pos从0开始)
void deleteAt(Node** head, int pos) {
// 1. 处理头删情况(pos=0)
if (pos == 0 && *head != NULL) {
Node* tmp = *head;
*head = (*head)->next;
free(tmp);
return;
}
// 2. 找到前驱节点
Node* p = *head;
for (int i = 0; i < pos - 1 && p != NULL && p->next != NULL; i++) {
p = p->next;
}
// 3. 如果前驱节点或要删除的节点不存在,直接返回
if (p == NULL || p->next == NULL) {
printf("删除位置无效!\n");
return;
}
// 4. 删除节点并释放内存
Node* tmp = p->next;
p->next = tmp->next;
free(tmp);
}
6. 头删与尾删
头删:直接删除头节点,更新头节点为下一个节点;
尾删:遍历找到倒数第二个节点,让其next指向NULL,释放最后一个节点。
代码实现:
// 头删:删除链表的第一个节点
void headDelete(Node** head) {
if (*head == NULL) {
printf("链表为空,无法头删!\n");
return;
}
Node* tmp = *head;
*head = (*head)->next;
free(tmp);
}
// 尾删:删除链表的最后一个节点
void tailDelete(Node** head) {
if (*head == NULL) {
printf("链表为空,无法尾删!\n");
return;
}
// 1. 如果只有一个节点,直接删除
if ((*head)->next == NULL) {
free(*head);
*head = NULL;
return;
}
// 2. 找到倒数第二个节点
Node* p = *head;
while (p->next->next != NULL) {
p = p->next;
}
// 3. 删除最后一个节点
Node* tmp = p->next;
p->next = NULL;
free(tmp);
}
四、链表 vs 数组
链表和数组是两种最常用的线性数据结构,它们的区别如下:

五、链表的适用场景
根据链表和数组的特性,它们的适用场景如下:
-
链表适用场景:
- 数据量不确定,需要动态扩容;
- 频繁进行插入、删除操作;
- 不需要频繁随机访问。
-
数组适用场景:
- 数据量固定,需要频繁随机访问;
- 内存连续,需要高访问效率。
六、完整代码示例
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构体
typedef struct Node {
int data;
struct Node* next;
} Node;
// 头插法
void headInsert(Node** head, int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = *head;
*head = newNode;
}
// 尾插法
void tailInsert(Node** head, int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = NULL;
if (*head == NULL) {
*head = newNode;
return;
}
Node* p = *head;
while (p->next != NULL) {
p = p->next;
}
p->next = newNode;
}
// 遍历链表
void traverseList(Node* head) {
Node* p = head;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 指定位置插入
void insertAt(Node** head, int pos, int value) {
if (pos == 0) {
headInsert(head, value);
return;
}
Node* p = *head;
for (int i = 0; i < pos - 1 && p != NULL; i++) {
p = p->next;
}
if (p == NULL) {
printf("插入位置无效!\n");
return;
}
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = p->next;
p->next = newNode;
}
// 指定位置删除
void deleteAt(Node** head, int pos) {
if (pos == 0 && *head != NULL) {
Node* tmp = *head;
*head = (*head)->next;
free(tmp);
return;
}
Node* p = *head;
for (int i = 0; i < pos - 1 && p != NULL && p->next != NULL; i++) {
p = p->next;
}
if (p == NULL || p->next == NULL) {
printf("删除位置无效!\n");
return;
}
Node* tmp = p->next;
p->next = tmp->next;
free(tmp);
}
// 头删
void headDelete(Node** head) {
if (*head == NULL) {
printf("链表为空,无法头删!\n");
return;
}
Node* tmp = *head;
*head = (*head)->next;
free(tmp);
}
// 尾删
void tailDelete(Node** head) {
if (*head == NULL) {
printf("链表为空,无法尾删!\n");
return;
}
if ((*head)->next == NULL) {
free(*head);
*head = NULL;
return;
}
Node* p = *head;
while (p->next->next != NULL) {
p = p->next;
}
Node* tmp = p->next;
p->next = NULL;
free(tmp);
}
// 释放链表内存
void freeList(Node** head) {
Node* p = *head;
while (p != NULL) {
Node* tmp = p;
p = p->next;
free(tmp);
}
*head = NULL;
}
int main() {
Node* head = NULL;
// 测试头插法
headInsert(&head, 3);
headInsert(&head, 5);
headInsert(&head, 7);
printf("头插后链表:");
traverseList(head); // 输出:7 5 3
// 测试尾插法
tailInsert(&head, 9);
printf("尾插后链表:");
traverseList(head); // 输出:7 5 3 9
// 测试指定位置插入
insertAt(&head, 2, 6);
printf("在位置2插入6后:");
traverseList(head); // 输出:7 5 6 3 9
// 测试指定位置删除
deleteAt(&head, 2);
printf("删除位置2的节点后:");
traverseList(head); // 输出:7 5 3 9
// 测试头删
headDelete(&head);
printf("头删后链表:");
traverseList(head); // 输出:5 3 9
// 测试尾删
tailDelete(&head);
printf("尾删后链表:");
traverseList(head); // 输出:5 3
// 释放链表内存
freeList(&head);
printf("释放内存后,链表为空:%s\n", head == NULL ? "是" : "否"); // 输出:是
return 0;
}
七、总结
链表是一种动态、灵活的线性数据结构,它通过指针将节点串联起来,支持高效的插入和删除操作,但随机访问效率较低。
在实际开发中,链表常用于:
- 动态数组(如
ArrayList的底层实现); - 队列、栈等数据结构的实现;
- 操作系统的内存管理;
- 图和树的遍历等。
希望这篇文章能帮助你深入理解链表的原理和实现!