数据结构:双向循环链表( Double Circular Linked List)及其实现

什么是双向循环链表?

双向循环链表是一种更高级的链表结构,它就像一条双向环形跑道,每节车厢(节点)都有两个挂钩(指针),一个指向下一节车厢,另一个指向上一节车厢。双向循环链表中的每个节点都包含三部分:

  1. 数据:存储实际的数据(比如数字、字符串等)。

  2. 前驱指针:指向前一个节点的地址。

  3. 后继指针:指向下一个节点的地址。

双向循环链表的特点是:链表的最后一个节点的后继指针指向头节点,头节点的前驱指针指向最后一个节点,形成一个双向闭环。正因为如此,双向循环链表可以从任意节点开始,向前或向后无限遍历。


双向循环链表的原理

双向循环链表的每个节点都是一个独立的内存块,节点之间通过前驱指针和后继指针连接。双向循环链表的开头有一个头指针(head),指向第一个节点;最后一个节点的后继指针指向头节点,头节点的前驱指针指向最后一个节点,形成一个双向环。

双向循环链表的优点:

  1. 双向遍历:可以从头到尾遍历,也可以从尾到头遍历。

  2. 插入和删除更方便:因为有前驱指针,删除和插入操作更高效。

  3. 环形结构:适合需要循环遍历的场景。

双向循环链表的缺点:

  1. 内存开销更大:每个节点需要额外存储前驱指针。

  2. 实现更复杂:需要同时维护前驱和后继指针。


双向循环链表的基本操作

双向循环链表的基本操作包括:增、删、查、改。下面我们用大白话解释这些操作。

  1. 增加节点

    • 在链表头部插入节点:新节点的后继指针指向原来的头节点,头节点的前驱指针指向新节点,最后一个节点的后继指针指向新节点,新节点的前驱指针指向最后一个节点,然后更新头指针。

    • 在链表中间插入节点:找到插入位置,修改前后节点的指针。

    • 在链表尾部插入节点:找到最后一个节点,让它的后继指针指向新节点,新节点的前驱指针指向它,新节点的后继指针指向头节点,头节点的前驱指针指向新节点。

  2. 删除节点

    • 删除头节点:让最后一个节点的后继指针指向第二个节点,第二个节点的前驱指针指向最后一个节点,然后更新头指针。

    • 删除中间节点:找到要删除的节点,让前一个节点的后继指针指向后一个节点,后一个节点的前驱指针指向前一个节点。

    • 删除尾节点:找到倒数第二个节点,让它的后继指针指向头节点,头节点的前驱指针指向它。

  3. 查找节点

    • 从头节点开始,依次遍历每个节点,直到找到目标节点。

  4. 修改节点

    • 找到目标节点后,直接修改它的数据。


C 语言实现双向循环链表

下面是一个简单的双向循环链表实现代码,包含初始化、插入、删除、查找和打印功能。

#include <stdio.h>
#include <stdlib.h>

// 定义双向循环链表节点结构体
typedef struct Node {
    int data;           // 数据
    struct Node *prev;  // 指向前一个节点的指针
    struct Node *next;  // 指向下一个节点的指针
} Node;

// 初始化链表(创建一个空链表)
Node* InitList() {
    return NULL;  // 头指针初始化为 NULL
}

// 在链表头部插入节点
Node* InsertHead(Node *head, int value) {
    Node *newNode = (Node*)malloc(sizeof(Node));  // 创建新节点
    newNode->data = value;  // 设置新节点的数据

    if (head == NULL) {
        newNode->prev = newNode;  // 如果链表为空,新节点的前驱指针指向自己
        newNode->next = newNode;  // 新节点的后继指针指向自己
        return newNode;          // 新节点就是头节点
    }

    newNode->next = head;         // 新节点的后继指针指向原来的头节点
    newNode->prev = head->prev;   // 新节点的前驱指针指向原来的尾节点
    head->prev->next = newNode;   // 原来的尾节点的后继指针指向新节点
    head->prev = newNode;         // 原来的头节点的前驱指针指向新节点
    return newNode;               // 返回新的头节点
}

// 在链表尾部插入节点
Node* InsertTail(Node *head, int value) {
    return InsertHead(head, value);  // 在双向循环链表中,头部插入和尾部插入是等价的
}

// 删除链表中指定值的节点
Node* DeleteNode(Node *head, int value) {
    if (head == NULL) {
        return NULL;  // 链表为空,直接返回
    }

    Node *current = head;
    do {
        if (current->data == value) {
            if (current->next == current) {
                // 如果链表只有一个节点
                free(current);
                return NULL;
            } else {
                // 如果链表有多个节点
                current->prev->next = current->next;  // 前一个节点的后继指针指向下一个节点
                current->next->prev = current->prev;  // 下一个节点的前驱指针指向前一个节点
                Node *newHead = (current == head) ? current->next : head;  // 如果删除的是头节点,更新头指针
                free(current);
                return newHead;
            }
        }
        current = current->next;
    } while (current != head);

    return head;  // 未找到要删除的节点
}

// 查找链表中指定值的节点
Node* FindNode(Node *head, int value) {
    if (head == NULL) {
        return NULL;  // 链表为空,直接返回
    }

    Node *current = head;
    do {
        if (current->data == value) {
            return current;  // 找到目标节点
        }
        current = current->next;
    } while (current != head);

    return NULL;  // 未找到目标节点
}

// 打印链表
void PrintList(Node *head) {
    if (head == NULL) {
        printf("链表为空\n");
        return;
    }

    Node *current = head;
    printf("链表内容(正向):");
    do {
        printf("%d -> ", current->data);
        current = current->next;
    } while (current != head);
    printf("(回到起点)\n");
}

// 反向打印链表
void PrintListReverse(Node *head) {
    if (head == NULL) {
        printf("链表为空\n");
        return;
    }

    Node *current = head->prev;  // 从尾节点开始
    printf("链表内容(反向):");
    do {
        printf("%d -> ", current->data);
        current = current->prev;
    } while (current != head->prev);
    printf("(回到起点)\n");
}

int main() {
    Node *head = InitList();  // 初始化链表

    // 插入节点
    head = InsertHead(head, 10);  // 在头部插入 10
    head = InsertHead(head, 20);  // 在头部插入 20
    head = InsertTail(head, 30);  // 在尾部插入 30
    PrintList(head);              // 打印链表(正向)
    PrintListReverse(head);       // 打印链表(反向)

    // 删除节点
    head = DeleteNode(head, 20);  // 删除值为 20 的节点
    PrintList(head);              // 打印链表(正向)

    // 查找节点
    Node *target = FindNode(head, 30);
    if (target != NULL) {
        printf("找到节点:%d\n", target->data);
    } else {
        printf("未找到节点\n");
    }

    return 0;
}
双向循环链表的使用场景
  1. 环形数据存储:比如实现一个环形缓冲区。

  2. 双向遍历需求:比如浏览器的前进和后退功能。

  3. 游戏开发:比如实现一个循环地图。


图片介绍

下面是一个双向循环链表的示意图:

复制

头指针 -> [10] <-> [20] <-> [30] -> (回到起点)
  • 每个方框代表一个节点,包含数据、前驱指针和后继指针。

  • 前驱指针指向前一个节点,后继指针指向下一个节点,最后一个节点的后继指针指向头节点,头节点的前驱指针指向最后一个节点。


应用实例:音乐播放器

音乐播放器的播放列表可以用双向循环链表实现:

  • 上一首:通过前驱指针访问上一首歌曲。

  • 下一首:通过后继指针访问下一首歌曲。

  • 循环播放:播放到最后一首歌曲后,自动回到第一首歌曲。


总结

双向循环链表是一种功能强大的数据结构,适合需要双向遍历和循环遍历的场景。虽然它的实现比单链表复杂,但在某些场景下非常高效。希望通过这篇文章,你能轻松掌握双向循环链表!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yann Chase

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值