单链表 —— 从原理到 C 语言实现

链表与数组的区别

  • 数组:连续内存空间,支持随机访问,但插入删除效率低,扩容麻烦。
  • 链表离散内存空间,每个节点包含数据域 + 指针域,通过指针串联,不支持随机访问,但插入删除高效。

 单链表结构

单链表每个节点只有一个指针,指向下一个节点,最后一个节点指针指向 NULL 表示结束。

头指针 -> 节点1 -> 节点2 -> 节点3 -> NULL

一,单链表的结构定义

首先我们需要定义单链表的节点结构,每个节点包含数据域指针域

  • 数据域:存储节点的具体数据(本文以int为例);
  • 指针域:存储下一个节点的地址,实现节点的串联。

对应的头文件SList.h核心定义如下:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

// 单链表数据类型(可按需修改,如char、float等)
typedef int SLDataType;
// 单链表节点结构
typedef struct SListNode
{
    SLDataType x;          // 数据域
    struct SListNode* next;// 指针域:指向后继节点
}SLNode;

二,单链表核心操作接口设计

我们先在头文件中声明所有对应函数

// 打印链表
void SLPrint(SLNode* phead);

// 头尾插删
void SLPushBack(SLNode** pphead, SLDataType x);  // 尾插
void SLPushFront(SLNode** pphead, SLDataType x); // 头插
void SLPopBack(SLNode** pphead);                 // 尾删
void SLPopFront(SLNode** pphead);                // 头删

// 查找节点
SLNode* SLFind(SLNode* phead, SLDataType x);

// 指定位置插删
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x); // pos前插入
void SLErase(SLNode** pphead, SLNode* pos);               // 删除pos节点
void SLInsertAfter(SLNode* pos, SLDataType x);             // pos后插入
void SLEraseAfter(SLNode* pos);                           // 删除pos后节点

// 销毁链表
void SListDesTroy(SLNode** pphead);

三,核心操作实现详解

1. 打印链表(SLPrint)

遍历链表并打印每个节点的数据,断言链表头节点非空:

void SLPrint(SLNode* phead)
{
    assert(phead); // 链表非空断言
    SLNode* pcur = phead;
    while (pcur)   // 遍历至链表尾(pcur为NULL时结束)
    {
        printf("%d ", pcur->x);
        pcur = pcur->next;
    }
    printf("\n"); // 换行优化输出
}

2. 尾部插入(SLPushBack)

尾插需要考虑两种情况:链表为空、链表非空。通过二级指针修改链表头节点(空链表时需更新头指针):

void SLPushBack(SLNode** pphead, SLDataType x)
{
    assert(pphead); // 二级指针非空(避免传入NULL)
    // 1. 创建新节点
    SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
    newnode->next = NULL;
    newnode->x = x;
    // 2. 空链表:直接让头指针指向新节点
    if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    // 3. 非空链表:遍历至尾节点,尾节点next指向新节点
    else
    {
        SLNode* pcur = *pphead;
        while (pcur->next) // 找到最后一个节点(pcur->next为NULL)
        {
            pcur = pcur->next;
        }
        pcur->next = newnode;
    }
}

3. 头部插入(SLPushFront)

头插无需遍历链表,只需让新节点的next指向原头节点,再更新头指针即可:

void SLPushFront(SLNode** pphead, SLDataType x)
{
    assert(pphead);
    // 1. 创建新节点
    SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
    newnode->next = NULL;
    newnode->x = x;
    // 2. 空链表:直接更新头指针
    if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    // 3. 非空链表:新节点next指向原头节点,更新头指针
    else
    {
        newnode->next = *pphead;
        *pphead = newnode;
    }
}

4. 尾部删除(SLPopBack)

尾删需考虑两种情况:链表只有一个节点、链表有多个节点。注意释放节点内存并避免野指针:

void SLPopBack(SLNode** pphead)
{
    assert(pphead);
    assert(*pphead); // 链表非空(避免删除空链表)
    // 1. 只有一个节点:释放节点,头指针置空
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    // 2. 多个节点:遍历至倒数第二个节点,释放尾节点
    else
    {
        SLNode* pcur = *pphead;
        SLNode* prev = *pphead;
        while (pcur->next) // 找到尾节点
        {
            prev = pcur;
            pcur = pcur->next;
        }
        free(pcur);    // 释放尾节点
        prev->next = NULL; // 倒数第二个节点next置空
    }
}

5. 头部删除(SLPopFront)

头删逻辑简单:保存原头节点的后继节点,释放原头节点,更新头指针:

void SLPopFront(SLNode** pphead)
{
    assert(pphead);
    assert(*pphead); // 链表非空
    SLNode* next = (*pphead)->next; // 保存后继节点
    free(*pphead);                  // 释放原头节点
    *pphead = next;                 // 更新头指针
}

6. 查找节点(SLFind)

遍历链表,找到数据等于目标值的节点并返回其地址,未找到则返回NULL

SLNode* SLFind(SLNode* phead, SLDataType x)
{
    assert(phead);
    SLNode* pcur = phead;
    while (pcur)
    {
        if (pcur->x == x)
            return pcur; // 找到目标节点,返回地址
        pcur = pcur->next;
    }
    return NULL; // 未找到
}

7. 指定位置前插入(SLInsert)

需区分两种情况:插入位置是头节点、插入位置是中间 / 尾节点。核心是找到pos的前驱节点:

void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
    assert(pphead);
    assert(*pphead);
    // 1. 创建新节点
    SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
    newnode->next = NULL;
    newnode->x = x;
    // 2. 插入位置是头节点:等价于头插
    if (*pphead == pos)
    {
        newnode->next = *pphead;
        *pphead = newnode;
    }
    // 3. 插入位置是中间/尾节点:找到pos前驱节点
    else
    {
        SLNode* pcur = *pphead;
        while (pcur->next != pos) // 找到pos的前驱节点
        {
            pcur = pcur->next;
        }
        pcur->next = newnode;     // 前驱节点指向新节点
        newnode->next = pos;      // 新节点指向pos
    }
}

8. 删除指定位置节点(SLErase)

同理区分头节点和非头节点,找到pos的前驱节点后,修改指针并释放pos节点:

void SLErase(SLNode** pphead, SLNode* pos)
{
    assert(pphead);
    assert(*pphead);
    // 1. 删除头节点
    if (*pphead == pos)
    {
        free(*pphead);
        *pphead = NULL;
    }
    // 2. 删除中间/尾节点
    else
    {
        SLNode* pcur = *pphead;
        while (pcur->next != pos) // 找到pos前驱节点
        {
            pcur = pcur->next;
        }
        pcur->next = pos->next;   // 前驱节点指向pos后继节点
        free(pos);                // 释放pos节点
    }
}

9. 指定位置后插入(SLInsertAfter)

无需遍历链表,直接修改pos和新节点的指针即可:

void SLInsertAfter(SLNode* pos, SLDataType x)
{
    assert(pos); // pos节点非空
    SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
    newnode->next = NULL;
    newnode->x = x;
    newnode->next = pos->next; // 新节点指向pos后继
    pos->next = newnode;       // pos指向新节点
}

10. 删除指定位置后节点(SLEraseAfter)

保存pos的后继节点,修改指针后释放该节点:

void SLEraseAfter(SLNode* pos)
{
    assert(pos);
    SLNode* next = pos->next;   // 保存pos后继节点
    pos->next = next->next;     // pos指向后继的后继
    free(next);                 // 释放后继节点
}

11. 销毁链表(SListDesTroy)

遍历链表,逐个释放节点内存,最终将头指针置空(避免野指针):

void SListDesTroy(SLNode** pphead)
{
    SLNode* pcur = *pphead;
    while (pcur->next) // 遍历至最后一个节点
    {
        SLNode* next = pcur->next;
        free(pcur);    // 释放当前节点
        pcur = next;   // 移动至下一个节点
    }
    *pphead = NULL;    // 头指针置空

四,单链表 vs 顺序表(核心对比)

特性单链表顺序表(数组)
随机访问不支持(O (n))支持(O (1))
插入 / 删除(头部)O(1)O (n)(移动元素)
插入 / 删除(尾部)O (n)(无尾指针)O (1)(不扩容)
内存分配动态、非连续静态 / 动态、连续
空间开销额外指针域无额外开销(可能浪费)
缓存友好性差(节点分散)好(连续内存)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值