单链表专题(完整代码版)

单链表专题(完整代码版)


文章目录

  • 单链表专题(完整代码版)
    • 一、链表的概念及结构
      • [1.1 什么是链表?](#1.1 什么是链表?)
      • [1.2 链表的节点结构](#1.2 链表的节点结构)
    • 二、单链表的完整实现
      • [2.1 节点定义与函数声明](#2.1 节点定义与函数声明)
      • [2.2 打印链表](#2.2 打印链表)
      • [2.3 尾插(在链表末尾插入节点)](#2.3 尾插(在链表末尾插入节点))
      • [2.4 头插(在链表头部插入节点)](#2.4 头插(在链表头部插入节点))
      • [2.5 尾删(删除最后一个节点)](#2.5 尾删(删除最后一个节点))
      • [2.6 头删(删除第一个节点)](#2.6 头删(删除第一个节点))
      • [2.7 查找节点(返回第一个值为x的节点指针)](#2.7 查找节点(返回第一个值为x的节点指针))
      • [2.8 在指定位置之前插入](#2.8 在指定位置之前插入)
      • [2.9 在指定位置之后插入](#2.9 在指定位置之后插入)
      • [2.10 删除指定节点](#2.10 删除指定节点)
      • [2.11 删除指定节点之后的节点](#2.11 删除指定节点之后的节点)
      • [2.12 销毁整个链表](#2.12 销毁整个链表)
    • 三、测试代码示例
    • 四、链表的分类
      • [4.1 单向 or 双向](#4.1 单向 or 双向)
      • [4.2 带头 or 不带头](#4.2 带头 or 不带头)
      • [4.3 循环 or 不循环](#4.3 循环 or 不循环)
      • [4.4 实际中最常用的两种结构](#4.4 实际中最常用的两种结构)
    • 五、总结

一、链表的概念及结构

1.1 什么是链表?

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

![链表结构示意图](https://img-blog.csdnimg.cn/direct/8c8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e.png

你可以把链表想象成火车车厢

  • 淡季时车厢可以减少,旺季时可以增加,不影响其他车厢。
  • 每节车厢独立存在,且每节车厢里都放着下一节车厢的钥匙(指针)。

1.2 链表的节点结构

每个节点由两部分组成:

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

注意:链表中的节点是动态申请的(通常从堆上申请),因此物理地址可能不连续,但逻辑上是连续的。


二、单链表的完整实现

2.1 节点定义与函数声明

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;    // 方便修改数据类型

// 节点结构
typedef struct SListNode {
    SLTDataType data;
    struct SListNode* next;
} SLTNode;

// 函数声明
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); // 在pos之前插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);   // 在pos之后插入
void SLTErase(SLTNode** pphead, SLTNode* pos);      // 删除pos节点
void SLTEraseAfter(SLTNode* pos);                   // 删除pos之后的节点
void SLTDestroy(SLTNode** pphead);                  // 销毁链表

为什么很多函数需要二级指针?

因为我们要修改头指针本身(例如头插、头删、销毁等操作会改变链表的头节点地址),必须传二级指针才能改变实参。

2.2 打印链表

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

2.3 尾插(在链表末尾插入节点)

c 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL) {
        perror("malloc fail");
        return;
    }
    newnode->data = x;
    newnode->next = NULL;

    // 空链表特殊处理
    if (*pphead == NULL) {
        *pphead = newnode;
        return;
    }

    // 找到尾节点
    SLTNode* tail = *pphead;
    while (tail->next != NULL) {
        tail = tail->next;
    }
    tail->next = newnode;
}

2.4 头插(在链表头部插入节点)

c 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL) {
        perror("malloc fail");
        return;
    }
    newnode->data = x;
    newnode->next = *pphead;
    *pphead = newnode;
}

2.5 尾删(删除最后一个节点)

c 复制代码
void SLTPopBack(SLTNode** pphead) {
    // 空链表
    if (*pphead == NULL) return;
    // 只有一个节点
    if ((*pphead)->next == NULL) {
        free(*pphead);
        *pphead = NULL;
        return;
    }
    // 多个节点
    SLTNode* prev = NULL;
    SLTNode* tail = *pphead;
    while (tail->next != NULL) {
        prev = tail;
        tail = tail->next;
    }
    prev->next = NULL;
    free(tail);
}

2.6 头删(删除第一个节点)

c 复制代码
void SLTPopFront(SLTNode** pphead) {
    if (*pphead == NULL) return;
    SLTNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;
}

2.7 查找节点(返回第一个值为x的节点指针)

c 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
    SLTNode* cur = phead;
    while (cur) {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}

2.8 在指定位置之前插入

c 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
    // 如果pos是头节点,相当于头插
    if (*pphead == pos) {
        SLTPushFront(pphead, x);
        return;
    }
    // 找到pos的前一个节点
    SLTNode* prev = *pphead;
    while (prev && prev->next != pos) {
        prev = prev->next;
    }
    if (prev == NULL) return; // pos不在链表中
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL) {
        perror("malloc fail");
        return;
    }
    newnode->data = x;
    newnode->next = pos;
    prev->next = newnode;
}

2.9 在指定位置之后插入

c 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
    if (pos == NULL) return;
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL) {
        perror("malloc fail");
        return;
    }
    newnode->data = x;
    newnode->next = pos->next;
    pos->next = newnode;
}

2.10 删除指定节点

c 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos) {
    if (*pphead == NULL || pos == NULL) return;
    // 删除头节点
    if (*pphead == pos) {
        *pphead = pos->next;
        free(pos);
        return;
    }
    // 找pos的前一个节点
    SLTNode* prev = *pphead;
    while (prev && prev->next != pos) {
        prev = prev->next;
    }
    if (prev == NULL) return; // pos不在链表中
    prev->next = pos->next;
    free(pos);
}

2.11 删除指定节点之后的节点

c 复制代码
void SLTEraseAfter(SLTNode* pos) {
    if (pos == NULL || pos->next == NULL) return;
    SLTNode* del = pos->next;
    pos->next = del->next;
    free(del);
}

2.12 销毁整个链表

c 复制代码
void SLTDestroy(SLTNode** pphead) {
    SLTNode* cur = *pphead;
    while (cur) {
        SLTNode* next = cur->next;
        free(cur);
        cur = next;
    }
    *pphead = NULL;
}

三、测试代码示例

c 复制代码
int main() {
    SLTNode* plist = NULL;

    // 尾插
    SLTPushBack(&plist, 1);
    SLTPushBack(&plist, 2);
    SLTPushBack(&plist, 3);
    SLTPrint(plist); // 1 -> 2 -> 3 -> NULL

    // 头插
    SLTPushFront(&plist, 0);
    SLTPrint(plist); // 0 -> 1 -> 2 -> 3 -> NULL

    // 查找
    SLTNode* pos = SLTFind(plist, 2);
    if (pos) {
        printf("找到了节点:%d\n", pos->data);
        // 在2之前插入99
        SLTInsert(&plist, pos, 99);
        SLTPrint(plist); // 0 -> 1 -> 99 -> 2 -> 3 -> NULL
        // 在2之后插入100
        SLTInsertAfter(pos, 100);
        SLTPrint(plist); // 0 -> 1 -> 99 -> 2 -> 100 -> 3 -> NULL
        // 删除2节点
        SLTErase(&plist, pos);
        SLTPrint(plist); // 0 -> 1 -> 99 -> 100 -> 3 -> NULL
    }

    // 头删
    SLTPopFront(&plist);
    SLTPrint(plist); // 1 -> 99 -> 100 -> 3 -> NULL

    // 尾删
    SLTPopBack(&plist);
    SLTPrint(plist); // 1 -> 99 -> 100 -> NULL

    // 销毁链表
    SLTDestroy(&plist);
    return 0;
}

四、链表的分类

链表的结构非常多样,以下情况组合起来共有 8种

分类维度 类型
单向 / 双向 单向链表、双向链表
带头 / 不带头 带头结点、不带头结点
循环 / 不循环 循环链表、非循环链表

4.1 单向 or 双向

  • 单向链表 :每个节点只有一个指向下一个节点的指针。
    ![单向链表](https://img-blog.csdnimg.cn/direct/xxx
  • 双向链表:每个节点有指向前一个和后一个节点的两个指针,可以双向遍历。

4.2 带头 or 不带头

  • 带头链表 :有一个哨兵位头节点 (不存储有效数据),简化插入删除操作。
    ![带头链表](https://img-blog.csdnimg.cn/direct/xxx
  • 不带头链表:头指针直接指向第一个有效节点。

4.3 循环 or 不循环

4.4 实际中最常用的两种结构

  1. 无头单向非循环链表

    • 结构简单,常用于其他数据结构的子结构(如哈希桶、图的邻接表)
    • 笔试面试中出现频率很高
  2. 带头双向循环链表

    • 结构最复杂,但实现起来反而简单(因为边界条件少)
    • 常用于实际存储数据(如C++ STL中的list)

五、总结

  • 链表是动态数据结构,插入删除不需要移动元素,但需要额外的指针存储空间。
  • 单链表实现简单,但只能单向遍历,删除节点时需要找到前驱。
  • 带头双向循环链表虽然结构复杂,但操作统一,实际开发中更常用。
  • 掌握单链表是理解更复杂链表的基础,也是面试中的高频考点。
相关推荐
CylMK2 小时前
题解:AT_abc382_d [ABC382D] Keep Distance
算法
Dfreedom.2 小时前
计算机视觉全景图
人工智能·算法·计算机视觉·图像算法
Morwit3 小时前
【力扣hot100】 1. 两数之和
数据结构·c++·算法·leetcode·职场和发展
无小道3 小时前
算法——暴力+优化
算法·优化·暴力
Free Tester3 小时前
如何判断 LeakCanary 报告的严重程度
java·jvm·算法
zyq99101_14 小时前
DFS算法实战:经典例题代码解析
python·算法·蓝桥杯·深度优先
智者知已应修善业4 小时前
【51单片机单按键切换广告屏】2023-5-17
c++·经验分享·笔记·算法·51单片机
广州灵眸科技有限公司4 小时前
为RK3588注入澎湃算力:RK1820 AI加速卡完整适配与评测指南
linux·网络·人工智能·物联网·算法
qinian_ztc4 小时前
frida 14.2.18 安装报错解决
算法·leetcode·职场和发展