【C语言程序设计】第37篇:链表数据结构(一):单向链表的实现

1 引言

数组有一个明显的缺点:插入或删除元素时,需要移动大量数据。例如,在数组开头插入一个元素,需要将所有元素向后移动一位。

c

复制代码
/* 数组在开头插入元素需要移动所有元素 */
for (int i = size; i > 0; i--) {
    arr[i] = arr[i-1];
}
arr[0] = new_value;

链表通过指针连接各个节点,插入和删除只需修改指针,不需要移动数据。

c

复制代码
/* 链表的节点结构 */
typedef struct Node {
    int data;           /* 数据域 */
    struct Node *next;  /* 指针域,指向下一个节点 */
} Node;

每个节点包含数据和指向下一个节点的指针,最后一个节点的指针为 NULL。这种结构就是单向链表


2 链表节点的结构体定义

2.1 节点结构

单向链表节点包含两个部分:

  • 数据域:存储实际数据(可以是任意类型)

  • 指针域:指向下一个节点

c

复制代码
typedef struct Node {
    int data;           /* 数据域 */
    struct Node *next;  /* 指针域 */
} Node;

注意 :在结构体内部,不能直接用 Node*,因为 Node 这个类型名还没有定义完成,必须用 struct Node*

2.2 带头节点 vs 不带头节点

链表有两种常见形式:

类型 特点 优点 缺点
带头节点 有一个不存储数据的头节点 操作统一,不需要单独处理空链表 多占用一个节点空间
不带头节点 头指针直接指向第一个数据节点 节省空间 插入删除需要单独处理头指针变化

本章使用不带头节点的方式,更直观地理解指针操作。

c

复制代码
Node *head = NULL;  /* 空链表,头指针指向 NULL */

2.3 头指针的作用

头指针指向链表的第一个节点,是整个链表的入口。

text

复制代码
head
  ↓
┌─────┐    ┌─────┐    ┌─────┐
│ data│ → │ data│ → │ data│ → NULL
│ next│    │ next│    │ next│
└─────┘    └─────┘    └─────┘

3 动态创建节点

3.1 创建新节点

c

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

typedef struct Node {
    int data;
    struct Node *next;
} Node;

/* 创建新节点 */
Node* create_node(int value)
{
    Node *new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

3.2 头插法

在链表头部插入新节点:

c

复制代码
/* 头插法:新节点成为新的头节点 */
Node* insert_head(Node *head, int value)
{
    Node *new_node = create_node(value);
    if (new_node == NULL) return head;
    
    new_node->next = head;  /* 新节点指向原来的头节点 */
    return new_node;        /* 新节点成为新的头节点 */
}

执行过程

text

复制代码
原链表:head → [10] → [20] → NULL
插入 5:new_node → [5] → head (指向[10])
结果:head → [5] → [10] → [20] → NULL

3.3 尾插法

在链表尾部插入新节点:

c

复制代码
/* 尾插法:新节点添加到末尾 */
Node* insert_tail(Node *head, int value)
{
    Node *new_node = create_node(value);
    if (new_node == NULL) return head;
    
    /* 空链表特殊处理 */
    if (head == NULL) {
        return new_node;
    }
    
    /* 找到最后一个节点 */
    Node *current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    current->next = new_node;
    return head;
}

3.4 在指定位置插入

c

复制代码
/* 在指定位置插入(位置从0开始计数) */
Node* insert_at(Node *head, int pos, int value)
{
    if (pos < 0) return head;
    
    Node *new_node = create_node(value);
    if (new_node == NULL) return head;
    
    /* 插入头部 */
    if (pos == 0) {
        new_node->next = head;
        return new_node;
    }
    
    /* 找到前一个节点 */
    Node *prev = head;
    for (int i = 0; i < pos - 1 && prev != NULL; i++) {
        prev = prev->next;
    }
    
    /* 位置超出链表长度 */
    if (prev == NULL) {
        free(new_node);
        printf("位置无效\n");
        return head;
    }
    
    new_node->next = prev->next;
    prev->next = new_node;
    return head;
}

4 链表的遍历

4.1 遍历并打印

c

复制代码
/* 遍历链表并打印所有元素 */
void print_list(Node *head)
{
    Node *current = head;
    printf("链表内容:");
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

4.2 获取链表长度

c

复制代码
/* 计算链表长度 */
int list_length(Node *head)
{
    int count = 0;
    Node *current = head;
    while (current != NULL) {
        count++;
        current = current->next;
    }
    return count;
}

4.3 查找元素

c

复制代码
/* 查找值为value的节点,返回位置(从0开始),找不到返回-1 */
int find_node(Node *head, int value)
{
    Node *current = head;
    int pos = 0;
    
    while (current != NULL) {
        if (current->data == value) {
            return pos;
        }
        current = current->next;
        pos++;
    }
    return -1;
}

/* 获取指定位置的节点值 */
int get_at(Node *head, int pos, int *value)
{
    Node *current = head;
    int i = 0;
    
    while (current != NULL && i < pos) {
        current = current->next;
        i++;
    }
    
    if (current == NULL) {
        return -1;  /* 位置无效 */
    }
    
    *value = current->data;
    return 0;
}

5 链表的释放

5.1 释放整个链表

链表节点是动态分配的,使用完后必须释放,否则会造成内存泄漏。

c

复制代码
/* 释放整个链表 */
void free_list(Node *head)
{
    Node *current = head;
    Node *next;
    
    while (current != NULL) {
        next = current->next;  /* 先保存下一个节点地址 */
        free(current);         /* 释放当前节点 */
        current = next;        /* 移动到下一个节点 */
    }
}

为什么不能直接 free(current) 再 current = current->next?

因为 free(current) 后,current->next 已经不能访问了。

5.2 删除指定节点

c

复制代码
/* 删除第一个值为value的节点 */
Node* delete_node(Node *head, int value)
{
    if (head == NULL) return NULL;
    
    /* 删除头节点 */
    if (head->data == value) {
        Node *temp = head;
        head = head->next;
        free(temp);
        return head;
    }
    
    /* 查找要删除的节点 */
    Node *prev = head;
    Node *current = head->next;
    
    while (current != NULL && current->data != value) {
        prev = current;
        current = current->next;
    }
    
    /* 找到则删除 */
    if (current != NULL) {
        prev->next = current->next;
        free(current);
    }
    
    return head;
}

6 完整示例:学生成绩链表

c

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

/* 学生节点结构 */
typedef struct StudentNode {
    char name[50];
    int id;
    float score;
    struct StudentNode *next;
} StudentNode;

/* 创建学生节点 */
StudentNode* create_student(const char *name, int id, float score)
{
    StudentNode *new_node = (StudentNode*)malloc(sizeof(StudentNode));
    if (new_node == NULL) return NULL;
    
    strcpy(new_node->name, name);
    new_node->id = id;
    new_node->score = score;
    new_node->next = NULL;
    return new_node;
}

/* 插入到头部 */
StudentNode* insert_head(StudentNode *head, const char *name, int id, float score)
{
    StudentNode *new_node = create_student(name, id, score);
    if (new_node == NULL) return head;
    
    new_node->next = head;
    return new_node;
}

/* 插入到尾部 */
StudentNode* insert_tail(StudentNode *head, const char *name, int id, float score)
{
    StudentNode *new_node = create_student(name, id, score);
    if (new_node == NULL) return head;
    
    if (head == NULL) {
        return new_node;
    }
    
    StudentNode *current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = new_node;
    return head;
}

/* 打印所有学生 */
void print_students(StudentNode *head)
{
    StudentNode *current = head;
    printf("\n学生列表:\n");
    printf("姓名\t\t学号\t成绩\n");
    printf("------------------------\n");
    
    while (current != NULL) {
        printf("%-15s %d\t%.1f\n", 
               current->name, current->id, current->score);
        current = current->next;
    }
    printf("\n");
}

/* 计算平均分 */
float average_score(StudentNode *head)
{
    if (head == NULL) return 0;
    
    StudentNode *current = head;
    float sum = 0;
    int count = 0;
    
    while (current != NULL) {
        sum += current->score;
        count++;
        current = current->next;
    }
    
    return sum / count;
}

/* 查找学生 */
StudentNode* find_student(StudentNode *head, int id)
{
    StudentNode *current = head;
    while (current != NULL) {
        if (current->id == id) {
            return current;
        }
        current = current->next;
    }
    return NULL;
}

/* 删除学生 */
StudentNode* delete_student(StudentNode *head, int id)
{
    if (head == NULL) return NULL;
    
    if (head->id == id) {
        StudentNode *temp = head;
        head = head->next;
        free(temp);
        return head;
    }
    
    StudentNode *prev = head;
    StudentNode *current = head->next;
    
    while (current != NULL && current->id != id) {
        prev = current;
        current = current->next;
    }
    
    if (current != NULL) {
        prev->next = current->next;
        free(current);
    }
    
    return head;
}

/* 释放链表 */
void free_students(StudentNode *head)
{
    StudentNode *current = head;
    StudentNode *next;
    
    while (current != NULL) {
        next = current->next;
        free(current);
        current = next;
    }
}

int main(void)
{
    StudentNode *head = NULL;
    
    /* 插入学生 */
    head = insert_tail(head, "张三", 1001, 88.5);
    head = insert_tail(head, "李四", 1002, 92.0);
    head = insert_tail(head, "王五", 1003, 78.5);
    head = insert_head(head, "赵六", 1000, 95.0);
    
    /* 打印 */
    print_students(head);
    
    /* 平均分 */
    printf("平均分:%.2f\n", average_score(head));
    
    /* 查找 */
    StudentNode *found = find_student(head, 1002);
    if (found) {
        printf("找到学生:%s 成绩:%.1f\n", found->name, found->score);
    }
    
    /* 删除 */
    head = delete_student(head, 1002);
    printf("删除学号1002后:\n");
    print_students(head);
    
    /* 释放 */
    free_students(head);
    
    return 0;
}

7 常见错误与注意事项

7.1 忘记检查 malloc 返回值

c

复制代码
Node *new_node = (Node*)malloc(sizeof(Node));
new_node->data = value;  /* 如果 malloc 失败,new_node 为 NULL,程序崩溃 */

7.2 修改链表时丢失节点

c

复制代码
/* 错误:在删除节点时,直接 current = current->next 后释放 */
Node *temp = current;
current = current->next;  /* 先移动指针 */
free(temp);  /* 正确,但顺序要对 */

7.3 对空链表解引用

c

复制代码
Node *current = head;
while (current->next != NULL) {  /* 如果 head 为 NULL,崩溃 */
    /* ... */
}

7.4 忘记释放链表

c

复制代码
void func(void)
{
    Node *head = create_list();
    /* 使用链表... */
    /* 忘记 free_list(head) → 内存泄漏 */
}

7.5 指针悬挂

c

复制代码
Node *p = head;
free_list(head);
p->data = 10;  /* 错误!p 指向已释放的内存 */

7.6 循环引用

c

复制代码
/* 错误:节点指向自己或形成环 */
last->next = first;  /* 形成循环链表,遍历时死循环 */

8 本章小结

本章系统介绍了单向链表的实现:

1. 链表节点结构

c

复制代码
typedef struct Node {
    int data;
    struct Node *next;
} Node;

2. 创建节点

  • 使用 malloc 动态分配

  • 设置数据域和指针域(next 置为 NULL)

3. 插入操作

  • 头插法:新节点指向原头节点,头指针指向新节点

  • 尾插法:找到最后一个节点,将其 next 指向新节点

  • 中间插入:找到前一个节点,修改指针

4. 遍历操作

  • 从头指针开始,依次访问每个节点

  • current = current->next 移动指针

  • current == NULL 时结束

5. 释放操作

  • 遍历链表,逐个 free 节点

  • 释放前先保存下一个节点地址

6. 注意事项

  • 总是检查 malloc 返回值

  • 修改指针前先保存下一个节点

  • 空链表特殊处理

  • 使用完后必须释放

相关推荐
天赐学c语言2 小时前
Linux - 应用层自定义协议与序列/反序列化
linux·服务器·网络·c++
啊哦呃咦唔鱼2 小时前
LeetCode hot100-73 矩阵置零
算法
阿贵---2 小时前
C++构建缓存加速
开发语言·c++·算法
紫丁香2 小时前
pytest_自动化测试3
开发语言·python·功能测试·单元测试·集成测试·pytest
bearpping2 小时前
java进阶知识点
java·开发语言
波特率1152002 小时前
C++当中is-a(继承)与has-a(成员对象)的辨析与使用指南(包含实际工程当中的使用示例)
c++·ros·串口通信
杰杰7982 小时前
Python面向对象——类的魔法方法
开发语言·python
Joker Zxc2 小时前
【前端基础(Javascript部分)】6、用JavaScript的递归函数和for循环,计算斐波那契数列的第 n 项值
开发语言·前端·javascript
kkkkatoq2 小时前
JAVA中的IO操作
java·开发语言