第一步:头文件和结构体定义
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义链表存储的数据类型(可改,比如char、float)
typedef int ElemType;
// 链表节点结构体(核心)
typedef struct Node {
ElemType data; // 数据域:存具体值
struct Node *next; // 指针域:指向下一个节点
} Node;

函数说明 :这是所有链表操作的基础,ElemType 统一管理数据类型,便于后续扩展;Node 结构体是链表的核心单元,包含数据域和指针域,指针域是实现链表 "链式" 的关键。
第二步
函数 1:初始化链表(创建头节点)
cpp
/**
* @brief 初始化带头节点的单链表
* @return 指向头节点的指针(链表的入口)
* @note 头节点不存储有效数据,仅用于简化插入/删除逻辑
*/
Node* initList() {
// 为头节点分配内存
Node *head = (Node*)malloc(sizeof(Node));
if (head == NULL) { // 内存分配失败校验
printf("内存分配失败!\n");
exit(1); // 终止程序
}
head->data = 0; // 头节点数据域无意义,赋值0仅作占位
head->next = NULL; // 初始状态下,头节点后无数据节点
return head;
}

函数说明 :创建链表的 "入口"(头节点),初始化后链表仅包含头节点,无有效数据;malloc 需校验返回值,避免空指针操作;exit(1) 表示异常退出。
函数 2:获取链表尾节点(为尾插法服务)
cpp
/**
* @brief 遍历链表,找到最后一个节点(尾节点)
* @param L 链表的头节点指针
* @return 指向尾节点的指针
* @note 尾节点的特征是next指针为NULL
*/
Node* get_tail(Node *L) {
Node *p = L; // 从表头开始遍历
// 只要当前节点的下一个节点不为空,就继续往后走
while (p->next != NULL) {
p = p->next;
}
return p; // 最终p指向尾节点
}
函数说明 :为 "尾插法" 服务,通过遍历找到链表的最后一个节点,避免每次插入都从头遍历;遍历起点是头节点 L,兼容空链表(仅头节点)的情况。
函数 3:尾插法插入节点
cpp
/**
* @brief 向链表尾部添加新节点(尾插法)
* @param tail 当前链表的尾节点指针
* @param e 要插入的节点值
* @return 新的尾节点指针(插入后,新节点变为尾节点)
* @note 需先通过get_tail获取初始尾节点
*/
Node* insertTail(Node *tail, ElemType e) {
// 为新节点分配内存
Node *p = (Node*)malloc(sizeof(Node));
if (p == NULL) {
printf("内存分配失败!\n");
exit(1);
}
p->data = e; // 给新节点赋值
p->next = NULL; // 新节点作为尾节点,next置空
tail->next = p; // 原尾节点指向新节点,完成挂载
return p; // 返回新的尾节点,供后续插入使用
}

- 核心逻辑:先通过
get_tail找到尾节点,再将新节点挂到尾节点后; - 关键注意:新节点
next必须置空,否则会出现野指针。
**函数说明:**在链表末尾添加节点,插入顺序与链表存储顺序一致;插入后需返回新的尾节点,否则下次插入会指向旧尾节点,导致链表断裂。
函数 4:头插法插入节点
插入是单链表的核心操作,核心口诀:先连后断,避免链表断裂。
1. 头插法(插入速度最快,顺序颠倒)
头插法直接在头节点后插入,无需遍历,时间复杂度 O (1),但插入顺序与链表存储顺序相反。
cpp
/**
* @brief 向链表头部(头节点后)添加新节点(头插法)
* @param L 链表的头节点指针
* @param e 要插入的节点值
* @note 插入速度快(无需遍历),但链表顺序与插入顺序相反
*/
// 头插法:在头节点后插入新节点
void insertHead(Node *L, ElemType e) {
Node *p = (Node*)malloc(sizeof(Node));// 为新节点分配内存
if (p == NULL) {
printf("内存分配失败!\n");
exit(1);//退出程序
}
p->data = e; // 赋值
p->next = L->next; // ① 新节点先接住原链表头
L->next = p; // ② 头节点再指向新节点
}

指针指向说明
- L :指向原链表的头节点(数据为 0 的那个节点),它是当前链表的起点。
- p :指向待插入的新节点(数据为 e 的那个节点)。
- 步骤拆解:先让新节点
p指向原首节点(p->next = L->next),再让头节点指向新节点(L->next = p),反向操作会导致链表断裂! - 输出结果:连续头插 10、20,链表顺序为「20 → 10」(插入顺序与存储顺序相反)
函数说明:核心逻辑是 "先连后断":先让新节点接住原链表的头部,再让头节点指向新节点;无需遍历,时间复杂度 O (1),适合追求插入效率的场景。

函数 5:指定位置插入节点
实际开发中更常用「指定位置增删」,核心是找到「前驱节点」 (要操作位置的前一个节点)。
cpp
/**
* @brief 向链表指定位置插入节点(pos从1开始计数)
* @param L 链表的头节点指针
* @param pos 插入位置(第pos个节点前)
* @param e 要插入的节点值
* @return 1-插入成功,0-插入失败(位置越界)
*/
// 指定位置插入(pos从1开始计数)
int insertNode(Node *L, int pos, ElemType e) {
Node *p = L; // 从表头开始找插入位置的前驱节点
int i = 0; // 记录当前节点的位置(头节点为0)
// 找到第pos-1个节点(插入位置的前驱节点)
while (i < pos - 1 && p != NULL) {
p = p->next;
i++;
}
if (p == NULL) { // 位置越界(如pos大于链表长度+1)
printf("插入位置错误!\n");
return 0;
}
// 创建新节点
Node *q = (Node*)malloc(sizeof(Node));
if (q == NULL) {
printf("内存分配失败!\n");
exit(1);
}
q->data = e;
q->next = p->next; // 新节点指向前驱节点的下一个节点(先连后继)
p->next = q; // 前驱节点指向新节点,完成插入(再连前驱)
return 1;
}

- 核心:插入位置
pos对应前驱节点是pos-1,比如插在第 2 位,需找到第 1 个节点作为前驱; - 容错:判断
p==NULL避免越界插入。
函数说明 :插入位置 pos 从 1 开始(符合日常计数习惯),核心是找到 "前驱节点"(要插入位置的前一个节点);若 p 为空,说明位置无效(如链表长度为 3,插入 pos=5);同样遵循 "先连后断" 原则,避免链表断裂。
函数 6:指定位置删除节点
删除是插入的逆操作,核心是「跳过待删节点 + 释放内存」:
cpp
* @brief 删除链表中指定位置的节点(pos从1开始计数)
* @param L 链表的头节点指针
* @param pos 要删除的节点位置
* @return 1-删除成功,0-删除失败(位置越界)
* @note 删除后需释放节点内存,避免内存泄漏
*/
// 指定位置删除节点(pos从1开始)
int deleteNode(Node *L, int pos) {
Node *p = L; // 找待删节点的前驱节点
int i = 0;
//找到第pos-1个节点(待删节点的前驱节点)
while (i < pos - 1 && p != NULL) {
p = p->next;
i++;
}
// 位置越界 或 前驱节点后无节点可删
if (p == NULL || p->next == NULL) {
printf("删除位置错误!\n");
return 0;
}
Node *q = p->next; // q指向待删除节点
p->next = q->next; // 前驱节点跳过待删节点,指向其后继节点
free(q); // 释放待删节点的内存
return 1;
}

函数说明 :删除核心是 "跳过待删节点 + 释放内存";必须先保存待删节点的地址(q),再修改指针,否则会找不到待删节点导致内存泄漏; free(q) 仅释放节点内存,不会自动置空指针,需手动处理逻辑。
函数 7:遍历并打印链表
cpp
/**
* @brief 遍历链表,打印所有有效节点的值
* @param L 链表的头节点指针
* @note 从第一个有效节点(L->next)开始遍历
*/
void listNode(Node *L) {
Node *p = L->next; // 跳过头节点,指向第一个有效节点
while (p != NULL) { // 遍历到尾节点为止
printf("%d ", p->data); // 打印当前节点的值
p = p->next; // 指向下一个节点
}
printf("\n"); // 换行,优化输出格式
}
函数说明 :遍历起点是 L->next(跳过无意义的头节点);循环条件 p != NULL 确保遍历到最后一个节点后停止;是验证链表操作结果的核心函数。
函数 8:计算链表长度
cpp
/**
* @brief 计算链表中有效节点的个数(不含头节点)
* @param L 链表的头节点指针
* @return 链表的有效长度
*/
int listLength(Node *L) {
Node *p = L->next; // 从第一个有效节点开始
int len = 0; // 长度计数器
while (p != NULL) {
p = p->next;
len++; // 每遍历一个节点,长度+1
}
return len;
}
函数说明:仅统计有效数据节点的数量,排除头节点;空链表(仅头节点)返回 0,符合直观认知;遍历逻辑与打印函数一致,仅增加了计数功能。
函数 9:释放链表内存
cpp
/**
* @brief 释放链表中所有有效节点的内存(保留头节点)
* @param L 链表的头节点指针
* @note 释放后需将头节点的next置空,避免野指针
*/
void freeList(Node *L) {
Node *p = L->next; // 从第一个有效节点开始
Node *q; // 临时保存下一个节点的地址
while (p != NULL) {
q = p->next; // 先保存下一个节点的地址,防止断链
free(p); // 释放当前节点
p = q; // 指向原下一个节点,继续释放
}
L->next = NULL; // 头节点next置空,标记链表为空
}
函数说明 :释放逻辑是 "先存后放":必须先保存下一个节点的地址(q),再释放当前节点,否则释放后无法找到后续节点;释放后仅保留头节点,若需完全销毁链表,需在外部 free(L)。
第三步:主函数(测试所有功能)
cpp
// 主函数:测试所有链表操作
int main() {
// 1. 初始化链表
Node *list = initList();
Node *tail = get_tail(list); // 初始尾节点是头节点
// 2. 尾插测试:插入10、20、30
tail = insertTail(tail, 10);
tail = insertTail(tail, 20);
tail = insertTail(tail, 30);
printf("初始链表(尾插10/20/30):");
listNode(list); // 输出:10 20 30
// 3. 指定位置插入:在第2位插15
insertNode(list, 2, 15);
printf("插入15后:");
listNode(list); // 输出:10 15 20 30
// 4. 删除测试:删除第2位节点
deleteNode(list, 2);
printf("删除第2个节点后:");
listNode(list); // 输出:10 20 30
// 5. 头插测试:插入5
insertHead(list, 5);
printf("头插5后:");
listNode(list); // 输出:5 10 20 30
// 6. 打印长度
printf("链表长度:%d\n", listLength(list)); // 输出:4
// 7. 释放内存
freeList(list);
printf("释放后链表长度:%d\n", listLength(list)); // 输出:0
// 释放头节点
free(list);
return 0;
}
最终操作:运行代码
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义链表存储的数据类型(可根据需求修改,如char、float等)
typedef int ElemType;
// 定义链表节点结构体
typedef struct Node {
ElemType data; // 数据域:存储节点的值
struct Node *next; // 指针域:指向下一个节点的地址
} Node;
/**
* @brief 初始化带头节点的单链表
* @return 指向头节点的指针(链表的入口)
*/
Node* initList() {
Node *head = (Node*)malloc(sizeof(Node));
if (head == NULL) { // 内存分配失败校验
printf("内存分配失败!\n");
exit(1); // 终止程序
}
head->data = 0; // 头节点数据域无意义,仅占位
head->next = NULL; // 初始状态下无数据节点
return head;
}
/**
* @brief 遍历链表,找到最后一个节点(尾节点)
* @param L 链表的头节点指针
* @return 指向尾节点的指针
*/
Node* get_tail(Node *L) {
Node *p = L; // 从表头开始遍历
while (p->next != NULL) {
p = p->next;
}
return p; // 最终p指向尾节点
}
/**
* @brief 向链表尾部添加新节点(尾插法)
* @param tail 当前链表的尾节点指针
* @param e 要插入的节点值
* @return 新的尾节点指针
*/
Node* insertTail(Node *tail, ElemType e) {
Node *p = (Node*)malloc(sizeof(Node));
if (p == NULL) {
printf("内存分配失败!\n");
exit(1);
}
p->data = e;
p->next = NULL; // 新节点作为尾节点,next置空
tail->next = p; // 原尾节点指向新节点
return p; // 返回新的尾节点
}
/**
* @brief 向链表头部(头节点后)添加新节点(头插法)
* @param L 链表的头节点指针
* @param e 要插入的节点值
*/
void insertHead(Node *L, ElemType e) {
Node *p = (Node*)malloc(sizeof(Node));
if (p == NULL) {
printf("内存分配失败!\n");
exit(1);
}
p->data = e;
p->next = L->next; // 新节点指向原首节点
L->next = p; // 头节点指向新节点
}
/**
* @brief 向链表指定位置插入节点(pos从1开始计数)
* @param L 链表的头节点指针
* @param pos 插入位置
* @param e 要插入的节点值
* @return 1-插入成功,0-插入失败
*/
int insertNode(Node *L, int pos, ElemType e) {
Node *p = L;
int i = 0;
// 找到第pos-1个节点(插入位置的前驱节点)
while (i < pos - 1 && p != NULL) {
p = p->next;
i++;
}
if (p == NULL) { // 位置越界
printf("插入位置错误!\n");
return 0;
}
Node *q = (Node*)malloc(sizeof(Node));
if (q == NULL) {
printf("内存分配失败!\n");
exit(1);
}
q->data = e;
q->next = p->next; // 新节点指向前驱节点的下一个节点
p->next = q; // 前驱节点指向新节点
return 1;
}
/**
* @brief 删除链表中指定位置的节点(pos从1开始计数)
* @param L 链表的头节点指针
* @param pos 要删除的节点位置
* @return 1-删除成功,0-删除失败
*/
int deleteNode(Node *L, int pos) {
Node *p = L;
int i = 0;
// 找到待删节点的前驱节点
while (i < pos - 1 && p != NULL) {
p = p->next;
i++;
}
// 位置越界 或 前驱节点后无节点可删
if (p == NULL || p->next == NULL) {
printf("删除位置错误!\n");
return 0;
}
Node *q = p->next; // q指向待删除节点
p->next = q->next; // 前驱节点跳过待删节点
free(q); // 释放待删节点内存
return 1;
}
/**
* @brief 遍历链表,打印所有有效节点的值
* @param L 链表的头节点指针
*/
void listNode(Node *L) {
Node *p = L->next; // 跳过头节点,指向第一个有效节点
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
/**
* @brief 计算链表中有效节点的个数(不含头节点)
* @param L 链表的头节点指针
* @return 链表的有效长度
*/
int listLength(Node *L) {
Node *p = L->next;
int len = 0;
while (p != NULL) {
len++;
p = p->next;
}
return len;
}
/**
* @brief 释放链表中所有有效节点的内存(保留头节点)
* @param L 链表的头节点指针
*/
void freeList(Node *L) {
Node *p = L->next;
Node *q;
while (p != NULL) {
q = p->next; // 先保存下一个节点地址,防止断链
free(p); // 释放当前节点
p = q; // 处理下一个节点
}
L->next = NULL; // 头节点next置空,避免野指针
}
// 主函数:测试所有链表操作
int main() {
// 1. 初始化链表
Node *list = initList();
Node *tail = get_tail(list); // 初始尾节点为头节点
// 2. 尾插法测试:插入10、20、30
tail = insertTail(tail, 10);
tail = insertTail(tail, 20);
tail = insertTail(tail, 30);
printf("初始链表(尾插10/20/30):");
listNode(list);
// 3. 指定位置插入测试:在第2位插入15
insertNode(list, 2, 15);
printf("在第2位插入15后:");
listNode(list);
// 4. 删除节点测试:删除第2位节点
deleteNode(list, 2);
printf("删除第2个节点后:");
listNode(list);
// 5. 头插法测试:插入5
insertHead(list, 5);
printf("头插5后:");
listNode(list);
// 6. 打印链表长度
printf("当前链表长度:%d\n", listLength(list));
// 7. 释放链表内存
freeList(list);
printf("释放内存后链表长度:%d\n", listLength(list));
// 释放头节点内存
free(list);
return 0;
}
运行结果
核心总结
- 单链表核心:指针操作遵循「先连后断」,避免链表断裂;
- 头插快但顺序反,尾插顺序正但需找尾,指定位置增删找「前驱节点」;
- 动态内存必释放:
malloc对应free,否则内存泄漏。