双向链表、双向循环链表之间的异同---嵌入式入门---Linux

📚1、 引言:链表的进化之路

在掌握单向链表之后,我们迎来了链表的进阶形态------双向链表和双向循环链表。这三种数据结构虽然血脉相连,但在实现细节和应用场景上各有千秋。本文将带你深入探索这些数据结构的精髓,通过对比分析,让你不仅知其然,更知其所以然。

核心价值:双向结构带来的不仅是技术上的优化,更是思维方式的转变。从前向后遍历已成常态,从后向前遍历则是新的可能。

2. 双向链表

2.1 结构体节点定义

cs 复制代码
typedef int DataType;

typedef struct ListNode {
    DataType val;           // 节点数据
    struct ListNode *prev;  // 指向前驱节点
    struct ListNode *next;  // 指向后继节点
} ListNode;

设计哲学

  • 对称性:每个节点都有两个方向的连接,这是双向链表的核心特征

  • 双向导航:既可以从头到尾遍历,也可以从尾到头回溯

  • 空间换时间:多一个指针的开销,换来操作的灵活性

应用思考:在需要频繁前后查找的场景中,双向链表的优势尤为明显。比如浏览器的前进后退功能,音乐播放器的上一曲下一曲等。

2.2 哑节点(头节点)创建

cs 复制代码
ListNode *CreateDummyNode(void) {
    ListNode *dummy = (ListNode *)malloc(sizeof(ListNode));
    if (dummy == NULL) {
        perror("malloc dummy failed");
        return NULL;
    }
    
    dummy->val = 0;        // 头节点数据域通常不使用或用于存储长度
    dummy->prev = NULL;    // 前驱指向空
    dummy->next = NULL;    // 后继指向空
    return dummy;
}

设计要点

  • 哨兵节点:头节点不存储实际数据,简化边界处理

  • 清晰边界prevnext都初始化为NULL,明确标识链表的空状态

  • 内存安全:严格的错误检查,防止内存分配失败导致的程序崩溃

编程思想:好的数据结构设计从清晰的初始化开始。一个明确的起点,为后续所有操作奠定基础。

2.3 头插法

cs 复制代码
int InsertHeadNode(ListNode *dummy, DataType val) {
    ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
    if (newNode == NULL) {
        perror("malloc newNode failed");
        return -1;
    }
    newNode->val = val;
    newNode->next = dummy->next;
    newNode->prev = dummy;
    
    // 如果原链表非空,需更新后继节点的前驱指针
    if (dummy->next != NULL) {
        dummy->next->prev = newNode;
    }
    dummy->next = newNode;
    
    return 0;
}

插入过程解析

复制代码
初始状态: dummy ⇄ NULL  (空链表)

插入节点A:dummy ⇄ A ⇄ NULL
         ↑______↑

插入节点B:dummy ⇄ B ⇄ A ⇄ NULL
         ↑______↑   ↑____↑

关键操作解析

  1. 顺序的重要性:先设置新节点的指针,再修改已有节点的指针

  2. 边界判断:只有在原链表非空时才需要更新原头部的前驱指针

  3. 原子性思维:每个指针操作都要考虑其对整体结构的影响

时间复杂度:O(1),无论链表多长,头插法都只需要常数时间

2.4 指定值删除(删除首个匹配节点)

cs 复制代码
int DeleteNode(ListNode *dummy, DataType val) {
    ListNode *cur = dummy->next;
    
    // 查找目标节点
    while (cur != NULL && cur->val != val) {
        cur = cur->next;
    }
    
    if (cur == NULL) {
        printf("未找到值为 %d 的节点\n", val);
        return -1;
    }
    
    // 调整前后节点的指针
    cur->prev->next = cur->next;
    if (cur->next != NULL) {
        cur->next->prev = cur->prev;
    }
    free(cur);
    
    return 0;
}

⚠️ 重要注意事项

在双向链表的插入和删除操作中,必须特别注意边界节点的处理:

当操作涉及链表尾部的节点时,next 指针可能为 NULL

若直接访问 NULL 指针的成员,将导致段错误

因此,在执行 cur->next->prev 或类似操作前,务必检查指针是否有效

3. 双向循环链表

3.1 与双向链表的区别

双向循环链表的尾节点后继指向头节点,头节点的前驱指向尾节点,形成一个闭环。

3.2 哑节点创建

cs 复制代码
ListNode *CreateDummyNode(void) {
    ListNode *dummy = (ListNode *)malloc(sizeof(ListNode));
    if (dummy == NULL) {
        perror("malloc dummy failed");
        return NULL;
    }
    dummy->val = 0;
    dummy->prev = dummy;  // 指向自身
    dummy->next = dummy;  // 指向自身
    
    return dummy;
}

**关键区别:**初始时 prev 和 next 均指向自身,形成自环。

3.3 头插法

cs 复制代码
int InsertHeadNode(ListNode *dummy, DataType val) {
    ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
    if (newNode == NULL) {
        perror("malloc newNode failed");
        return -1;
    }
    
    newNode->val = val;
    newNode->next = dummy->next;
    newNode->prev = dummy;
    
    // 由于是循环链表,dummy->next 永远不会为 NULL
    newNode->next->prev = newNode;
    newNode->prev->next = newNode;
    
    return 0;
}

3.4 尾插法

cs 复制代码
int InsertTailNode(ListNode *dummy, DataType val) {
    ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
    if (newNode == NULL) {
        perror("malloc newNode failed");
        return -1;
    }
    
    newNode->val = val;
    newNode->next = dummy;
    newNode->prev = dummy->prev;
    
    // 对称更新相邻节点的指针
    newNode->next->prev = newNode;
    newNode->prev->next = newNode;
    
    return 0;
}

3.5 遍历与结束条件

循环链表的遍历结束条件不再是判断 NULL,而是判断是否回到头节点:

cs 复制代码
ListNode *cur = dummy->next;
while (cur != dummy) {  // 回到起点时结束
    // 处理当前节点
    printf("%d ", cur->val);
    cur = cur->next;
}

这一原则同样适用于删除、销毁等操作。

实际应用:双向循环链表因其操作的对称性和遍历的便利性,在实现缓存、轮询调度等场景中应用广泛。

4. 数组与链表的对比

特性 数组 链表
存储空间 连续 可以不连续
容量限制 大小固定 动态扩展,理论上无上限
插入/删除效率 较低(需移动元素) 较高(仅修改指针)
访问效率 O(1) 随机访问 O(n) 顺序访问
内存利用率 可能浪费或不足 按需分配,但需额外指针空间

选择建议

  • 需要频繁随机访问 → 数组

  • 需要频繁插入/删除 → 链表

  • 数据规模变化大 → 链表

  • 内存空间紧张 → 数组(指针开销更小)

5. 总结

本文详细介绍了双向链表和双向循环链表的实现原理与关键操作,并对比了数组与链表的优缺点。掌握这些基础数据结构对于后续学习栈、队列等更复杂的抽象数据类型至关重要。

下篇预告 :我们将进入数据结构之栈的学习,探讨其LIFO(后进先出)特性及实际应用。

如有疑问或发现错误,欢迎在评论区留言讨论!

相关推荐
H Journey2 小时前
Linux sudo 命令完全指南
linux·运维·服务器·sudo
开开心心_Every2 小时前
家常菜谱软件推荐:分类齐全无广告步骤详细
linux·运维·服务器·华为od·edge·pdf·华为云
i建模2 小时前
在 Arch Linux 中安装 **Xorg 服务器**
linux·运维·服务器
liyuanchao_blog2 小时前
linuxptp适配记录
linux·云计算
RisunJan2 小时前
Linux命令-logger(将消息写入系统日志)
linux·运维
智驾2 小时前
嵌入式Linux DMA深度解析:原理、应用与性能优化实践
linux·dma
独自破碎E3 小时前
【滑动窗口+计数】LCR015找到字符串中所有字母异位词
数据结构·算法
Trouvaille ~3 小时前
【Linux】线程同步与互斥(一):线程互斥原理与mutex详解
linux·运维·服务器·c++·算法·线程·互斥锁
HalvmånEver3 小时前
Linux:进程 vs 线程:资源共享与独占全解析(线程四)
java·linux·运维