hyx0411

导航

C语言链表

C语言链表

一、什么是链表

1.1 链表的定义

链表是非连续、动态的线性数据结构,由若干个节点(Node) 串联而成。

每一个节点由两部分组成

• 数据域:存储我们需要的有效数据(int、char、自定义结构体等)

• 指针域:存储下一个节点的内存地址,负责串联所有节点

简单类比:链表就像一条铁链,每一节铁链(节点)自带数据,同时扣住下一节铁链,最后一节悬空(指向NULL)。

1.2 链表 vs 数组(核心区别)

特性 数组 链表
内存分布 连续固定内存 分散动态内存,按需申请
长度限制 必须初始化固定长度、易浪费/溢出 无固定长度,可无限增删
插入/删除 中间操作需大量元素位移,O(n)复杂度 仅需修改指针指向,无需位移
随机访问 支持下标快速访问 只能遍历寻找
内存开销 无额外开销 每个节点需额外占用指针内存

总结使用场景:

• 频繁查询、数据量固定 → 用数组

• 频繁增删、数据量不确定 → 用链表

二、单链表节点定义(固定模板)

C语言中,链表节点依靠结构体+指针实现,这是所有链表操作的基础,通用模板直接背:

// 定义单链表节点结构

typedef struct Node {
    // 数据域:存储数据,可根据需求修改类型
    int data;
    // 指针域:指向同类型的下一个节点
    struct Node* next;
} Node;

关键说明:

  1. typedef 简化结构体命名,后续直接用Node代替struct Node,代码更简洁

  2. next指针必须是struct Node*类型,实现节点自链接

  3. 最后一个节点的next默认赋值为NULL,代表链表结束

三、单链表全套核心操作(可直接运行)

单链表的核心操作共6个:创建节点、头插、尾插、遍历打印、删除节点、清空链表。

3.1 创建新节点(工具函数)

所有增删操作都需要新建节点,封装成独立函数,避免代码冗余:

// 创建一个新节点,返回节点地址
Node* createNode(int val) {
    // 动态申请堆内存
    Node* newNode = (Node*)malloc(sizeof(Node));
    // 内存分配失败判断(必备容错)
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    // 初始化数据域
    newNode->data = val;
    // 新节点默认无后继,指针置空
    newNode->next = NULL;
    return newNode;
}

3.2 头插法(链表头部添加节点)

原理:新节点指向原链表第一个节点,头指针指向新节点
优点:速度最快,时间复杂度O(1),无遍历过程

// 头插:在链表头部插入节点
void headInsert(Node** head, int val) {
    Node* newNode = createNode(val);
    // 新节点后继指向原头节点
    newNode->next = *head;
    // 头指针更新为新节点
    *head = newNode;
}

3.3 尾插法(链表尾部添加节点)

原理:遍历找到链表尾节点,尾节点指针指向新节点
特点:符合日常数据存储逻辑

// 尾插:在链表尾部插入节点
void tailInsert(Node** head, int val) {
    Node* newNode = createNode(val);
    // 链表为空,直接让新节点作为头节点
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    // 工作指针,从头节点遍历到尾节点
    Node* p = *head;
    while (p->next != NULL) {
        p = p->next;
    }
    // 尾节点指向新节点
    p->next = newNode;
}

3.4 遍历打印链表

遍历是链表最基础的查询操作,所有链表查找、修改都依赖遍历:

// 遍历并打印链表所有元素
void printList(Node* head) {
    // 空链表判断
    if (head == NULL) {
        printf("链表为空!\n");
        return;
    }
    Node* p = head;
    printf("链表元素:");
    while (p != NULL) {
        printf("%d ", p->data);
        // 指针后移,访问下一个节点
        p = p->next;
    }
    printf("\n");
}

3.5 按值删除节点

原理:遍历找到目标节点的前驱节点,修改前驱指针跳过目标节点,最后释放内存

// 删除指定数值的节点
void deleteNode(Node** head, int val) {
    // 空链表直接返回
    if (*head == NULL) {
        printf("链表为空,无法删除!\n");
        return;
    }

    Node* p = *head;  // 工作指针
    Node* pre = NULL; // 记录前驱节点

    // 遍历查找目标节点
    while (p != NULL && p->data != val) {
        pre = p;
        p = p->next;
    }

    // 未找到目标节点
    if (p == NULL) {
        printf("未找到目标数据%d!\n", val);
        return;
    }

    // 情况1:删除的是头节点
    if (pre == NULL) {
        *head = p->next;
    }
    // 情况2:删除中间/尾节点
    else {
        pre->next = p->next;
    }

    // 释放被删除节点的内存
    free(p);
    printf("成功删除节点:%d\n", val);
}

3.6 清空整个链表

程序结束前必须清空链表,释放堆内存,杜绝内存泄漏:

// 清空链表所有节点
void clearList(Node** head) {
    Node* p = *head;
    while (p != NULL) {
        // 记录下一个节点
        Node* temp = p->next;
        // 释放当前节点
        free(p);
        // 指针后移
        p = temp;
    }
    // 头指针置空
    *head = NULL;
    printf("链表已清空!\n");
}

四、完整测试主函数

int main() {
    // 定义空链表,头指针初始化为NULL
    Node* head = NULL;

    // 尾插数据 1 2 3
    tailInsert(&head, 1);
    tailInsert(&head, 2);
    tailInsert(&head, 3);
    printList(head);

    // 头插数据 0
    headInsert(&head, 0);
    printList(head);

    // 删除节点2
    deleteNode(&head, 2);
    printList(head);

    // 清空链表
    clearList(&head);
    printList(head);

    return 0;
}

运行结果
链表元素:1 2 3
链表元素:0 1 2 3
成功删除节点:2
链表元素:0 1 3
链表已清空!
链表为空!

五、易错点总结

1. 空指针报错

• 操作链表前必须判断NULL,空链表直接遍历、删除会导致程序崩溃

• 尾节点next必须置空,否则遍历会越界

2. 内存泄漏问题

• malloc申请的堆内存,必须用free释放

• 删除节点、清空链表时,先保存后继节点地址,再释放当前节点

3. 遍历死循环

遍历终止条件必须是p != NULL,不能写p->next != NULL(会漏掉最后一个节点)

六、链表核心复杂度总结

  1. 头插/头删:O(1) (无需遍历,直接修改指针)

  2. 尾插/中间插/任意删/查找:O(n) (需要遍历链表)

  3. 随机访问:不支持,只能从头遍历

结语

链表的核心:指针改变指向,代替数据移动。

相比于数组的死板,链表的动态特性让它成为数据结构的基石,后续栈、队列、哈希表、图的底层实现都离不开链表思想。

posted on 2026-05-23 21:44  芸歆  阅读(59)  评论(1)    收藏  举报