C语言实现双向循环链表:创建、插入与删除操作

在数据结构中,链表是基础而重要的一部分,而双向链表和循环链表则在一些特定场景下提供了更多灵活性和高效性。今天,我们将探讨如何使用 C 语言实现一个双向循环链表,并实现其基本操作:创建链表、插入节点、删除节点、遍历等。

双向循环链表的定义

双向链表每个节点都包含三个部分:

  1. 数据域:存储实际数据。
  2. 前驱指针:指向当前节点的前一个节点。
  3. 后继指针:指向当前节点的下一个节点。

循环特性则意味着链表的尾节点的后继指针指向头节点,头节点的前驱指针指向尾节点。这种结构使得从任意一个节点开始都可以进行完整的遍历。

imgs双项循环链表图2.jpg

/**
 * @file name : DoubleCircularLinkedList.c
 * @brief     : 实现双向循环链表的增删查
 * @author    : qrshxc@163.com
 * @date      : 2025/04/23
 * @version   : 1.0
 * @note      : 
 * CopyRight (c)  2025-2026   qrshxc@163.com   All Right Reseverd
 */

1. 创建双向链表

首先,我们需要定义链表节点的数据结构。每个节点将包含数据域、前驱指针和后继指针。

// 单向链表的数据类型
typedef int DataType_t;

// 构造链表的节点
typedef struct DoubleCirLinkedList
{
    DataType_t   data;  // 节点的数据域
    struct DoubleCirLinkedList *prev; // 直接前驱的指针域
    struct DoubleCirLinkedList *next; // 直接后继的指针域
} DoubleCirLinkedList_t;

我们首先定义一个函数来创建一个空链表并初始化头结点。头结点的 nextprev 都指向自己,体现了链表的循环特性。

/**
 * @name       DoubleCirLinkedList_Create
 * @brief      创建一个空双向链表,并对空链表进行初始化
 * @param      无
 * @return     头结点地址    
 * @date       2025/04/23
 * @version    1.0
 * @note       返回初始化的头结点
 */
DoubleCirLinkedList_t * DoubleCirLinkedList_Create(void)
{
    // 给头结点申请一片内存
   DoubleCirLinkedList_t *head = (DoubleCirLinkedList_t *)calloc(1, sizeof(DoubleCirLinkedList_t));
    
    // 错误处理
    if (head == NULL)
    {
        perror("calloc memory for head failed!");
        exit(-1);
    }
    
    // 对头结点的prev和next进行初始化,指向自己体现“循环”思想
    head->next = head;
    head->prev = head;
    
    // 返回头结点地址
    return head;
}

2. 创建新节点

接下来,我们定义一个函数来创建一个新节点,并初始化其数据和指针域:

/**
 * @name       DoubleCirLinkedList_NewNode
 * @brief      创建新节点,并进行初始化(数据域+指针域)
 * @param      data 数据域
 * @return     NewNode新结点地址    
 * @date       2025/04/23
 * @version    1.0
 * @note       返回新创建的节点
 */
DoubleCirLinkedList_t *DoubleCirLinkedList_NewNode(DataType_t data)
{
    // 给新结点申请一片内存
    DoubleCirLinkedList_t *NewNode = (DoubleCirLinkedList_t *)calloc(1, sizeof(DoubleCirLinkedList_t));
    
    // 错误处理
    if (NewNode == NULL)
    {
        perror("calloc memory for NewNode failed!");
        return NULL;
    }
    
    // 对新结点进行初始化
    NewNode->data = data;
    NewNode->prev = NewNode;
    NewNode->next = NewNode;
    
    // 返回新结点地址
    return NewNode;
}

3.在链表中插入节点

我们提供了两种方式来插入节点:头部插入和尾部插入。我们分别编写这两个插入操作函数。

头部插入

头部插入意味着我们将新节点插入到链表的开头。在双向循环链表中,插入时需要更新链表的前驱和后继指针。

imgs头插2.jpg

/**
 * @name       DoubleCirLinkedList_headInsert
 * @brief      头部插入新节点
 * @param      head 头结点
 * @param      data 数据域
 * @return     true 插入成功
 * @return     false 插入失败
 * @date       2025/04/23
 * @version    1.0
 * @note       
 */
bool DoubleCirLinkedList_headInsert(DoubleCirLinkedList_t *head, DataType_t data)
{
    // 创建新节点
    DoubleCirLinkedList_t *NewNode = DoubleCirLinkedList_NewNode(data);
    if (NewNode == NULL)
    {
        printf("Can not insert NewNode node\n");
        return false;
    }

    // 判断链表是否为空
    if (head->next == head)
    {
        // 链表为空时直接插入
        head->next = NewNode;
    }
    else
    {
        head->next->prev->next = NewNode;  // 尾节点的 next 指向新节点
        NewNode->prev = head->next->prev;  // 新节点的 prev 指向尾节点

        NewNode->next = head->next;        // 新节点的 prev 指向原首结点
        head->next->prev = NewNode;        // 原首结点的prev指向新节点
        head->next = NewNode;        // 头结点的 next 指向新节点
        
    }

    return true;
}

尾部插入

尾部插入的逻辑与头部插入类似,但是我们插入的是链表的末尾。

imgs尾插2.jpg

/**
 * @name       DoubleCirLinkedList_tailInsert
 * @brief      尾部插入新节点
 * @param      head 头结点
 * @param      data 数据域
 * @return     true 插入成功
 * @return     false 插入失败 
 * @date       2025/04/23
 * @version    1.0
 * @note       
 */
bool DoubleCirLinkedList_TailInsert(DoubleCirLinkedList_t *head, DataType_t data)
{
    // 创建新节点
    DoubleCirLinkedList_t *NewNode = DoubleCirLinkedList_NewNode(data);
    if (NewNode == NULL)
    {
        printf("Can not insert NewNode node\n");
        return false;
    }

    // 判断链表是否为空
    if (head->next == head)
    {
        // 链表为空时直接插入
        head->next = NewNode;
    }

    //不为空情况
    else
    {
        NewNode->prev = head->next->prev;     // 新节点的 prev 指向原尾结点
        head->next->prev->next = NewNode;     // 尾结点的 next 指向新节点
        NewNode->next = head->next;           // 新节点的 next 指向 首节点
        head->next->prev = NewNode;           // 首节点的 prev 指向新节点
    }

    return true;
}

指定位置插入

如果我们想在链表中某个特定节点后插入一个新节点,可以使用以下函数:

imgs中插2.jpg

/**
 * @name       DoubleCirLinkedList_destvallInsert
 * @brief      在目标值后插入新节点
 * @param      head 头结点
 * @param      data 数据域
 * @param      destvalval 目标值
 * @return     插入成功返回 true,插入失败返回 false
 * @date       2025/04/23
 * @version    1.0
 * @note       找到目标值节点后插入新节点
 */
bool DoubleCirLinkedList_DestInsert(DoubleCirLinkedList_t *head, DataType_t destval, DataType_t data)
{
    DoubleCirLinkedList_t *current = head->next; // 操作指针 初始为指向首结点, 若为空链表则指向头结点

    // 1.创建新结点并对新结点进行初始化
    DoubleCirLinkedList_t *NewNode = DoubleCirLinkedList_NewNode(data);
    if (NULL == NewNode)
    {
        printf("can not insert NewNode node , Failed to create a node\n");
        return false;
    }

    // 2.判断双向循环链表是否为空,如果为空,则直接插入到头结点之后
    if (head->next == head)
    {

        head->next = NewNode; // 让头结点的next指针指向新结点
        return true;
    }

    // 3.若双向循环链表非空,需要让尾结点的next指针指向新结点,新结点指向首结点
    // 目标结点是首结点, 不再继续查找. 目标结点非首结点, 继续向下查找
    while (current->data != destval)
    {
        current = current->next;                                      // 进入下一个结点
        if ((current->next == head->next) && (current->data != destval)) // 达到末尾 且 末尾不是目标
        {
            printf("The target doesn't exist! \n");
            return false;
        }
    } // 找到目标结点, current此时指向目标

    if (current->next == head->next) // 目标结点是尾结点
    {

        NewNode->next = head->next; // 尾处理: 新结点直接后继链接首结点
        head->next->prev = NewNode; // 头处理: 首结点直接前驱链接新结点
        NewNode->prev = current;    // 内部处理: 新结点直接前驱链接旧尾结点
        current->next = NewNode;    // 内部处理: 旧结点链接新尾结点
    }
    else // 目标结点是中间结点 或首结点
    {
        NewNode->next = current->next;
        NewNode->prev = current;
        current->next->prev = NewNode;
        current->next = NewNode;
    }
    return true;
}

4. 删除节点

双向循环链表支持三种删除操作:删除头部节点、删除尾部节点和删除目标值节点。

头部删除

删除链表中的头部节点并调整相应的指针。

imgs头删2.jpg

/**
 * @name      DoubleCirLinkedList_headDel
 * @brief     删除头部节点
 * @param     head 头结点
 * @return    true 删除成功
 * @return    false 删除失败
 * @date      2025/04/23
 * @version   1.0
 * @note       
 */
bool DoubleCirLinkedList_headDel(DoubleCirLinkedList_t *head)
{
    DoubleCirLinkedList_t *firstNode = head->next; // 备份首节点地址
    // 判断链表是否为空
    if (head->next == head)
    {
        printf("The list is empty!\n");
        return false;
    }
    // 1.如果链表只有一个有效节点
    if (head->next->next == head->next)
    {
        head->next = head; // 自己指向自己
    } else // 有多个有效节点
    {
        firstNode->prev->next = firstNode->next; // 尾结点的next指向第二个节点
        firstNode->next->prev = firstNode->prev; // 第二个节点的prev指向尾结点
        head->next = firstNode->next;             // 头结点指向第二个有效节点
    }
    firstNode->next = NULL;
    firstNode->prev = NULL;
    free(firstNode); // 释放原首节点内存
    return true;
}

尾部删除

删除链表中的尾部节点并调整相应的指针。

imgs尾删2.jpg

/**
 * @name      DoubleCirLinkedList_tailDel
 * @brief     删除尾部节点
 * @param     head 头结点
 * @return    true 删除成功
 * @return    false 删除失败
 * @date      2025/04/23
 * @version   1.0
 * @note       
 */
bool DoubleCirLinkedList_TailDel(DoubleCirLinkedList_t *head)
{
    DoubleCirLinkedList_t *tail = head->next->prev; // 备份尾节点地址
    // 判断链表是否为空
    if (head->next == head)
    {
        printf("The list is empty!\n");
        return false;
    }
    // 1.如果链表只有一个有效节点
    if (head->next->next == head->next)
    {
        head->next = head; // 自己指向自己
    } else // 有多个有效节点
    {
        tail->prev->next = head->next; // 倒数第二个节点的next指向首节点
        head->next->prev = tail->prev; // 首节点的prev指向倒数第二个节点
    } 
    tail->next = NULL;
    tail->prev = NULL;
    free(tail); // 释放尾首节点内存
    return true;
}

指定删除

按值删除操作删除链表中第一个匹配目标值的节点。

imgs中删2.jpg

bool DoubleCirLinkedList_DestDel(DoubleCirLinkedList_t *head, DataType_t destvalval)
{
    // 空链表检查
    if (head->next == head)
    {
        printf("The list is empty!\n");
        return false;
    }

    // 遍历链表找到目标值节点
    DoubleCirLinkedList_t *current = head->next;
    do{
        if (current->data == destvalval) {
            // 情况1:链表只有一个节点
            if (current->next == current) {
                head->next = head;
            } 
            // 情况2:删除首节点
            else if (current == head->next) {
                current->prev->next = current->next;
                current->next->prev = current->prev;
                head->next = current->next; // 更新头结点指向
            }
            // 情况3:删除尾节点或中间节点
            else {
                current->prev->next = current->next;
                current->next->prev = current->prev;
            }

            free(current);
            return true;
        }

        current = current->next; // 进入下一个节点
    }while(current != head->next);

    current->next = NULL;
    current->prev = NULL;
    free(current);

    printf("Node with value %d not found!\n", destvalval);
    return false;
}

5. 打印链表

打印链表的函数可以用于检查链表的当前状态:

/**
 * @name      DoubleCirLinkedList_print
 * @brief     遍历并打印双向链表中的所有节点数据
 * @param     head 头结点
 * @return    无返回值
 * @date      2025/04/23
 * @version   1.0
 * @note      
 */
void DoubleCirLinkedList_print(DoubleCirLinkedList_t *head)
{
    if (head == NULL || head->next == head) {
        printf("List is empty or invalid!\n");
        return;
    }

    DoubleCirLinkedList_t *current = head->next;
    printf("List: [head] -> ");
    
    do {
        printf("%d -> ", current->data);
        current = current->next;
    } while (current != head->next);
    
    printf("[head]\n");
}

6.测试及结果

int main() {
    // 创建空链表
    DoubleCirLinkedList_t *head = DoubleCirLinkedList_Create();
    printf("After creating the list:\n");
    DoubleCirLinkedList_print(head);  // 输出: [head] -> [head] (空链表)

    // 测试头部插入
    DoubleCirLinkedList_headInsert(head, 10);  // 头部插入10
    printf("\nAfter inserting 10 at the head:\n");
    DoubleCirLinkedList_print(head);  // 输出: [head] -> 10 -> [head]

    // 测试尾部插入
    DoubleCirLinkedList_TailInsert(head, 20);  // 尾部插入20
    printf("\nAfter inserting 20 at the tail:\n");
    DoubleCirLinkedList_print(head);  // 输出: [head] -> 10 -> 20 -> [head]

    // 测试在目标值后插入
    DoubleCirLinkedList_DestInsert(head, 10, 15);  // 在目标值10后插入15
    printf("\nAfter inserting 15 after 10:\n");
    DoubleCirLinkedList_print(head);  // 输出: [head] -> 10 -> 15 -> 20 -> [head]

    // 测试删除目标节点
    DoubleCirLinkedList_DestDel(head, 15);  // 删除目标值15
    printf("\nAfter deleting 15:\n");
    DoubleCirLinkedList_print(head);  // 输出: [head] -> 10 -> 20 -> [head]

    // 测试删除头部节点
    DoubleCirLinkedList_headDel(head);  // 删除头部节点
    printf("\nAfter deleting the head node (10):\n");
    DoubleCirLinkedList_print(head);  // 输出: [head] -> 20 -> [head]

    // 测试删除尾部节点
    DoubleCirLinkedList_TailDel(head);  // 删除尾部节点
    printf("\nAfter deleting the tail node (20):\n");
    DoubleCirLinkedList_print(head);  // 输出: List is empty or invalid!

    // 释放内存
    free(head);
    return 0;
}

结果

After creating the list:
List is empty or invalid!

After inserting 10 at the head:
List: [head] -> 10 -> [head]

After inserting 20 at the tail:
List: [head] -> 10 -> 20 -> [head]

After inserting 15 after 10:
List: [head] -> 10 -> 15 -> 20 -> [head]

After deleting 15:
List: [head] -> 10 -> 20 -> [head]

After deleting the head node (10):
List: [head] -> 20 -> [head]

After deleting the tail node (20):
List is empty or invalid!

通过本文的介绍,我们学习了如何使用 C 语言实现双向循环链表,并实现了链表的插入、删除和打印等基本操作。双向循环链表的优势在于它能够灵活地进行插入和删除操作,特别是在需要频繁修改链表内容的情况下。

posted @ 2025-04-24 17:11  九思0404  阅读(79)  评论(0)    收藏  举报