C语言单向链表实现详解:从基础操作到完整测试

前言

链表是数据结构中最基础也是最重要的一种线性结构,与数组不同,链表不需要连续的内存空间,通过指针将零散的内存块连接起来。本文将详细分析一个完整的C语言单向链表实现,涵盖创建、插入、删除、查找等核心操作,并通过完整的测试案例验证其正确性。

目录

前言

正文

链表结构定义

核心功能实现

[1. 链表打印](#1. 链表打印)

[2. 节点创建](#2. 节点创建)

[3. 尾插操作](#3. 尾插操作)

[4. 头插操作](#4. 头插操作)

[5. 尾删操作](#5. 尾删操作)

[6. 查找操作](#6. 查找操作)

[7. 指定位置插入](#7. 指定位置插入)

[8. 删除操作](#8. 删除操作)

测试代码分析

总结


正文

链表结构定义

在头文件SList.h中,我们定义了链表的基本结构:

复制代码
typedef int SLTDataType;

typedef struct SListNode
{
    SLTDataType data;
    struct SListNode* next;
}SLTNode;

这是一个典型的单向链表节点结构,包含数据域data和指向下一个节点的指针next

核心功能实现

1. 链表打印
复制代码
void SLTPrint(SLTNode* phead)
{
    SLTNode* pcur = phead;
    while (pcur)
    {
        printf("%d->", pcur->data);
        pcur = pcur->next;
    }
    printf("NULL\n");
}

这个函数遍历整个链表并打印每个节点的数据,用"->"连接,最后以"NULL"结束,直观展示链表结构。

2. 节点创建
复制代码
SLTNode* SLTBuyNode(SLTDataType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL)
    {
        perror("malloc fail!");
        exit(1);
    }
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

封装了节点创建逻辑,包含内存分配失败的错误处理,确保程序健壮性。

3. 尾插操作
复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
    SLTNode* newnode = SLTBuyNode(x);

    if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    else
    {
        SLTNode* ptail = *pphead;
        while (ptail->next)
        {
            ptail = ptail->next;
        }
        ptail->next = newnode;
    }
}

关键点分析:

  • 使用二级指针pphead,因为可能修改头指针(当链表为空时)

  • 区分空链表和非空链表两种情况

  • 时间复杂度为O(n),需要遍历找到尾节点

4. 头插操作
复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
    SLTNode* newnode = SLTBuyNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}

头插操作相对简单,时间复杂度为O(1),直接将新节点作为新的头节点。

5. 尾删操作
复制代码
void SLTPopBack(SLTNode** pphead)
{
    assert(pphead && *pphead);
    
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLTNode* prev = *pphead;
        SLTNode* ptail = *pphead;

        while (ptail->next)
        {
            prev = ptail;
            ptail = ptail->next;
        }

        free(ptail);
        prev->next = NULL;
    }
}

关键点分析:

  • 需要维护前驱指针prev,因为单链表无法直接访问前一个节点

  • 处理单节点和多节点两种情况的边界条件

  • 释放内存后要将指针置为NULL,避免野指针

6. 查找操作
复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
    assert(phead);
    SLTNode* pcur = phead;
    while (pcur)
    {
        if (pcur->data == x)
            return pcur;
        pcur = pcur->next;
    }
    return NULL;
}

线性查找,时间复杂度O(n),返回找到的节点指针或NULL。

7. 指定位置插入

在指定位置之前插入:

复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
    assert(pphead && *pphead);
    assert(pos);
    
    if (*pphead == pos)
    {
        SLTPushFront(pphead,x);
    }
    else
    {
        SLTNode* prev = *pphead;
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        SLTNode* newnode = SLTBuyNode(x);
        newnode->next = pos;
        prev->next = newnode;
    }
}

在指定位置之后插入:

复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
    assert(pos);
    SLTNode* newnode = SLTBuyNode(x);
    newnode->next = pos->next;
    pos->next = newnode;
}

对比分析:

  • 前插需要找到前驱节点,时间复杂度O(n)

  • 后插直接操作,时间复杂度O(1)

  • 前插在头节点位置时退化为头插操作

8. 删除操作

删除指定节点:

复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    assert(pphead &&*pphead);
    assert(pos);
    
    if (*pphead == pos)
    {
        SLTPopFront(pphead);
    }
    else
    {
        SLTNode* prev = *pphead;
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        prev->next = pos->next;
        free(pos);
    }
}

删除指定位置后的节点:

复制代码
void SLTEraseAfter(SLTNode* pos)
{
    assert(pos && pos->next);
    SLTNode* del = pos->next;
    pos->next = del->next;
    free(del);
}

测试代码分析

测试函数SListTest02全面验证了链表的各种操作:

复制代码
void SListTest02()
{
    SLTNode* plist = NULL;
    
    // 构建基础链表:1->2->3->4->5->NULL
    SLTPushBack(&plist, 1);
    SLTPushBack(&plist, 2);
    SLTPushBack(&plist, 3);
    SLTPushBack(&plist, 4);
    SLTPushBack(&plist, 5);
    SLTPrint(plist); // 预期:1->2->3->4->5->NULL
    
    // 头插构建:6->7->8->9->10->1->2->3->4->5->NULL
    SLTPushFront(&plist, 10);
    SLTPushFront(&plist, 9);
    SLTPushFront(&plist, 8);
    SLTPushFront(&plist, 7);
    SLTPushFront(&plist, 6);
    SLTPrint(plist);
    
    // 删除操作验证
    SLTPopBack(&plist);  // 删除尾节点5
    SLTPrint(plist);     // 6->7->8->9->10->1->2->3->4->NULL
    
    SLTPopFront(&plist); // 删除头节点6
    SLTPrint(plist);     // 7->8->9->10->1->2->3->4->NULL
    
    // 查找功能测试
    SLTNode* find1 = SLTFind(plist, 8);
    if (find1 != NULL)
    {
        printf("找到了\n");
    }
    
    // 复杂插入操作
    SLTNode* find2 = SLTFind(plist, 7);
    SLTInsert(&plist, find2, 100);  // 在7之前插入100
    SLTPrint(plist); // 100->7->8->9->10->1->2->3->4->NULL
    
    // 后续各种插入、删除操作...
    
    // 最终销毁链表
    SListDestroy(&plist);
    SLTPrint(plist); // 应该打印NULL
}

测试亮点:

  • 覆盖了所有边界情况(头节点、尾节点操作)

  • 验证了操作的顺序正确性

  • 测试了查找失败的情况

  • 确保内存正确释放

总结

通过这个完整的单向链表实现,我们可以得出以下几点重要结论:

  1. 二级指针的必要性:在需要修改头指针的函数中必须使用二级指针,这是很多初学者容易出错的地方。

  2. 边界条件处理:链表操作要特别注意空链表、单节点链表、头尾节点等边界情况的处理。

  3. 时间复杂度分析

    • 头插、头删:O(1)

    • 尾插、尾删:O(n)

    • 查找:O(n)

    • 指定位置前插:O(n)

    • 指定位置后插:O(1)

  4. 内存管理:每次分配内存都要检查是否成功,释放内存后要及时置空指针。

  5. 代码复用:通过复用已有的头插、头删函数,提高了代码的复用性和可维护性。

这个链表实现展示了良好的编程实践,包括错误处理、断言检查、代码复用等,是一个很好的学习范例。理解这个实现对于掌握更复杂的数据结构和指针操作具有重要意义。

相关推荐
初夏睡觉1 小时前
循环比赛日程表 题解
数据结构·c++·算法
好好研究1 小时前
SpringMVC框架 - 异常处理
java·开发语言·spring·mvc
songroom2 小时前
Rust: 量化策略回测与简易线程池构建(MPMC)
开发语言·后端·rust
CS_浮鱼2 小时前
【Linux编程】线程同步与互斥
linux·网络·c++
派大星爱吃鱼2 小时前
素数检验方法
算法
摇滚侠2 小时前
Vue 项目实战《尚医通》,完成确定挂号业务,笔记46
java·开发语言·javascript·vue.js·笔记
十五年专注C++开发2 小时前
libdatrie: 一个高效的 基于双数组字典树(Double-Array Trie)的C语言函数库
c语言·开发语言·trie
Greedy Alg2 小时前
LeetCode 72. 编辑距离(中等)
算法
xinxingrs2 小时前
贪心算法、动态规划以及相关应用(python)
笔记·python·学习·算法·贪心算法·动态规划