一、环境与基础定义
1. 编译环境
- 编译器:gcc
- 运行方式:终端编译
gcc main.c linklist.c -o linklist,执行./linklist
2. 头文件定义(linklist.h)
将链表的结构体定义、数据类型别名、函数声明统一放在头文件中,实现工程化的代码分离,便于维护。
#ifndef __LINKLIST_H__
#define __LINKLIST_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
// 自定义数据类型,后续可直接修改为char/struct等,适配不同场景
typedef int DataType;
// 链表节点结构体定义
typedef struct Node
{
DataType Data; // 节点存储的有效数据
struct Node *pNext; // 指向下一个节点的指针
} Node_t;
// 函数声明:带头节点单链表全套基础操作
// 1. 创建空的带头节点单链表
Node_t *CreateEmptyLinkList(void);
// 2. 头插法插入节点(从链表头部插入,逆序插入)
int InsertHeadNode(Node_t *pHead, DataType TmpData);
// 3. 尾插法插入节点(从链表尾部插入,顺序插入)
int InsertTailLinkNode(Node_t *pHead, DataType TmpData);
// 4. 遍历打印链表所有有效节点
int ShowLinkList(Node_t *pHead);
// 5. 查找指定数据的节点,返回节点指针(未找到返回NULL)
Node_t *FindLinkNode(Node_t *pHead, DataType TmpData);
// 6. 替换链表中所有指定旧数据为新数据
int ReplaceLinkNode(Node_t *pHead, DataType OldData, DataType NewData);
// 7. 删除链表中所有指定数据的节点,返回删除个数
int DeleteLinkNode(Node_t *pHead, DataType TmpData);
#endif // __LINKLIST_H__
关键说明 :使用头文件保护宏#ifndef __LINKLIST_H__避免头文件被重复包含,这是 C 语言工程开发的基本规范。
二、链表核心操作实现(linklist.c)
所有函数的实现均基于头文件的声明,逐函数讲解功能、实现思路和核心逻辑,代码中添加详细注释,新手可直接复制使用。
1. 创建空的带头节点单链表
#include "linklist.h"
// 创建空的带头节点单链表:仅分配头节点,pNext置NULL
Node_t *CreateEmptyLinkList(void)
{
Node_t *pNewNode = NULL;
// 为头节点分配内存
pNewNode = malloc(sizeof(Node_t));
if (NULL == pNewNode)
{
perror("malloc for head node fail"); // 打印内存分配失败原因
return NULL;
}
pNewNode->pNext = NULL; // 空链表,头节点后继指针置空
return pNewNode; // 返回头节点指针,作为链表入口
}
核心 :空链表的标志是头节点的 pNext 为 NULL,头节点本身不存储有效数据。
2. 头插法插入节点
功能 :从链表头部插入新节点,新节点成为第一个有效节点,插入顺序与最终链表顺序相反 (如依次插入 1、2、3,链表为 3→2→1)。思路 :新节点的pNext指向原第一个有效节点,头节点的pNext指向新节点(两步指向,不可颠倒)。
// 头插法插入节点:pHead为链表头节点,TmpData为插入数据
int InsertHeadNode(Node_t *pHead, DataType TmpData)
{
if (NULL == pHead) // 防崩溃:判断头节点是否有效
{
printf("error: pHead is NULL\n");
return -1;
}
Node_t *pNewNode = NULL;
// 为新节点分配内存
pNewNode = malloc(sizeof(Node_t));
if (NULL == pNewNode)
{
perror("malloc for new node fail");
return -1;
}
// 初始化新节点:赋值+指向原第一个有效节点
pNewNode->Data = TmpData;
pNewNode->pNext = pHead->pNext;
// 头节点指向新节点,完成插入
pHead->pNext = pNewNode;
return 0; // 插入成功返回0
}
关键 :头插法的两步指向顺序不可颠倒,若先修改头节点的pNext,会丢失原第一个有效节点的地址,导致链表断链。
3. 尾插法插入节点
功能 :从链表尾部插入新节点,新节点成为最后一个有效节点,插入顺序与最终链表顺序一致(如依次插入 1、2、3,链表为 1→2→3)。
思路 :遍历找到链表尾节点(pNext为 NULL 的节点),尾节点的pNext指向新节点,新节点的pNext置 NULL。
// 尾插法插入节点:pHead为链表头节点,TmpData为插入数据
int InsertTailLinkNode(Node_t *pHead, DataType TmpData)
{
if (NULL == pHead) // 防崩溃:判断头节点是否有效
{
printf("error: pHead is NULL\n");
return -1;
}
Node_t *pNewNode = NULL;
// 为新节点分配内存
pNewNode = malloc(sizeof(Node_t));
if (NULL == pNewNode)
{
perror("malloc for new node fail");
return -1;
}
// 初始化新节点:赋值+后继置空(必做,避免野指针)
pNewNode->Data = TmpData;
pNewNode->pNext = NULL;
// 查找尾节点:从第一个有效节点开始遍历
Node_t *pTailNode = pHead; // 优化:从pHead开始,兼容空链表
while (pTailNode->pNext != NULL)
{
pTailNode = pTailNode->pNext;
}
// 尾节点指向新节点,完成插入
pTailNode->pNext = pNewNode;
return 0; // 插入成功返回0
}
4. 遍历打印链表
功能 :遍历链表所有有效节点,打印节点数据,跳过头节点 。思路 :从第一个有效节点(pHead->pNext)开始,遍历至pNext为 NULL,循环中打印当前节点数据。
// 遍历打印链表:pHead为链表头节点
int ShowLinkList(Node_t *pHead)
{
if (NULL == pHead) // 防崩溃:判断头节点是否有效
{
printf("error: pHead is NULL\n");
return -1;
}
Node_t *pTmpNode = pHead->pNext; // 指向第一个有效节点
if (NULL == pTmpNode) // 判断是否为空链表(无有效节点)
{
printf("link list is empty\n");
return 0;
}
// 遍历所有有效节点并打印
printf("link list: ");
while (pTmpNode != NULL)
{
printf("%d ", pTmpNode->Data);
pTmpNode = pTmpNode->pNext;
}
printf("\n");
return 0;
}
优化:增加空链表判断,避免空链表时无打印输出,提升代码友好性。
5. 查找指定数据的节点
功能 :查找链表中第一个匹配指定数据的节点,返回节点指针 (便于后续对节点的操作),未找到返回 NULL,同时打印匹配数据。思路:遍历所有有效节点,找到数据匹配的节点立即返回(无需继续遍历),遍历结束未找到则返回 NULL。
// 查找指定数据的节点:返回节点指针,未找到返回NULL
Node_t *FindLinkNode(Node_t *pHead, DataType TmpData)
{
if (NULL == pHead) // 防崩溃:判断头节点是否有效
{
printf("error: pHead is NULL\n");
return NULL;
}
Node_t *pFindNode = pHead->pNext; // 指向第一个有效节点
// 遍历所有有效节点
while (pFindNode != NULL) // 原代码为pFindNode->pNext!=NULL,会漏掉最后一个节点,已修正
{
if (pFindNode->Data == TmpData)
{
printf("find node: %d\n", pFindNode->Data);
return pFindNode; // 找到后立即返回节点指针,提升效率
}
pFindNode = pFindNode->pNext;
}
// 遍历结束未找到
printf("not find node: %d\n", TmpData);
return NULL;
}
6. 替换指定节点数据
功能 :替换链表中所有 匹配旧数据的节点为新数据(若有多个重复数据,全部替换)。思路 :遍历所有有效节点,找到数据匹配的节点,直接修改节点的Data值(链表节点为结构体,直接解引用修改即可)。
// 替换所有旧数据为新数据:OldData为旧值,NewData为新值
int ReplaceLinkNode(Node_t *pHead, DataType OldData, DataType NewData)
{
if (NULL == pHead) // 防崩溃:判断头节点是否有效
{
printf("error: pHead is NULL\n");
return -1;
}
Node_t *pReplaceNode = pHead->pNext; // 指向第一个有效节点
int replaceCnt = 0; // 记录替换个数,便于反馈结果
// 遍历所有有效节点
while (pReplaceNode != NULL)
{
if (pReplaceNode->Data == OldData)
{
pReplaceNode->Data = NewData; // 直接修改节点数据
replaceCnt++;
}
pReplaceNode = pReplaceNode->pNext;
}
// 替换结果反馈
if (replaceCnt > 0)
{
printf("replace success: %d nodes ( %d → %d )\n", replaceCnt, OldData, NewData);
}
else
{
printf("replace fail: not find data %d\n", OldData);
}
return replaceCnt; // 返回替换个数,便于调用者处理
}
优化:增加替换个数统计,返回替换结果,让调用者可获取替换状态(比固定返回 0 更实用)。
7. 删除指定数据的节点
功能 :删除链表中所有 匹配指定数据的节点,返回删除个数,删除后释放节点内存 (避免内存泄漏)。思路 :使用双指针遍历 (前驱节点pPreNode+ 当前节点pTmpNode),找到匹配节点时,前驱节点的pNext指向当前节点的后继节点,释放当前节点内存,再将当前节点指向前驱节点的后继节点。
c
运行
// 删除所有指定数据的节点:返回删除个数
int DeleteLinkNode(Node_t *pHead, DataType TmpData)
{
if (NULL == pHead) // 防崩溃:判断头节点是否有效
{
printf("error: pHead is NULL\n");
return -1;
}
int delCnt = 0; // 记录删除个数
Node_t *pPreNode = pHead; // 前驱节点,初始指向头节点
Node_t *pTmpNode = pHead->pNext; // 当前节点,初始指向第一个有效节点
// 双指针遍历所有有效节点
while (pTmpNode != NULL)
{
if (pTmpNode->Data == TmpData)
{
// 前驱节点指向当前节点的后继节点,断链
pPreNode->pNext = pTmpNode->pNext;
free(pTmpNode); // 释放当前节点内存,避免内存泄漏
pTmpNode = pPreNode->pNext; // 当前节点指向新的后继节点
delCnt++;
}
else
{
// 未找到,双指针同时后移
pPreNode = pPreNode->pNext;
pTmpNode = pTmpNode->pNext;
}
}
// 删除结果反馈
if (delCnt > 0)
{
printf("delete success: %d nodes ( %d )\n", delCnt, TmpData);
}
else
{
printf("delete fail: not find data %d\n", TmpData);
}
return delCnt; // 返回删除个数
}
核心:
- 双指针遍历是链表删除的标准写法,避免丢失节点地址导致断链;
- 删除节点后必须释放内存 (
free(pTmpNode)),否则会造成内存泄漏; - 释放内存后,需将当前节点指向新的后继节点,不可直接后移双指针。
三、全套功能测试(main.c)
编写测试代码,覆盖创建链表、头插、尾插、遍历、查找、替换、删除所有操作,新手可直接复制,编译运行即可看到效果。
#include "linklist.h"
int main(void)
{
Node_t *pHead = NULL;
// 1. 创建空的带头节点单链表
pHead = CreateEmptyLinkList();
if (NULL == pHead)
{
printf("create link list fail\n");
return -1;
}
printf("1. create empty link list success\n");
ShowLinkList(pHead); // 打印空链表
// 2. 头插法插入节点:1、2、3
InsertHeadNode(pHead, 1);
InsertHeadNode(pHead, 2);
InsertHeadNode(pHead, 3);
printf("\n2. insert head node (1,2,3)\n");
ShowLinkList(pHead); // 3→2→1
// 3. 尾插法插入节点:4、5
InsertTailLinkNode(pHead, 4);
InsertTailLinkNode(pHead, 5);
printf("\n3. insert tail node (4,5)\n");
ShowLinkList(pHead); // 3→2→1→4→5
// 4. 查找节点:查找3(存在)、6(不存在)
printf("\n4. find node\n");
FindLinkNode(pHead, 3);
FindLinkNode(pHead, 6);
// 5. 替换节点数据:替换2为20(存在)、7为70(不存在)
printf("\n5. replace node data\n");
ReplaceLinkNode(pHead, 2, 20);
ShowLinkList(pHead); // 3→20→1→4→5
ReplaceLinkNode(pHead, 7, 70);
// 6. 删除节点:删除1(存在)、8(不存在)、5(尾节点)
printf("\n6. delete node\n");
DeleteLinkNode(pHead, 1);
ShowLinkList(pHead); // 3→20→4→5
DeleteLinkNode(pHead, 8);
DeleteLinkNode(pHead, 5);
ShowLinkList(pHead); // 3→20→4
// 7. 再次删除所有3,测试重复删除
printf("\n7. delete all 3\n");
InsertHeadNode(pHead, 3);
ShowLinkList(pHead); // 3→3→20→4
DeleteLinkNode(pHead, 3);
ShowLinkList(pHead); // 20→4
return 0;
}
编译运行结果
1. create empty link list success
link list is empty
2. insert head node (1,2,3)
link list: 3 2 1
3. insert tail node (4,5)
link list: 3 2 1 4 5
4. find node
find node: 3
not find node: 6
5. replace node data
replace success: 1 nodes ( 2 → 20 )
link list: 3 20 1 4 5
replace fail: not find data 7
6. delete node
delete success: 1 nodes ( 1 )
link list: 3 20 4 5
delete fail: not find data 8
delete success: 1 nodes ( 5 )
link list: 3 20 4
7. delete all 3
link list: 3 3 20 4
delete success: 2 nodes ( 3 )
link list: 20 4