C 语言单链表与双向链表解析

在 C 语言的数据结构学习中,链表是仅次于数组的重要线性结构。它通过指针将离散的内存节点串联起来,克服了数组固定大小、插入删除效率低的缺点。本文将深入讲解单链表与双向链表的原理、实现及应用场景,帮助读者彻底掌握这两种基础数据结构。

一、链表基础概念

链表是由多个节点组成的线性数据结构,每个节点包含两部分:

  • 数据域:存储节点的实际数据(如 int、char、自定义结构体等)
  • 指针域:存储下一个(或上一个)节点的内存地址,实现节点间的关联

与数组相比,链表的核心优势在于:

  1. 内存动态分配,无需预先指定大小
  2. 插入 / 删除操作无需移动大量元素,仅需修改指针指向
  3. 有效利用碎片化内存空间

二、单链表详解

2.1 单链表结构特点

单链表是最简单的链表形式,每个节点仅包含一个指针域,指向下一个节点。链表的操作需通过头指针(指向第一个节点)开始,尾节点的指针域为NULL,表示链表结束。

2.2 单链表节点定义

首先定义单链表节点结构体,包含数据域和指针域:

#include <stdio.h>

#include <stdlib.h>

// 单链表节点结构体

typedef struct Node {

        int data; // 数据域(存储整型数据)

        struct Node *next; // 指针域(指向后续节点)

} Node, *LinkedList; // Node:节点类型;LinkedList:节点指针类型

2.3 单链表核心操作实现

2.3.1 初始化链表

初始化操作创建一个头节点(不存储实际数据,仅用于简化操作),并将头节点的next指针设为NULL:

// 初始化单链表(返回头节点指针)

LinkedList initLinkedList() {

        // 分配头节点内存

        LinkedList head = (LinkedList)malloc(sizeof(Node));

        if (head == NULL) {

                printf("内存分配失败!\n");

                exit(1); // 退出程序

        }

        head->next = NULL; // 初始状态下无后续节点

        return head;

}

2.3.2 尾插法添加节点

在链表末尾插入新节点,适用于顺序创建链表:

// 尾插法添加节点(head:头节点;data:待插入数据)

void insertAtTail(LinkedList head, int data) {

        // 1. 创建新节点

        LinkedList newNode = (LinkedList)malloc(sizeof(Node));

        if (newNode == NULL) {

                printf("内存分配失败!\n");

                exit(1);

        }

        newNode->data = data;

        newNode->next = NULL; // 新节点为尾节点,next设为NULL

        // 2. 找到当前尾节点(从头节点开始遍历)

        LinkedList p = head;

        while (p->next != NULL) {

                p = p->next; // 移动到下一个节点

        }

        // 3. 插入新节点(尾节点的next指向新节点)

        p->next = newNode;

}

2.3.3 遍历链表

从头部开始依次访问每个节点,输出数据:

// 遍历单链表(head:头节点)

void traverseLinkedList(LinkedList head) {

        LinkedList p = head->next; // 跳过头节点,从第一个数据节点开始

        if (p == NULL) {

                printf("链表为空!\n");

                return;

        }

        printf("链表内容:");

        while (p != NULL) {

                printf("%d ", p->data); // 输出当前节点数据

                p = p->next; // 移动到下一个节点

        }

        printf("\n");

}

2.3.4 删除指定节点

根据数据值删除节点,需处理 “删除头节点后第一个节点”“删除中间节点”“删除尾节点” 三种场景:

// 删除指定数据的节点(head:头节点;target:待删除数据)

void deleteNode(LinkedList head, int target) {

        LinkedList p = head; // p指向当前节点的前驱节点

        LinkedList q = head->next; // q指向当前节点

        // 1. 查找待删除节点

        while (q != NULL && q->data != target) {

                p = q; // p跟随q移动

                q = q->next;

        }

        // 2. 处理查找结果

        if (q == NULL) {

                printf("未找到数据为%d的节点!\n", target);

                return;

        }

        // 3. 删除节点(修改前驱节点的指针,跳过待删除节点)

        p->next = q->next;

        free(q); // 释放待删除节点的内存(避免内存泄漏)

        printf("成功删除数据为%d的节点!\n", target);

}

2.4 单链表完整示例

int main() {

        // 1. 初始化链表

        LinkedList head = initLinkedList();

        // 2. 添加节点

        insertAtTail(head, 10);

        insertAtTail(head, 20);

        insertAtTail(head, 30);

        traverseLinkedList(head); // 输出:10 20 30

        // 3. 删除节点

        deleteNode(head, 20);

        traverseLinkedList(head); // 输出:10 30

        // 4. 尝试删除不存在的节点

        deleteNode(head, 40); // 输出:未找到数据为40的节点

        return 0;

}

三、双向链表详解

3.1 双向链表结构特点

双向链表在单链表基础上增加了前驱指针域(指向前一个节点),每个节点包含三个部分:

  • 前驱指针域(prev):指向当前节点的前一个节点
  • 数据域(data):存储实际数据
  • 后继指针域(next):指向当前节点的后一个节点

双向链表的优势在于:

  1. 可双向遍历,访问前驱节点无需从头遍历
  2. 删除节点时无需查找前驱节点(单链表需查找)
  3. 插入操作更灵活(支持前插、后插)

3.2 双向链表节点定义

// 双向链表节点结构体

typedef struct DNode {

        int data; // 数据域

        struct DNode *prev; // 前驱指针域(指向前一个节点)

        struct DNode *next; // 后继指针域(指向后一个节点)

} DNode, *DLinkedList;

3.3 双向链表核心操作实现

3.3.1 初始化双向链表

与单链表类似,创建头节点,头节点的prev和next均设为NULL:

// 初始化双向链表(返回头节点指针)

DLinkedList initDLinkedList() {

        DLinkedList head = (DLinkedList)malloc(sizeof(DNode));

        if (head == NULL) {

                printf("内存分配失败!\n");

                exit(1);

        }

        head->prev = NULL;

        head->next = NULL;

        return head;

}

3.3.2 插入节点(前插法)

在指定节点p的前面插入新节点,步骤如下:

  1. 创建新节点并赋值
  2. 新节点的prev指向p的前驱节点
  3. 若p的前驱节点不为空,更新其next指向新节点
  4. 新节点的next指向p
  5. p的prev指向新节点

// 前插法:在节点p前插入数据为data的节点

void insertBefore(DNode *p, int data) {

        if (p == NULL) {

                printf("目标节点为空,无法插入!\n");

                return;

        }

        // 1. 创建新节点

        DLinkedList newNode = (DLinkedList)malloc(sizeof(DNode));

        if (newNode == NULL) {

                printf("内存分配失败!\n");

                exit(1);

        }

        newNode->data = data;

        // 2. 调整指针(关键步骤)

        newNode->prev = p->prev; // 新节点的前驱 = p的前驱

        if (p->prev != NULL) {

                p->prev->next = newNode; // p的前驱的后继 = 新节点

        }

        newNode->next = p; // 新节点的后继 = p

        p->prev = newNode; // p的前驱 = 新节点

}

3.3.3 双向遍历链表

支持正向遍历(从头部到尾部)和反向遍历(从尾部到头部):

// 正向遍历双向链表

void traverseForward(DLinkedList head) {

        DLinkedList p = head->next;

        if (p == NULL) {

                printf("双向链表为空!\n");

                return;

        }

        printf("正向遍历:");

        while (p != NULL) {

                printf("%d ", p->data);

                p = p->next;

        }

        printf("\n");

}

// 反向遍历双向链表

void traverseBackward(DLinkedList head) {

        DLinkedList p = head;

        // 先找到尾节点

        while (p->next != NULL) {

                p = p->next;

        }

        if (p == head) { // 链表为空(只有头节点)

                printf("双向链表为空!\n");

                return;

        }

        printf("反向遍历:");

        while (p != head) {

                printf("%d ", p->data);

                p = p->prev; // 反向移动

        }

        printf("\n");

}

3.3.4 删除指定节点

由于双向链表可直接访问前驱节点,删除操作无需遍历查找,步骤如下:

  1. 若待删除节点p的前驱不为空,更新其next指向p的后继
  2. 若待删除节点p的后继不为空,更新其prev指向p的前驱
  3. 释放p的内存

// 删除双向链表中的节点p

void deleteDNode(DNode *p) {

        if (p == NULL) {

                printf("待删除节点为空!\n");

                return;

        }

        // 1. 调整前驱节点的指针

        if (p->prev != NULL) {

                p->prev->next = p->next;

        }

        // 2. 调整后继节点的指针

        if (p->next != NULL) {

                p->next->prev = p->prev;

        }

        // 3. 释放节点内存

        free(p);

        printf("节点删除成功!\n");

}

3.4 双向链表完整示例

int main() {

        // 1. 初始化双向链表

        DLinkedList head = initDLinkedList();

        // 2. 尾插法添加节点(借助前插法实现)

        DNode *tail = head; // 尾节点初始为头节点

        insertBefore(tail->next, 10); // 在尾节点后插入(即尾插)

        tail = tail->next; // 更新尾节点

        insertBefore(tail->next, 20);

        tail = tail->next;

        insertBefore(tail->next, 30);

        tail = tail->next;

        // 3. 遍历链表

        traverseForward(head); // 输出:10 20 30

        traverseBackward(head); // 输出:30 20 10

        // 4. 删除中间节点(数据为20的节点)

        DNode *p = head->next->next; // 指向数据为20的节点

        deleteDNode(p);

        traverseForward(head); // 输出:10 30

        return 0;

}

四、单链表与双向链表对比

对比维度

单链表

双向链表

节点结构

1 个指针域(next)

2 个指针域(prev+next)

内存占用

较小(每个节点少一个指针)

较大(每个节点多一个指针)

遍历方向

仅正向(从头到尾)

双向(正向 + 反向)

插入操作效率

需查找前驱节点(O (n))

直接访问前驱(O (1))

删除操作效率

需查找前驱节点(O (n))

直接访问前驱(O (1))

实现复杂度

简单

较复杂(指针调整步骤多)

适用场景

仅需正向遍历、内存有限场景

需双向遍历、频繁插入删除场景

五、常见问题与注意事项

  1. 内存泄漏问题:链表节点通过malloc分配内存,删除节点或销毁链表时必须用free释放,否则会导致内存泄漏。
  2. 空指针访问:操作前需判断指针是否为NULL(如头节点、待删除节点),避免程序崩溃。
  3. 头节点作用:头节点不存储实际数据,仅用于简化操作(如避免插入第一个节点时判断头指针是否为空)。
  4. 循环链表:链表尾节点的next指向头节点(单链表)或头节点的prev指向尾节点(双向链表),形成循环结构,适用于需要循环访问的场景(如约瑟夫环问题)。

六、总结

单链表和双向链表是 C 语言中最基础的链表结构,掌握它们是学习更复杂数据结构(如链表哈希表、二叉树)的基础。选择哪种链表需根据实际需求:

  • 若内存资源有限、仅需正向遍历,优先选择单链表
  • 若需频繁双向遍历、插入删除操作频繁,优先选择双向链表

通过本文的讲解和代码示例,相信读者已掌握两种链表的核心操作。建议结合实际问题(如链表反转、链表排序、链表合并)进行练习,进一步巩固知识点。

【四旋翼无人机】具备螺旋桨倾斜机构的全驱动四旋翼无人机:建模控制研究(Matlab代码、Simulink仿真实现)内容概要:本文围绕具备螺旋桨倾斜机构的全驱动四旋翼无人机展开研究,重点探讨其系统建模控制策略,结合Matlab代码Simulink仿真实现。文章详细分析了无人机的动力学模型,特别是引入螺旋桨倾斜机构后带来的全驱动特性,使其在姿态位置控制上具备更强的机动性自由度。研究涵盖了非线性系统建模、控制器设计(如PID、MPC、非线性控制等)、仿真验证及动态响应分析,旨在提升无人机在复杂环境下的稳定性和控制精度。同时,文中提供的Matlab/Simulink资源便于读者复现实验并进一步优化控制算法。; 适合人群:具备一定控制理论基础和Matlab/Simulink仿真经验的研究生、科研人员及无人机控制系统开发工程师,尤其适合从事飞行器建模先进控制算法研究的专业人员。; 使用场景及目标:①用于全驱动四旋翼无人机的动力学建模仿真平台搭建;②研究先进控制算法(如模型预测控制、非线性控制)在无人机系统中的应用;③支持科研论文复现、课程设计或毕业课题开发,推动无人机高机动控制技术的研究进展。; 阅读建议:建议读者结合文档提供的Matlab代码Simulink模型,逐步实现建模控制算法,重点关注坐标系定义、力矩分配逻辑及控制闭环的设计细节,同时可通过修改参数和添加扰动来验证系统的鲁棒性适应性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值