【数据结构实战】 C 语言单链表通关:初始化 / 头插 / 尾插 / 增删改查全实现(附图解、可运行完整代码)

第一步:头文件和结构体定义

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

// 定义链表存储的数据类型(可改,比如char、float)
typedef int ElemType;

// 链表节点结构体(核心)
typedef struct Node {
    ElemType data;   // 数据域:存具体值
    struct Node *next; // 指针域:指向下一个节点
} Node;

函数说明 :这是所有链表操作的基础,ElemType 统一管理数据类型,便于后续扩展;Node 结构体是链表的核心单元,包含数据域和指针域,指针域是实现链表 "链式" 的关键。

第二步

函数 1:初始化链表(创建头节点)
cpp 复制代码
/**
 * @brief 初始化带头节点的单链表
 * @return 指向头节点的指针(链表的入口)
 * @note 头节点不存储有效数据,仅用于简化插入/删除逻辑
 */
Node* initList() {
    // 为头节点分配内存
    Node *head = (Node*)malloc(sizeof(Node));
    if (head == NULL) { // 内存分配失败校验
        printf("内存分配失败!\n");
        exit(1); // 终止程序
    }
    head->data = 0;  // 头节点数据域无意义,赋值0仅作占位
    head->next = NULL; // 初始状态下,头节点后无数据节点
    return head;
}

函数说明 :创建链表的 "入口"(头节点),初始化后链表仅包含头节点,无有效数据;malloc 需校验返回值,避免空指针操作;exit(1) 表示异常退出。

函数 2:获取链表尾节点(为尾插法服务)
cpp 复制代码
/**
 * @brief 遍历链表,找到最后一个节点(尾节点)
 * @param L 链表的头节点指针
 * @return 指向尾节点的指针
 * @note 尾节点的特征是next指针为NULL
 */
Node* get_tail(Node *L) {
    Node *p = L; // 从表头开始遍历
    // 只要当前节点的下一个节点不为空,就继续往后走
    while (p->next != NULL) {
        p = p->next;
    }
    return p; // 最终p指向尾节点
}

函数说明 :为 "尾插法" 服务,通过遍历找到链表的最后一个节点,避免每次插入都从头遍历;遍历起点是头节点 L,兼容空链表(仅头节点)的情况。

函数 3:尾插法插入节点
cpp 复制代码
/**
 * @brief 向链表尾部添加新节点(尾插法)
 * @param tail 当前链表的尾节点指针
 * @param e 要插入的节点值
 * @return 新的尾节点指针(插入后,新节点变为尾节点)
 * @note 需先通过get_tail获取初始尾节点
 */
Node* insertTail(Node *tail, ElemType e) {
    // 为新节点分配内存
    Node *p = (Node*)malloc(sizeof(Node));
    if (p == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    p->data = e;    // 给新节点赋值
    p->next = NULL; // 新节点作为尾节点,next置空
    tail->next = p; // 原尾节点指向新节点,完成挂载
    return p;       // 返回新的尾节点,供后续插入使用
}
  • 核心逻辑:先通过 get_tail 找到尾节点,再将新节点挂到尾节点后;
  • 关键注意:新节点 next 必须置空,否则会出现野指针。

**函数说明:**在链表末尾添加节点,插入顺序与链表存储顺序一致;插入后需返回新的尾节点,否则下次插入会指向旧尾节点,导致链表断裂。

函数 4:头插法插入节点

插入是单链表的核心操作,核心口诀:先连后断,避免链表断裂

1. 头插法(插入速度最快,顺序颠倒)

头插法直接在头节点后插入,无需遍历,时间复杂度 O (1),但插入顺序与链表存储顺序相反。

cpp 复制代码
/**
 * @brief 向链表头部(头节点后)添加新节点(头插法)
 * @param L 链表的头节点指针
 * @param e 要插入的节点值
 * @note 插入速度快(无需遍历),但链表顺序与插入顺序相反
 */
// 头插法:在头节点后插入新节点
void insertHead(Node *L, ElemType e) {
    Node *p = (Node*)malloc(sizeof(Node));// 为新节点分配内存
    if (p == NULL) {
        printf("内存分配失败!\n");
        exit(1);//退出程序
    }
    p->data = e;                // 赋值
    p->next = L->next; // ① 新节点先接住原链表头
    L->next = p;       // ② 头节点再指向新节点
}

指针指向说明

  • L :指向原链表的头节点(数据为 0 的那个节点),它是当前链表的起点。
  • p :指向待插入的新节点(数据为 e 的那个节点)。
  • 步骤拆解:先让新节点 p 指向原首节点(p->next = L->next),再让头节点指向新节点(L->next = p),反向操作会导致链表断裂!
  • 输出结果:连续头插 10、20,链表顺序为「20 → 10」(插入顺序与存储顺序相反)

函数说明:核心逻辑是 "先连后断":先让新节点接住原链表的头部,再让头节点指向新节点;无需遍历,时间复杂度 O (1),适合追求插入效率的场景。

函数 5:指定位置插入节点

实际开发中更常用「指定位置增删」,核心是找到「前驱节点」要操作位置的前一个节点)。

cpp 复制代码
/**
 * @brief 向链表指定位置插入节点(pos从1开始计数)
 * @param L 链表的头节点指针
 * @param pos 插入位置(第pos个节点前)
 * @param e 要插入的节点值
 * @return 1-插入成功,0-插入失败(位置越界)
 */
// 指定位置插入(pos从1开始计数)
int insertNode(Node *L, int pos, ElemType e) {
    Node *p = L;   // 从表头开始找插入位置的前驱节点
    int i = 0;     // 记录当前节点的位置(头节点为0)
    
    // 找到第pos-1个节点(插入位置的前驱节点)
    while (i < pos - 1 && p != NULL) {
        p = p->next;
        i++;
    }
    
    if (p == NULL) { // 位置越界(如pos大于链表长度+1)
        printf("插入位置错误!\n");
        return 0;
    }
    
    // 创建新节点
    Node *q = (Node*)malloc(sizeof(Node));
    if (q == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    q->data = e;
    q->next = p->next; // 新节点指向前驱节点的下一个节点(先连后继)
    p->next = q;       // 前驱节点指向新节点,完成插入(再连前驱)
    return 1;
}
  • 核心:插入位置 pos 对应前驱节点是 pos-1,比如插在第 2 位,需找到第 1 个节点作为前驱;
  • 容错:判断 p==NULL 避免越界插入。

函数说明 :插入位置 pos 从 1 开始(符合日常计数习惯),核心是找到 "前驱节点"(要插入位置的前一个节点);若 p 为空,说明位置无效(如链表长度为 3,插入 pos=5);同样遵循 "先连后断" 原则,避免链表断裂。

函数 6:指定位置删除节点

删除是插入的逆操作,核心是「跳过待删节点 + 释放内存」:

cpp 复制代码
* @brief 删除链表中指定位置的节点(pos从1开始计数)
 * @param L 链表的头节点指针
 * @param pos 要删除的节点位置
 * @return 1-删除成功,0-删除失败(位置越界)
 * @note 删除后需释放节点内存,避免内存泄漏
 */
// 指定位置删除节点(pos从1开始)
int deleteNode(Node *L, int pos) {
    Node *p = L; // 找待删节点的前驱节点
    int i = 0;
    //找到第pos-1个节点(待删节点的前驱节点) 
    while (i < pos - 1 && p != NULL) {
        p = p->next;
        i++;
    }
     // 位置越界 或 前驱节点后无节点可删
    if (p == NULL || p->next == NULL) {
        printf("删除位置错误!\n");
        return 0;
    }
    Node *q = p->next; // q指向待删除节点
    p->next = q->next; // 前驱节点跳过待删节点,指向其后继节点
    free(q);           // 释放待删节点的内存
    return 1;
}

函数说明 :删除核心是 "跳过待删节点 + 释放内存";必须先保存待删节点的地址(q),再修改指针,否则会找不到待删节点导致内存泄漏; free(q) 仅释放节点内存,不会自动置空指针,需手动处理逻辑。

函数 7:遍历并打印链表
cpp 复制代码
/**
 * @brief 遍历链表,打印所有有效节点的值
 * @param L 链表的头节点指针
 * @note 从第一个有效节点(L->next)开始遍历
 */
void listNode(Node *L) {
    Node *p = L->next; // 跳过头节点,指向第一个有效节点
    while (p != NULL) { // 遍历到尾节点为止
        printf("%d ", p->data); // 打印当前节点的值
        p = p->next;            // 指向下一个节点
    }
    printf("\n"); // 换行,优化输出格式
}

函数说明 :遍历起点是 L->next(跳过无意义的头节点);循环条件 p != NULL 确保遍历到最后一个节点后停止;是验证链表操作结果的核心函数。

函数 8:计算链表长度
cpp 复制代码
/**
 * @brief 计算链表中有效节点的个数(不含头节点)
 * @param L 链表的头节点指针
 * @return 链表的有效长度
 */
int listLength(Node *L) {
    Node *p = L->next; // 从第一个有效节点开始
    int len = 0;       // 长度计数器
    while (p != NULL) {
        p = p->next;
        len++;         // 每遍历一个节点,长度+1
    }
    return len;
}

函数说明:仅统计有效数据节点的数量,排除头节点;空链表(仅头节点)返回 0,符合直观认知;遍历逻辑与打印函数一致,仅增加了计数功能。

函数 9:释放链表内存
cpp 复制代码
/**
 * @brief 释放链表中所有有效节点的内存(保留头节点)
 * @param L 链表的头节点指针
 * @note 释放后需将头节点的next置空,避免野指针
 */
void freeList(Node *L) {
    Node *p = L->next; // 从第一个有效节点开始
    Node *q;           // 临时保存下一个节点的地址
    while (p != NULL) {
        q = p->next;   // 先保存下一个节点的地址,防止断链
        free(p);       // 释放当前节点
        p = q;         // 指向原下一个节点,继续释放
    }
    L->next = NULL;    // 头节点next置空,标记链表为空
}

函数说明 :释放逻辑是 "先存后放":必须先保存下一个节点的地址(q),再释放当前节点,否则释放后无法找到后续节点;释放后仅保留头节点,若需完全销毁链表,需在外部 free(L)

第三步:主函数(测试所有功能)

cpp 复制代码
// 主函数:测试所有链表操作
int main() {
    // 1. 初始化链表
    Node *list = initList();
    Node *tail = get_tail(list); // 初始尾节点是头节点
    
    // 2. 尾插测试:插入10、20、30
    tail = insertTail(tail, 10);
    tail = insertTail(tail, 20);
    tail = insertTail(tail, 30);
    printf("初始链表(尾插10/20/30):");
    listNode(list); // 输出:10 20 30
    
    // 3. 指定位置插入:在第2位插15
    insertNode(list, 2, 15);
    printf("插入15后:");
    listNode(list); // 输出:10 15 20 30
    
    // 4. 删除测试:删除第2位节点
    deleteNode(list, 2);
    printf("删除第2个节点后:");
    listNode(list); // 输出:10 20 30
    
    // 5. 头插测试:插入5
    insertHead(list, 5);
    printf("头插5后:");
    listNode(list); // 输出:5 10 20 30
    
    // 6. 打印长度
    printf("链表长度:%d\n", listLength(list)); // 输出:4
    
    // 7. 释放内存
    freeList(list);
    printf("释放后链表长度:%d\n", listLength(list)); // 输出:0
    
    // 释放头节点
    free(list);
    return 0;
}

最终操作:运行代码

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

// 定义链表存储的数据类型(可根据需求修改,如char、float等)
typedef int ElemType;

// 定义链表节点结构体
typedef struct Node {
    ElemType data;   // 数据域:存储节点的值
    struct Node *next; // 指针域:指向下一个节点的地址
} Node;

/**
 * @brief 初始化带头节点的单链表
 * @return 指向头节点的指针(链表的入口)
 */
Node* initList() {
    Node *head = (Node*)malloc(sizeof(Node));
    if (head == NULL) { // 内存分配失败校验
        printf("内存分配失败!\n");
        exit(1); // 终止程序
    }
    head->data = 0;  // 头节点数据域无意义,仅占位
    head->next = NULL; // 初始状态下无数据节点
    return head;
}

/**
 * @brief 遍历链表,找到最后一个节点(尾节点)
 * @param L 链表的头节点指针
 * @return 指向尾节点的指针
 */
Node* get_tail(Node *L) {
    Node *p = L; // 从表头开始遍历
    while (p->next != NULL) {
        p = p->next;
    }
    return p; // 最终p指向尾节点
}

/**
 * @brief 向链表尾部添加新节点(尾插法)
 * @param tail 当前链表的尾节点指针
 * @param e 要插入的节点值
 * @return 新的尾节点指针
 */
Node* insertTail(Node *tail, ElemType e) {
    Node *p = (Node*)malloc(sizeof(Node));
    if (p == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    p->data = e;
    p->next = NULL; // 新节点作为尾节点,next置空
    tail->next = p; // 原尾节点指向新节点
    return p;       // 返回新的尾节点
}

/**
 * @brief 向链表头部(头节点后)添加新节点(头插法)
 * @param L 链表的头节点指针
 * @param e 要插入的节点值
 */
void insertHead(Node *L, ElemType e) {
    Node *p = (Node*)malloc(sizeof(Node));
    if (p == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    p->data = e;
    p->next = L->next; // 新节点指向原首节点
    L->next = p;       // 头节点指向新节点
}

/**
 * @brief 向链表指定位置插入节点(pos从1开始计数)
 * @param L 链表的头节点指针
 * @param pos 插入位置
 * @param e 要插入的节点值
 * @return 1-插入成功,0-插入失败
 */
int insertNode(Node *L, int pos, ElemType e) {
    Node *p = L;
    int i = 0;
    // 找到第pos-1个节点(插入位置的前驱节点)
    while (i < pos - 1 && p != NULL) {
        p = p->next;
        i++;
    }
    if (p == NULL) { // 位置越界
        printf("插入位置错误!\n");
        return 0;
    }
    Node *q = (Node*)malloc(sizeof(Node));
    if (q == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    q->data = e;
    q->next = p->next; // 新节点指向前驱节点的下一个节点
    p->next = q;       // 前驱节点指向新节点
    return 1;
}

/**
 * @brief 删除链表中指定位置的节点(pos从1开始计数)
 * @param L 链表的头节点指针
 * @param pos 要删除的节点位置
 * @return 1-删除成功,0-删除失败
 */
int deleteNode(Node *L, int pos) {
    Node *p = L;
    int i = 0;
    // 找到待删节点的前驱节点
    while (i < pos - 1 && p != NULL) {
        p = p->next;
        i++;
    }
    // 位置越界 或 前驱节点后无节点可删
    if (p == NULL || p->next == NULL) {
        printf("删除位置错误!\n");
        return 0;
    }
    Node *q = p->next; // q指向待删除节点
    p->next = q->next; // 前驱节点跳过待删节点
    free(q);           // 释放待删节点内存
    return 1;
}

/**
 * @brief 遍历链表,打印所有有效节点的值
 * @param L 链表的头节点指针
 */
void listNode(Node *L) {
    Node *p = L->next; // 跳过头节点,指向第一个有效节点
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

/**
 * @brief 计算链表中有效节点的个数(不含头节点)
 * @param L 链表的头节点指针
 * @return 链表的有效长度
 */
int listLength(Node *L) {
    Node *p = L->next;
    int len = 0;
    while (p != NULL) {
        len++;
        p = p->next;
    }
    return len;
}

/**
 * @brief 释放链表中所有有效节点的内存(保留头节点)
 * @param L 链表的头节点指针
 */
void freeList(Node *L) {
    Node *p = L->next;
    Node *q;
    while (p != NULL) {
        q = p->next; // 先保存下一个节点地址,防止断链
        free(p);     // 释放当前节点
        p = q;       // 处理下一个节点
    }
    L->next = NULL; // 头节点next置空,避免野指针
}

// 主函数:测试所有链表操作
int main() {
    // 1. 初始化链表
    Node *list = initList();
    Node *tail = get_tail(list); // 初始尾节点为头节点

    // 2. 尾插法测试:插入10、20、30
    tail = insertTail(tail, 10);
    tail = insertTail(tail, 20);
    tail = insertTail(tail, 30);
    printf("初始链表(尾插10/20/30):");
    listNode(list);

    // 3. 指定位置插入测试:在第2位插入15
    insertNode(list, 2, 15);
    printf("在第2位插入15后:");
    listNode(list);

    // 4. 删除节点测试:删除第2位节点
    deleteNode(list, 2);
    printf("删除第2个节点后:");
    listNode(list);

    // 5. 头插法测试:插入5
    insertHead(list, 5);
    printf("头插5后:");
    listNode(list);

    // 6. 打印链表长度
    printf("当前链表长度:%d\n", listLength(list));

    // 7. 释放链表内存
    freeList(list);
    printf("释放内存后链表长度:%d\n", listLength(list));

    // 释放头节点内存
    free(list);
    return 0;
}

运行结果

核心总结

  1. 单链表核心:指针操作遵循「先连后断」,避免链表断裂;
  2. 头插快但顺序反,尾插顺序正但需找尾,指定位置增删找「前驱节点」
  3. 动态内存必释放:malloc 对应 free,否则内存泄漏。
相关推荐
网易独家音乐人Mike Zhou2 小时前
【嵌入式基础】Keil自动编译脚本及环境变量配置
c语言·stm32·单片机·51单片机·嵌入式·keil
2301_821700532 小时前
模板代码生成工具
开发语言·c++·算法
wuhen_n2 小时前
回溯算法入门 - LeetCode经典回溯算法题
前端·javascript·算法
宵时待雨2 小时前
C++笔记归纳12:二叉搜索树
开发语言·数据结构·c++·笔记·算法
炎爆的土豆翔2 小时前
SIMD常见操作,结合样例一文理解
开发语言·c++·算法
仰泳的熊猫2 小时前
题目2305:蓝桥杯2019年第十届省赛真题-等差数列
数据结构·c++·算法·蓝桥杯
ん贤2 小时前
Go map 底层原理
算法·golang·map
奔跑吧邓邓子2 小时前
Hash算法性能优化:从理论到实战的飞跃
算法·性能优化·哈希算法·hash·理论到实战
m0_528174452 小时前
多平台UI框架C++开发
开发语言·c++·算法