单链表专题:从概念到实现

单链表专题:从概念到实现

链表是数据结构中的重要基石,它解决了顺序表在中间插入删除时效率低下的问题。本文将系统讲解链表的概念、结构,带你从零实现一个单链表(包括增删改查、销毁等操作),并介绍链表的多种分类。

目录


一、链表的概念及结构

1.1 什么是链表

链表是一种物理存储结构上非连续、非顺序 的存储结构,数据元素的逻辑顺序通过链表中的指针链接次序实现。

类比火车车厢 :每节车厢独立存在,车厢内存放着下一节车厢的钥匙(地址)。

1.2 节点结构

链表中的每个节点独立申请(通常从堆上申请),包含两部分:

  • 数据域:存储当前节点的数据
  • 指针域:存储下一个节点的地址
c 复制代码
struct SListNode {
    int data;                // 数据域
    struct SListNode* next;  // 指针域,指向下一个节点
};

关键点:节点在逻辑上连续,物理上不一定连续;每次插入数据时才申请新节点,实现了按需分配。


二、实现单链表

2.1 基本结构定义

c 复制代码
typedef int SLTDataType;
typedef struct SListNode {
    SLTDataType data;
    struct SListNode* next;
} SLTNode;

2.2 接口声明

c 复制代码
// 打印链表
void SLTPrint(SLTNode* phead);

// 头插 / 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);

// 头删 / 尾删
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);

// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

// 在指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

// 删除指定位置节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

// 在指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

// 删除指定位置之后的节点
void SLTEraseAfter(SLTNode* pos);

// 销毁链表
void SListDestroy(SLTNode** pphead);

注意 :尾插、头插等需要修改头指针的函数,参数需要使用二级指针(SLTNode**),因为可能要改变头指针的指向。

2.3 核心函数实现

创建新节点
c 复制代码
SLTNode* BuySLTNode(SLTDataType x) {
    SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newNode == NULL) {
        perror("malloc fail");
        exit(-1);
    }
    newNode->data = x;
    newNode->next = NULL;
    return newNode;
}
尾插
c 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
    SLTNode* newNode = BuySLTNode(x);
    if (*pphead == NULL) {
        *pphead = newNode;
    } else {
        SLTNode* tail = *pphead;
        while (tail->next) {
            tail = tail->next;
        }
        tail->next = newNode;
    }
}
头插
c 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
    SLTNode* newNode = BuySLTNode(x);
    newNode->next = *pphead;
    *pphead = newNode;
}
尾删
c 复制代码
void SLTPopBack(SLTNode** pphead) {
    assert(*pphead);
    if ((*pphead)->next == NULL) {
        free(*pphead);
        *pphead = NULL;
    } else {
        SLTNode* prev = NULL;
        SLTNode* tail = *pphead;
        while (tail->next) {
            prev = tail;
            tail = tail->next;
        }
        free(tail);
        prev->next = NULL;
    }
}
头删
c 复制代码
void SLTPopFront(SLTNode** pphead) {
    assert(*pphead);
    SLTNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;
}
在指定位置之前插入
c 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
    assert(pos);
    if (*pphead == pos) {
        SLTPushFront(pphead, x);
    } else {
        SLTNode* prev = *pphead;
        while (prev->next != pos) {
            prev = prev->next;
        }
        SLTNode* newNode = BuySLTNode(x);
        prev->next = newNode;
        newNode->next = pos;
    }
}
删除指定位置节点
c 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos) {
    assert(*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);
    }
}
在指定位置之后插入(更简单)
c 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
    assert(pos);
    SLTNode* newNode = BuySLTNode(x);
    newNode->next = pos->next;
    pos->next = newNode;
}
删除指定位置之后的节点
c 复制代码
void SLTEraseAfter(SLTNode* pos) {
    assert(pos && pos->next);
    SLTNode* del = pos->next;
    pos->next = del->next;
    free(del);
}
销毁链表
c 复制代码
void SListDestroy(SLTNode** pphead) {
    SLTNode* cur = *pphead;
    while (cur) {
        SLTNode* next = cur->next;
        free(cur);
        cur = next;
    }
    *pphead = NULL;
}

2.4 打印链表

c 复制代码
void SLTPrint(SLTNode* phead) {
    SLTNode* cur = phead;
    while (cur) {
        printf("%d -> ", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

2.5 使用示例

c 复制代码
int main() {
    SLTNode* plist = NULL;
    SLTPushBack(&plist, 1);
    SLTPushBack(&plist, 2);
    SLTPushFront(&plist, 0);
    SLTPrint(plist);  // 0 -> 1 -> 2 -> NULL
    
    SLTNode* pos = SLTFind(plist, 1);
    if (pos) SLTInsert(&plist, pos, 99);
    SLTPrint(plist);  // 0 -> 99 -> 1 -> 2 -> NULL
    
    SListDestroy(&plist);
    return 0;
}

三、链表的分类

链表有多种组合,共有 8 种可能的结构((2 \times 2 \times 2)):

3.1 单向 / 双向

  • 单向链表:每个节点只有一个指向下一个节点的指针。
  • 双向链表:每个节点既有指向下一个节点的指针,也有指向上一个节点的指针。

3.2 带头 / 不带头

  • 带头链表:有一个哨兵位头节点(不存储有效数据),简化插入删除逻辑。
  • 不带头链表:头指针直接指向第一个有效节点。

3.3 循环 / 非循环

  • 循环链表:尾节点的 next 指向头节点(或哨兵位),形成环。
  • 非循环链表:尾节点的 next 指向 NULL。

3.4 常用链表类型

  • 无头单向非循环链表:结构简单,常用于笔试面试题,或作为其他数据结构的子结构(如哈希桶、图的邻接表)。
  • 带头双向循环链表:结构最复杂,但实际使用中优势明显(如简化插入删除操作),常用于存储数据。

总结:单链表解决了顺序表中间插入删除效率低和扩容浪费空间的问题。每个节点独立申请,通过指针链接,实现了真正的按需分配。掌握单链表的增删改查是学习更复杂数据结构的基础。在实际项目中,根据需求选择合适的链表结构(如带头双向循环链表更常用)。建议读者动手实现所有接口,并尝试用单链表改写通讯录项目。

相关推荐
CSharp精选营4 天前
关系型 vs 非关系型:从原理到选型,一文搞定数据库核心分类
数据结构·nosql·关系型数据库·非关系型数据库·技术选型
刘马想放假7 天前
Modbus 全栈技术解析:TCP、RTU、ASCII、RTU over TCP
数据结构·网络协议
北域码匠8 天前
冒泡排序太慢?鸡尾酒排序双向优化,原生 C# 零第三方库完整代码
数据结构·排序算法·泛型·c# 算法·鸡尾酒排序·原生 c# 开发·冒泡排序优化·嵌入式算法
Darling噜啦啦15 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试
小小工匠16 天前
Redis - 事务机制:能实现 ACID 属性吗
数据结构·redis·性能优化·并发·持久化
玖玥拾16 天前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
Qres82116 天前
算法复键——树状数组
数据结构·算法
牛油果子哥q16 天前
并查集(DSU)超精讲,路径压缩、按秩合并、万能模板、连通性判定、最小生成树与刷题实战全解
数据结构·c++·最小生成树·并查集
凌波粒16 天前
LeetCode--491.递增子序列(回溯算法)
数据结构·算法·leetcode
WL学习笔记16 天前
单项不带头不循环链表
数据结构·链表