双向循环带头链表详解

链表类型概述

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

  • 单向 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)

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

相关推荐
野犬寒鸦5 小时前
从零起步学习并发编程 || 第一章:初步认识进程与线程
java·服务器·后端·学习
我爱娃哈哈5 小时前
SpringBoot + Flowable + 自定义节点:可视化工作流引擎,支持请假、报销、审批全场景
java·spring boot·后端
李梨同学丶7 小时前
0201好虫子周刊
后端
思想在飞肢体在追7 小时前
Springboot项目配置Nacos
java·spring boot·后端·nacos
Loo国昌10 小时前
【垂类模型数据工程】第四阶段:高性能 Embedding 实战:从双编码器架构到 InfoNCE 损失函数详解
人工智能·后端·深度学习·自然语言处理·架构·transformer·embedding
ONE_PUNCH_Ge11 小时前
Go 语言泛型
开发语言·后端·golang
良许Linux11 小时前
DSP的选型和应用
后端·stm32·单片机·程序员·嵌入式
不光头强11 小时前
spring boot项目欢迎页设置方式
java·spring boot·后端
怪兽毕设11 小时前
基于SpringBoot的选课调查系统
java·vue.js·spring boot·后端·node.js·选课调查系统
学IT的周星星11 小时前
Spring Boot Web 开发实战:第二天,从零搭个“会卖萌”的小项目
spring boot·后端·tomcat