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

在这篇博客中,我们将深入探讨如何用 C 语言实现一个双向链表。我们将逐步实现双向链表的基本操作,包括节点的增、删、查、遍历等,并解释相关的概念与代码实现。与普通链表相比,双向链表的每个节点包含两个指针域:一个指向前一个节点,另一个指向下一个节点,从而使得操作变得更加灵活。

什么是双向链表?

  • 链表是一种常见的数据结构,通常包含多个节点,每个节点包含一个数据元素和一个指向下一个节点的指针。而双向链表的特点是:
    • 双向链表中,每个节点不仅包含指向下一个节点的指针,还包含指向前一个节点的指针。这使得双向链表可以在两个方向上进行遍历。
    • 双向链表非常适用于需要在两个方向遍历的数据结构,例如浏览器的前进/后退按钮、双向队列等。

设计双向链表

为了实现双向链表,我们首先需要定义一个链表节点的结构体(DoubleLinkedList_t)。该结构体包含三个成员:

  • data:存储节点的数据。
  • next:指向下一个节点的指针。
  • prev:指向前一个节点的指针。

接下来,我们将实现几个基本操作:

  1. 链表创建:创建一个空的双向链表。
  2. 节点插入:在链表的头部、尾部或指定位置插入节点。
  3. 节点删除:删除链表中的指定节点(头节点、尾节点或指定值节点)。
  4. 链表遍历:遍历并打印链表中的所有节点。

版本:

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

1. 创建双向链表

首先,我们需要实现一个函数来创建一个空的双向链表,并返回链表的头结点。头结点不存储实际数据,它的 next 指针指向 NULL,表示链表为空。

image-20250506190259402

/**
 * @name       DoubleLinkedList_Create
 * @brief      创建一个空双向链表,并对空链表进行初始化
 * @param      无
 * @return     头结点地址    
 * @date       2025/04/19
 * @version    1.0
 * @note       返回初始化的头结点
 */
DoubleLinkedList_t * DoubleLinkedList_Create(void)
{
    // 给头结点申请一片内存
    DoubleLinkedList_t *head = (DoubleLinkedList_t *)calloc(1, sizeof(DoubleLinkedList_t));
    
    // 错误处理
    if (head == NULL)
    {
        perror("calloc memory for head failed!");
        exit(-1);
    }
    
    // 对头结点进行初始化,注意:头结点没有数据域
    head->next = NULL;
    head->prev = NULL;
    
    // 返回头结点地址
    return head;
}

2. 创建新节点

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

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

3.插入节点

头部插入

头插法是将新的节点插入到链表的头部。我们实现了 DoubleLinkedList_HeadInsert 函数,检查链表是否为空。如果链表为空,则直接将新节点插入;否则,将新节点插入到头部并更新相应的前后指针。

imgs头插1.jpg

/**
 * @name       DoubleLinkedList_HeadInsert
 * @brief      头部插入新节点
 * @param      head 头结点
 * @param      data 数据域
 * @return     插入成功返回 true,插入失败返回 false
 * @date       2025/04/21
 * @version    1.0
 * @note       在链表头部插入新节点
 */
bool DoubleLinkedList_HeadInsert(DoubleLinkedList_t *head, DataType_t data)
{
    // 创建新节点
    DoubleLinkedList_t *NewNode = DoubleLinkedList_NewNode(data);
    if (NewNode == NULL)
    {
        printf("Can not insert new node\n");
        return false;
    }

    // 判断链表是否为空
    if (head->next == NULL)
    {
        // 链表为空时直接插入
        head->next = NewNode;
        NewNode->prev = head;  // 新节点的 prev 指向头结点
        NewNode->next = NULL;  // 新节点的 next 指向 NULL
    }
    else
    {
        // 链表不为空时插入到头部
        NewNode->next = head->next;  // 新节点的 next 指向原首节点
        head->next->prev = NewNode;  // 原首节点的 prev 指向新节点
        head->next = NewNode;        // 头结点的 next 指向新节点
        NewNode->prev = head;        // 新节点的 prev 指向头结点
    }

    return true;
}

尾部插入

尾插法是将新节点插入到链表的尾部。我们通过遍历链表找到尾节点,并将新节点插入尾部。

image-20250506190321097

/**
 * @name       DoubleLinkedList_TailInsert
 * @brief      尾部插入新节点
 * @param      head 头结点
 * @param      data 数据域
 * @return     插入成功返回 true,插入失败返回 false
 * @date       2025/04/21
 * @version    1.0
 * @note       在链表尾部插入新节点
 */
bool DoubleLinkedList_TailInsert(DoubleLinkedList_t *head, DataType_t data)
{
    // 创建新节点
    DoubleLinkedList_t *NewNode = DoubleLinkedList_NewNode(data);
    if (NewNode == NULL)
    {
        printf("Can not insert new node\n");
        return false;
    }

    // 判断链表是否为空
    if (head->next == NULL)
    {
        // 链表为空时直接插入
        head->next = NewNode;
        NewNode->prev = head;  // 新节点的 prev 指向头结点
        NewNode->next = NULL;  // 新节点的 next 指向 NULL
        return true;
    }
    
    // 遍历链表,找到尾结点
    DoubleLinkedList_t *tail = head;
    while(tail->next != NULL)
    {
        tail = tail->next;
    }
    
    // 更新尾结点的指针
    tail->next = NewNode;     // 尾结点的 next 指向新节点
    NewNode->prev = tail;     // 新节点的 prev 指向尾结点
    NewNode->next = NULL;     // 新节点的 next 指向 NULL
    
    return true;
}

指定位置插入

我们还可以根据指定的目标值(destval)来插入新节点。我们遍历链表,找到目标节点后,将新节点插入到目标节点之后。

image-20250506190333867

/**
 * @name       DoubleLinkedList_DestlInsert
 
 
 * @brief      在目标值后插入新节点
 * @param      head 头结点
 * @param      data 数据域
 * @param      destval 目标值
 * @return     插入成功返回 true,插入失败返回 false
 * @date       2025/04/21
 * @version    1.0
 * @note       找到目标值节点后插入新节点
 */
bool DoubleLinkedList_DestlInsert(DoubleLinkedList_t *head, DataType_t data, DataType_t destval)
{
    // 创建新节点
    DoubleLinkedList_t *NewNode = DoubleLinkedList_NewNode(data);
    if (NewNode == NULL)
    {
        printf("Can not insert new node\n");
        return false;
    }

    if (head->next == NULL)
    {
        printf("The list is empty!\n");
        return false;
    }

    // 遍历链表,找到目标值节点
    DoubleLinkedList_t *current = head->next;
    while(current != NULL)
    {
        // 如果找到目标值节点,则直接插入
        if (current->data == destval)
        {
            // 插入新节点
            NewNode->next = current->next;         // 新节点指向当前节点的下一个节点
            if (current->next != NULL)
            {
                current->next->prev = NewNode;    // 当前节点的下一节点的 prev 指向新节点
            }
            current->next = NewNode;               // 当前节点的 next 指向新节点
            NewNode->prev = current;               // 新节点的 prev 指向当前节点
            return true;  // 插入成功
        }

        current = current->next;
    }

    printf("Target value not found!\n");
    return false;
}

4. 删除节点

删除节点时,我们可以根据目标值或位置来删除相应的节点。我们提供头部删除、尾部删除和按值删除三种操作。

头部删除

头部删除操作删除链表的第一个节点。

image-20250506190343259

/**
 * @name      DoubleLinkedList_HeadDel
 * @brief     删除头部节点
 * @param     head 头结点
 * @return     删除成功返回 true,删除失败返回 false
 * @date       2025/04/21
 * @version    1.0
 * @note       删除链表头部的节点
 */
bool DoubleLinkedList_HeadDel(DoubleLinkedList_t *head)
{
    // 空链表检查
    if (head->next == NULL)
    {
        printf("The list is empty!\n");
        return false;
    }

    // 获取第一个有效节点
    DoubleLinkedList_t *delete = head->next;

    // 如果链表中只有一个节点
    if (delete->next == NULL)
    {
        head->next = NULL;  // 更新头结点指向 NULL
    }
    else
    {
        head->next = delete->next;  // 头结点指向第二个节点
        delete->next->prev = head;  // 第二个节点的 prev 指向头结点
    }

    // 释放第一个节点的内存
    free(delete);

    return true;
}

尾部删除

尾部删除操作删除链表的最后一个节点。

image-20250506190351972

/**
 * @name      DoubleLinkedList_TailDel
 * @brief     删除尾部节点
 * @param     head 头结点
 * @return     删除成功返回 true,删除失败返回 false
 * @date       2025/04/21
 * @version    1.0
 * @note       删除链表尾部的节点
 */
bool DoubleLinkedList_TailDel(DoubleLinkedList_t *head)
{
    // 空链表检查
    if (head->next == NULL)
    {
        printf("The list is empty!\n");
        return false;
    }

    // 获取第一个有效节点
    DoubleLinkedList_t *tail = head->next;

    // 如果链表只有一个节点
    if (tail->next == NULL)
    {
        head->next = NULL;
        free(tail);
    }
    else
    {
        // 遍历链表找到尾结点
        while (tail->next != NULL)
        {
            tail = tail->next;
        }

        // 更新尾结点前一个节点的 next 指针
        tail->prev->next = NULL;
        // 释放尾结点
        free(tail);
    }

    return true;
}

指定删除

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

image-20250506190405729

/**
 * @name      DoubleLinkedList_DestlDel
 * @brief     删除目标节点
 * @param     head 头结点
 * @param     destval 目标值
 * @return     删除成功返回 true,删除失败返回 false
 * @date       2025/04/21
 * @version    1.0
 * @note       根据目标值删除链表中的节点
 */
bool DoubleLinkedList_DestlDel(DoubleLinkedList_t *head, DataType_t destval)
{
    // 空链表检查
    if (head->next == NULL)
    {
        printf("The list is empty!\n");
        return false;
    }

    // 遍历链表找到目标值节点
    DoubleLinkedList_t *current = head->next;
    while (current != NULL)
    {
        if (current->data == destval)
        {
            // 如果目标节点是尾节点
            if (current->next == NULL)
            {
                head->next = NULL;  // 更新头结点指向NULL
            }
            else
            {
                current->prev->next = current->next;  // 更新前驱节点的 next
                current->next->prev = current->prev;  // 更新后继节点的 prev
            }

            // 释放目标节点
            free(current);
            return true;
        }
        current = current->next;
    }

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

5. 打印链表

我们可以通过遍历链表并打印每个节点的数据来实现链表的打印功能。

/**
 * @name      DoubleLinkedList_print
 * @brief     遍历并打印双向链表中的所有节点数据
 * @param     head 头结点
 * @return    无返回值
 * @date      2025/04/21
 * @version   1.0
 * @note      用于遍历并显示双向链表中的所有节点数据
 */
void DoubleLinkedList_print(DoubleLinkedList_t *head)
{
    // 空链表检查
    if (head->next == NULL)
    {
        printf("The list is empty!\n");
        return;
    }

    // 遍历链表并打印每个节点的数据
    DoubleLinkedList_t *current = head->next;
    while (current != NULL)
    {
        printf("Node data: %d\n", current->data);  // 打印当前节点的数据
        current = current->next;  // 移动到下一个节点
    }
}

6.测试及结果

int main()
{
    printf("===== 双向链表测试程序 =====\n");

    // 1. 创建空链表
    DoubleLinkedList_t *list = DoubleLinkedList_Create();
    printf("\n[测试1] 创建空链表成功\n");
    DoubleLinkedList_print(list);  // 应显示空链表

    // 2. 头部插入测试
    printf("\n[测试2] 头部插入3个节点(10->20->30)\n");
    DoubleLinkedList_HeadInsert(list, 10);
    DoubleLinkedList_HeadInsert(list, 20);
    DoubleLinkedList_HeadInsert(list, 30);
    DoubleLinkedList_print(list);  // 应显示 30->20->10

    // 3. 尾部插入测试
    printf("\n[测试3] 尾部插入2个节点(40->50)\n");
    DoubleLinkedList_TailInsert(list, 40);
    DoubleLinkedList_TailInsert(list, 50);
    DoubleLinkedList_print(list);  // 应显示 30->20->10->40->50

    // 4. 指定位置插入测试
    printf("\n[测试4] 在20后插入25,在30后插入35\n");
    DoubleLinkedList_DestlInsert(list, 25, 20);
    DoubleLinkedList_DestlInsert(list, 35, 30);
    DoubleLinkedList_print(list);  // 应显示 30->35->20->25->10->40->50

    // 5. 头部删除测试
    printf("\n[测试5] 删除头部节点(30)\n");
    DoubleLinkedList_HeadDel(list);
    DoubleLinkedList_print(list);  // 应显示 35->20->25->10->40->50

    // 6. 尾部删除测试
    printf("\n[测试6] 删除尾部节点(50)\n");
    DoubleLinkedList_TailDel(list);
    DoubleLinkedList_print(list);  // 应显示 35->20->25->10->40

    // 7. 指定值删除测试
    printf("\n[测试7] 删除值为25的节点\n");
    DoubleLinkedList_DestlDel(list, 25);
    DoubleLinkedList_print(list);  // 应显示 35->20->10->40

    // 8. 错误测试
    printf("\n[测试8] 错误情况测试:\n");
    printf("尝试删除不存在的值99:\n");
    DoubleLinkedList_DestlDel(list, 99);  // 应显示未找到
    printf("尝试在空链表删除:\n");
    DoubleLinkedList_t *emptyList = DoubleLinkedList_Create();
    DoubleLinkedList_HeadDel(emptyList);  // 应显示链表为空

    // 9. 清空链表测试
    printf("\n[测试9] 清空链表:\n");
    while (list->next != NULL) {
        DoubleLinkedList_HeadDel(list);
    }
    DoubleLinkedList_print(list);  // 应显示空链表

    free(list);
    free(emptyList);


    return 0;
}

结果

===== 双向链表测试程序 =====

[测试1] 创建空链表成功
The list is empty!

[测试2] 头部插入3个节点(10->20->30)
Node data: 30
Node data: 20
Node data: 10

[测试3] 尾部插入2个节点(40->50)
Node data: 30
Node data: 20
Node data: 10
Node data: 40
Node data: 50

[测试4] 在20后插入25,在30后插入35
Node data: 30
Node data: 35
Node data: 20
Node data: 25
Node data: 10
Node data: 40
Node data: 50

[测试5] 删除头部节点(30)
Node data: 35
Node data: 20
Node data: 25
Node data: 10
Node data: 40
Node data: 50

[测试6] 删除尾部节点(50)
Node data: 35
Node data: 20
Node data: 25
Node data: 10
Node data: 40

[测试7] 删除值为25的节点
Node data: 35
Node data: 20
Node data: 10
Node data: 40

[测试8] 错误情况测试:
尝试删除不存在的值99:
Node with value 99 not found!
尝试在空链表删除:
The list is empty!

[测试9] 清空链表:
The list is empty!

这篇博客详细介绍了如何在 C 语言中实现双向链表的创建、插入、删除、查找、遍历等操作。通过这篇文章,您不仅了解了双向链表的基本操作,还能够实现一个完整的双向链表数据结构并在 C 语言中进行管理和操作。

posted @ 2025-04-22 22:27  九思0404  阅读(102)  评论(0)    收藏  举报