双向循环链表完整实现与详解

双向循环链表完整实现与详解

本文将详细解析双向循环链表的设计思路、实现原理以及完整代码实现。通过本文,你将掌握双向循环链表的核心操作和实现技巧。

一、双向循环链表概述

双向循环链表是一种特殊的链表结构,具有以下特点:

  1. 双向性:每个节点包含指向前驱和后继的指针
  2. 循环性:尾节点的next指向头节点,头节点的prev指向尾节点
  3. 高效操作:支持O(1)时间复杂度的头插、尾插、头删、尾删操作

数据结构定义

typedef int TypData;

typedef struct DoubleListNode {
    TypData data;                // 数据域
    struct DoubleListNode *prev; // 指向前一个节点的指针
    struct DoubleListNode *next; // 指向下一个节点的指针
} DListNode;

二、核心功能实现

1. 头节点创建

DListNode * CreHeadNode() {
    DListNode *Head = (DListNode *)calloc(1, sizeof(DListNode));
    if (NULL == Head) {
        perror("创建头节点失败");
        exit(EXIT_FAILURE);
    }
    Head->prev = Head;  // 初始化为空链表(指向自身)
    Head->next = Head;  // 初始化为空链表(指向自身)
    return Head;
}

关键点

  • 使用calloc分配内存并初始化为0
  • 头节点的prev和next指针都指向自身
  • 错误处理确保内存分配失败时程序安全退出

2. 新节点创建

DListNode * CreaOneNode(TypData data) {
    DListNode *Node = (DListNode *)calloc(1, sizeof(DListNode));
    if (NULL == Node) {
        perror("创建节点失败");
        exit(EXIT_FAILURE);
    }
    Node->data = data;
    Node->prev = NULL;
    Node->next = NULL;
    return Node;
}

注意

  • 新节点的prev和next指针初始化为NULL
  • 后续插入操作会正确设置这些指针

3. 链表判空

bool IsEmpty(DListNode *List) {
    return (List->next == List); // 判断是否指向自身
}

原理

  • 当头节点的next指向自身时,链表为空
  • 这是循环链表特有的判空方式

4. 头插法

bool InstHeadNode(DListNode *Head, TypData data) {
    // 创建新节点
    DListNode *New = CreaOneNode(data);
    if (NULL == New) return false;
    
    if (IsEmpty(Head)) {
        // 空链表插入
        Head->next = New;
        Head->prev = New;
        New->prev = Head;
        New->next = Head;
        return true;
    }
    
    // 非空链表插入
    DListNode *first = Head->next;
    New->next = first;
    New->prev = Head;
    first->prev = New;
    Head->next = New;
    return true;
}

操作流程

  1. 创建新节点
  2. 处理空链表情况
  3. 非空链表时:
    • 新节点next指向原首节点
    • 新节点prev指向头节点
    • 原首节点prev指向新节点
    • 头节点next指向新节点

时间复杂度:O(1)

5. 尾插法

bool InstTailNode(DListNode *Head, TypData data) {
    DListNode *New = CreaOneNode(data);
    if (NULL == New) return false;
    
    if (IsEmpty(Head)) {
        // 空链表插入
        Head->next = New;
        Head->prev = New;
        New->prev = Head;
        New->next = Head;
        return true;
    }
    
    // 非空链表插入
    DListNode *last = Head->prev;
    last->next = New;
    New->prev = last;
    New->next = Head;
    Head->prev = New;
    return true;
}

操作流程

  1. 创建新节点
  2. 处理空链表情况
  3. 非空链表时:
    • 原尾节点next指向新节点
    • 新节点prev指向原尾节点
    • 新节点next指向头节点
    • 头节点prev指向新节点

时间复杂度:O(1)

6. 指定位置插入

bool InstDataNode(DListNode *Head, TypData NewData, TypData targetData) {
    if (IsEmpty(Head)) {
        return false;
    }

    DListNode *p = Head->next;
    do {
        if (p->data == targetData) {
            DListNode *New = CreaOneNode(NewData);
            if (NULL == New) return false;
            
            // 插入操作
            New->next = p->next;
            New->prev = p;
            p->next->prev = New;
            p->next = New;
            return true;
        }
        p = p->next;
    } while (p != Head);
    
    printf("未找到数据 %d,插入失败\n", targetData);
    return false;
}

特点

  • 在目标节点后插入新节点
  • 需要遍历链表查找目标位置
  • 时间复杂度:O(n)

7. 头节点删除

bool DelHeadNode(DListNode *Head) {
    if (IsEmpty(Head)) {
        printf("链表为空,删除失败\n");
        return false;
    }
    
    DListNode *delNode = Head->next;
    
    if (delNode->next == Head) { // 只有一个节点
        Head->next = Head;
        Head->prev = Head;
    } else {
        Head->next = delNode->next;
        delNode->next->prev = Head;
    }
    
    free(delNode);
    return true;
}

处理逻辑

  • 空链表直接返回
  • 单节点链表:头节点指向自身
  • 多节点链表:更新头节点的next指针和新的首节点的prev指针

8. 尾节点删除

bool DelTailNode(DListNode *Head) {
    if (IsEmpty(Head)) {
        printf("链表为空,删除失败\n");
        return false;
    }
    
    DListNode *delNode = Head->prev;
    
    if (delNode->prev == Head) { // 只有一个节点
        Head->next = Head;
        Head->prev = Head;
    } else {
        Head->prev = delNode->prev;
        delNode->prev->next = Head;
    }
    
    free(delNode);
    return true;
}

处理逻辑

  • 空链表直接返回
  • 单节点链表:头节点指向自身
  • 多节点链表:更新头节点的prev指针和新的尾节点的next指针

9. 指定节点删除

bool DelSpecNode(DListNode *Head, TypData data) {
    if (IsEmpty(Head)) {
        printf("链表为空,删除失败\n");
        return false;
    }
    
    DListNode *curr = Head->next;
    do {
        if (curr->data == data) {
            // 更新相邻节点的指针
            curr->prev->next = curr->next;
            curr->next->prev = curr->prev;
            
            // 更新头节点指针(如果删除的是首节点或尾节点)
            if (curr == Head->next) Head->next = curr->next;
            if (curr == Head->prev) Head->prev = curr->prev;
            
            free(curr);
            return true;
        }
        curr = curr->next;
    } while (curr != Head);
    
    printf("未找到数据 %d,删除失败\n", data);
    return false;
}

关键点

  • 需要遍历查找目标节点
  • 更新相邻节点的指针
  • 特殊处理删除首节点或尾节点的情况

10. 链表遍历打印

void ListPrint(DListNode *Head) {
    if (IsEmpty(Head)) {
        printf("链表为空\n");
        return;
    }
    
    DListNode *p = Head->next;
    printf("链表内容: ");
    do {
        printf("%d", p->data);
        p = p->next;
        if (p != Head) printf(" <-> ");
    } while (p != Head);
    printf("\n");
}

输出示例

链表内容: 10 <-> 20 <-> 30

11. 链表内存释放

void FreeList(DListNode *Head) {
    if (Head == NULL) return;
    
    if (!IsEmpty(Head)) {
        DListNode *curr = Head->next;
        while (curr != Head) {
            DListNode *temp = curr;
            curr = curr->next;
            free(temp);
        }
    }
    
    free(Head);
}

释放流程

  1. 遍历释放所有数据节点
  2. 最后释放头节点
  3. 防止内存泄漏

三、测试用例

int main() {
    // 创建头节点
    DListNode *list = CreHeadNode();
    
    // 测试插入
    InstHeadNode(list, 10); // 头插
    InstTailNode(list, 30); // 尾插
    InstDataNode(list, 20, 10); // 在10后面插入20
    
    printf("插入后: ");
    ListPrint(list); // 输出: 10 <-> 20 <-> 30
    
    // 测试删除
    DelHeadNode(list); // 删除头节点(10)
    printf("删除头节点后: ");
    ListPrint(list); // 输出: 20 <-> 30
    
    DelTailNode(list); // 删除尾节点(30)
    printf("删除尾节点后: ");
    ListPrint(list); // 输出: 20
    
    InstTailNode(list, 40);
    InstTailNode(list, 50);
    printf("添加节点后: ");
    ListPrint(list); // 输出: 20 <-> 40 <-> 50
    
    DelSpecNode(list, 40); // 删除40
    printf("删除40后: ");
    ListPrint(list); // 输出: 20 <-> 50
    
    // 释放链表
    FreeList(list);
    
    return 0;
}

测试结果

插入后: 链表内容: 10 <-> 20 <-> 30
删除头节点后: 链表内容: 20 <-> 30
删除尾节点后: 链表内容: 20
添加节点后: 链表内容: 20 <-> 40 <-> 50
删除40后: 链表内容: 20 <-> 50

四、双向循环链表的优势

  1. 高效的头尾操作:头插、尾插、头删、尾删都是O(1)时间复杂度
  2. 双向遍历:可以从任意节点向前或向后遍历
  3. 循环结构:没有明确的起点和终点,适合循环访问场景
  4. 空间效率:相比数组不需要预先分配固定大小

五、应用场景

  1. 实现高效的双端队列(Deque)
  2. 音乐播放器的播放列表(循环播放)
  3. 浏览器历史记录(前进/后退)
  4. 内存管理中的页面置换算法
  5. 游戏开发中的对象池管理

六、总结

本文详细介绍了双向循环链表的实现原理和核心操作:

  1. 使用头节点简化边界条件处理
  2. 头插法和尾插法的高效实现
  3. 完整的删除操作(头删、尾删、指定删除)
  4. 安全的内存管理
  5. 详尽的测试用例

双向循环链表是计算机科学中重要的基础数据结构,掌握其原理和实现对于深入理解数据结构与算法具有重要意义。

完整代码已在上文提供,可直接复制使用。实际应用中可根据需求扩展功能,如链表反转、节点查找、链表合并等操作。

posted @ 2025-07-29 22:45  Rare_30  阅读(73)  评论(0)    收藏  举报