双向循环带头链表详解

链表类型概述

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

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

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

相关推荐
找不到对象就NEW一个33 分钟前
用wechatapi进行微信二次开发,微信api
后端
charlie11451419133 分钟前
勇闯前后端Week2:后端基础——Flask API速览
笔记·后端·python·学习·flask·教程
有风6339 分钟前
基于顺序表完成通讯录项目
后端
yuuki23323340 分钟前
【C++】初识C++基础
c语言·c++·后端
q***87601 小时前
springboot下使用druid-spring-boot-starter
java·spring boot·后端
程序员西西1 小时前
SpringBoot无感刷新Token实战指南
java·开发语言·前端·后端·计算机·程序员
南雨北斗1 小时前
mysql视图的作用
后端
Pa2sw0rd丶1 小时前
Fastjson 反序列化漏洞深度解析:从原理到实战防护
java·后端·安全
q***64971 小时前
SpringSecurity踢出指定用户
android·前端·后端