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

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

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

目录


一、链表的概念及结构

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 常用链表类型

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

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

相关推荐
闪电悠米2 小时前
力扣hot100-41.缺失的第一个正数-原地哈希详解
数据结构·算法·哈希算法
WL学习笔记3 小时前
堆(数据结构)
数据结构·
wabs6667 小时前
关于图论【卡码网100.最大岛屿的面积的思考】
数据结构·算法·图论
yuannl108 小时前
图的最短路径Dijkstra
数据结构
zander2589 小时前
138. 随机链表的复制
数据结构·算法·链表
过期动态9 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面10 小时前
栈在递归中的应用
数据结构·算法
皓月斯语10 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king10 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
Adios79418 小时前
设置交集大小至少为2
数据结构·算法·leetcode