数据结构:带头节点单链表

一、环境与基础定义

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; // 返回删除个数
}

核心

  1. 双指针遍历是链表删除的标准写法,避免丢失节点地址导致断链;
  2. 删除节点后必须释放内存free(pTmpNode)),否则会造成内存泄漏;
  3. 释放内存后,需将当前节点指向新的后继节点,不可直接后移双指针。

三、全套功能测试(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 
相关推荐
定偶2 小时前
MySQL多表连接查询详解
c语言·数据库·mysql
鹿角片ljp2 小时前
力扣9.回文数-转字符双指针和反转数字
java·数据结构·算法
梦梦代码精2 小时前
开源、免费、可商用:BuildingAI一站式体验报告
开发语言·前端·数据结构·人工智能·后端·开源·知识图谱
呱呱巨基3 小时前
c语言 文件操作
c语言·开发语言·c++·笔记·学习
云小逸4 小时前
【Nmap 设备类型识别技术】整体概况
服务器·c语言·网络·c++·nmap
梵刹古音4 小时前
【C语言】 跳转语句
c语言·开发语言·算法
C语言小火车5 小时前
Qt样式实现方式详解:六大方法全面解析
c语言·c++·qt·学习
what丶k6 小时前
深度解析:以Kafka为例,消息队列消费幂等性的实现方案与生产实践
java·数据结构·kafka
AllData公司负责人6 小时前
【亲测好用】实时开发平台能力演示
java·c语言·数据库