双向循环带头链表详解

链表类型概述

链表作为基础数据结构,根据不同的特性组合可以分为多种类型。主要分类维度包括:

  • 单向 vs 双向:节点指针指向单一方向还是可以双向访问
  • 循环 vs 非循环:尾节点是否指向头节点形成闭环
  • 带头节点 vs 不带头节点:是否存在不存储数据的哨兵节点

我们之前学习的单链表属于单向不循环无头节点 链表,而本文将实现功能更完善的双向循环带头节点链表。

1. 链表结构与初始化

节点结构定义

c

arduino 复制代码
typedef int LTDataType;

typedef struct ListNode {
    LTDataType data;
    struct ListNode* next;
    struct ListNode* prev;
} ListNode;

每个节点包含数据域和两个指针域,分别指向前驱和后继节点。

链表初始化

c

ini 复制代码
// 创建新节点
ListNode* CreateNode(LTDataType x) {
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    assert(newNode);
    newNode->data = x;
    newNode->next = newNode;
    newNode->prev = newNode;
    return newNode;
}

// 初始化双向循环链表
ListNode* LTInit() {
    ListNode* pHead = CreateNode(-1);  // 创建哨兵节点
    return pHead;
}

哨兵节点特点

  • 数据域通常存储无效值(如-1)
  • 不参与实际数据存储
  • 永远存在于链表中,避免空链表特殊情况

2. 链表基本操作

打印链表

c

scss 复制代码
void LTPrint(ListNode* pHead) {
    assert(pHead);
    ListNode* pCur = pHead->next;
    
    printf("链表内容: ");
    while (pCur != pHead) {
        printf("%d -> ", pCur->data);
        pCur = pCur->next;
    }
    printf("HEAD\n");
}

实现要点

  • 从哨兵节点的下一个节点开始遍历
  • 遇到哨兵节点时停止,避免无限循环

尾插操作

c

ini 复制代码
void LTPushBack(ListNode* pHead, LTDataType x) {
    assert(pHead);
    ListNode* newNode = CreateNode(x);
    
    // 新节点与链表建立连接
    newNode->prev = pHead->prev;
    newNode->next = pHead;
    
    // 链表与新节点建立连接
    pHead->prev->next = newNode;
    pHead->prev = newNode;
}

操作流程

  1. 创建新节点
  2. 新节点前驱指向原尾节点,后继指向头节点
  3. 原尾节点后继指向新节点
  4. 头节点前驱指向新节点

头插操作

c

ini 复制代码
void LTPushFront(ListNode* pHead, LTDataType x) {
    assert(pHead);
    ListNode* newNode = CreateNode(x);
    
    // 新节点与链表建立连接
    newNode->next = pHead->next;
    newNode->prev = pHead;
    
    // 链表与新节点建立连接
    pHead->next->prev = newNode;
    pHead->next = newNode;
}

3. 链表删除操作

尾删操作

c

ini 复制代码
void LTPopBack(ListNode* pHead) {
    assert(pHead && pHead->next != pHead);  // 确保链表不为空
    
    ListNode* delNode = pHead->prev;
    
    // 重新链接节点
    delNode->prev->next = pHead;
    pHead->prev = delNode->prev;
    
    // 释放内存
    free(delNode);
    delNode = NULL;
}

头删操作

c

ini 复制代码
void LTPopFront(ListNode* pHead) {
    assert(pHead && pHead->next != pHead);  // 确保链表不为空
    
    ListNode* delNode = pHead->next;
    
    // 重新链接节点
    pHead->next = delNode->next;
    delNode->next->prev = pHead;
    
    // 释放内存
    free(delNode);
    delNode = NULL;
}

4. 指定位置操作

查找节点

c

ini 复制代码
ListNode* LTFind(ListNode* pHead, LTDataType x) {
    assert(pHead);
    ListNode* pCur = pHead->next;
    
    while (pCur != pHead) {
        if (pCur->data == x) {
            return pCur;  // 返回找到的节点指针
        }
        pCur = pCur->next;
    }
    return NULL;  // 未找到返回空指针
}

在指定位置后插入

c

ini 复制代码
void LTInsert(ListNode* pos, LTDataType x) {
    assert(pos);
    ListNode* newNode = CreateNode(x);
    
    // 新节点与前后节点建立连接
    newNode->next = pos->next;
    newNode->prev = pos;
    
    // 前后节点与新节点建立连接
    pos->next->prev = newNode;
    pos->next = newNode;
}

删除指定位置节点

c

perl 复制代码
void LTErase(ListNode* pos) {
    assert(pos);
    
    // 前后节点直接建立连接,跳过当前节点
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    
    // 释放内存
    free(pos);
    pos = NULL;
}

5. 完整测试示例

c

scss 复制代码
#include "List.h"

void TestList() {
    // 初始化链表
    ListNode* plist = LTInit();
    
    // 测试尾插
    printf("=== 尾插测试 ===\n");
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPrint(plist);  // 输出: 1 -> 2 -> 3 -> HEAD
    
    // 测试头插
    printf("\n=== 头插测试 ===\n");
    LTPushFront(plist, 0);
    LTPushFront(plist, -1);
    LTPrint(plist);  // 输出: -1 -> 0 -> 1 -> 2 -> 3 -> HEAD
    
    // 测试查找和插入
    printf("\n=== 查找插入测试 ===\n");
    ListNode* pos = LTFind(plist, 1);
    if (pos != NULL) {
        LTInsert(pos, 99);
        LTPrint(plist);  // 在1后面插入99
    }
    
    // 测试删除
    printf("\n=== 删除测试 ===\n");
    LTErase(pos);  // 删除节点1
    LTPrint(plist);
    
    // 测试头删尾删
    printf("\n=== 头尾删除测试 ===\n");
    LTPopFront(plist);
    LTPopBack(plist);
    LTPrint(plist);
}

int main() {
    TestList();
    return 0;
}

双向循环带头链表的优势

  1. 统一的空链表处理:哨兵节点确保链表永不为空,简化代码逻辑
  2. 高效的双向遍历:支持从前向后和从后向前遍历
  3. 循环特性:尾节点直接连接头节点,形成闭环
  4. 操作一致性:所有插入删除操作遵循相同模式,不需要特殊处理边界情况

复杂度分析

操作 时间复杂度 空间复杂度
初始化 O(1) O(1)
插入(头/尾/指定位置) O(1) O(1)
删除(头/尾/指定位置) O(1) O(1)
查找 O(n) O(1)
遍历 O(n) O(1)

这种链表结构在实际应用中非常常见,特别是在需要频繁在两端进行插

相关推荐
雄大1 分钟前
使用 QWebChannel 实现 JS 与 C++ 双向通信(超详细 + 踩坑总结 + Demo)
后端
计算机学姐3 分钟前
基于SpringBoot的汉服租赁系统【颜色尺码套装+个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·信息可视化·推荐算法
回家路上绕了弯3 分钟前
定期归档历史数据实战指南:从方案设计到落地优化
分布式·后端
+VX:Fegn08953 分钟前
计算机毕业设计|基于springboot + vue建筑材料管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
掘金者阿豪5 分钟前
Redis `WRONGTYPE` 错误的原因及解决方法
后端
天天摸鱼的java工程师8 分钟前
线程池深度解析:核心参数 + 拒绝策略 + 动态调整实战
java·后端
小杨同学4915 分钟前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
后端
Cache技术分享17 分钟前
290. Java Stream API - 从文本文件的行创建 Stream
前端·后端
用户9483570165118 分钟前
拒绝 try-catch:如何设计全局通用的异常拦截体系?
后端
golang学习记21 分钟前
Go 1.22 隐藏彩蛋:cmp.Or —— 让“默认值”写起来像呼吸一样自然!
后端