轻松掌握C语言双链表编程:从零到高手,实用示例全解析!

文章目录

    • 一、介绍
    • 二、构建双链表的数据结构
      • <一>节点的定义
      • <二>节点的创建
      • <三>查找
        • <三> 1. 根据位置查找
        • <三> 2. 根据值查找
        • <三> 3. 检查链表中是否存在特定的值
      • <四>插入
        • <四> 1. 插入头部
          • <四> 1.1 不带返回值
          • <四> 1.2 带返回值
        • <四> 2. 插入尾部
          • <四> 2.1 不带返回值
          • <四> 2.2 带返回值
        • <四> 3. 一般插入
          • <四> 3.1 不带返回值
          • <四> 3.2 带返回值
      • <五>删除
        • <五> 1. 删除头部
          • <五> 1.1 不带返回值
          • <五> 1.2 带返回值
        • <五> 2. 删除尾部
          • <五> 2.1 不带返回值
          • <五> 2.2 带返回值
        • <五> 3. 一般删除
          • <五> 3.1 不带返回值
          • <五> 3.2 带返回值
      • <六>修改值
      • <七>遍历
      • <八>翻转
        • <八> 1. 递归实现
        • <八> 2. 迭代实现
      • <>九 清空链表
      • <十>检查是否是合理的双向链表
    • 三、函数接口(注释详细)
    • 四、函数实现(注释详细)
    • 五、主文件(贼拉详细)
    • 六、演示
    • 七、结语

一、介绍

  1. 什么是双链表?

    双链表,也称作“双向链表”或“双重链表”,它由节点构成,每个节点既包含数据(或值),又包含两个指针,一个指向前一个节点,另一个指向后一个节点。

  2. 双链表的应用场景是什么?

    双链表常用于需要频繁插入和删除节点的场景。
    例如文本编辑器中的撤销和恢复功能、图形界面框架、浏览器历史记录、数据库管理系统等。

  3. 双链表的优点是什么?

    • 双向查找更便捷,快速访问前一个或后一个节点。
    • 支持双向遍历使得在某个节点附近进行操作更加方便,如在文本编辑器中移动光标。
  4. 双链表的缺点是什么?

    • 每个节点包含两个指针,内存消耗增加。
    • 操作复杂度相对较高。
  5. 双链表如何改进?

    • 为了降低内存消耗,可以考虑使用单链表,并在每个节点中存储指向前一个节点的指针。这种方法被称为“前向单链表”或“反向单链表”。
    • 对于频繁操作的场景,采用平衡二叉树或哈希表等数据结果,可以提高插入和删除的效率。
  6. 双链表的实现难度如何?

    双链表的基本实现相对较简单,但在处理复杂的操作时,需要谨慎处理指针的连接和断开。在面对涉及多个节点的复杂操作时,复杂的边界情况和内存管理会很让人头疼。

  7. 第三、四、五部分附赠1000行代码(自定义头文件、实现代码,以及调试主文件)

二、构建双链表的数据结构

<一>节点的定义

双链表的核心思想是通过一个节点,既能访问到上一个节点(前驱),也能访问下一个节点(后继)。

为了定义一个双链表节点,通常遵循以下结构:

struct Node 
{
    // 可以根据需求添加多个元素
    elementType element1;
    elementType element2;
    // ...
    elementType elementn;
    
    struct Node *prev; // 指向前一个节点
    struct Node *next; // 指向下一个节点
};

在上述代码中,elementType 可以是任何数据类型,根据节点需要存储的数据而定。你可以自行修改并扩展节点的成员。

在本文中,我们采用如下的节点定义,以便于示例和代码的理解:

typedef struct linkedListNode linkedList;
typedef linkedList Node;
typedef int elementType;

struct linkedListNode
{
    elementType value;  // 存储节点的值
    linkedList *next;   // 指向下一个节点
    linkedList *prev;   // 指向前一个节点
};

这种节点定义旨在清晰地表达了节点的基本结构,包括值以及与前一个和后一个节点的关系。
需要注意的是,代码中使用了 typedef 来重新定义 linkedListNode,这使得代码更易于阅读和理解,因为我们可以使用短一些的名称。

<二>节点的创建

在这个节点创建函数中,我们接收两个参数:值和后继指针(next)。新创建的节点将使用传递的值来填充元素域(value),并使用传递的指针来指定后继指针域(next)。

与我之前在《掌握C语言单链表编程:从零到高手,实用示例全解析!》中实现的节点创建函数不同,这次我们接受了新参数 next。这种设计允许在执行插入、删除等链表操作时更容易隐藏细节。

同时,因为没有接收 prev(前驱指针),这避免了过度封装。如果将节点的前驱指针也封装在函数中,将对这个函数的灵活性产生不利影响。

// 创建一个新节点并返回其指针
Node *createNode(elementType value, Node *next)
{
    Node *node = (Node *)malloc(sizeof(Node));
    if (node == NULL)
    {
        // 处理内存分配失败(例如,打印错误消息)
        printf("错误:在 createNode 中内存分配失败。\n");
        exit(1); // 或返回 NULL 或执行其他错误处理
    }
    node->value = value; // 设置节点的值
    node->next = next;   // 设置节点的下一个节点指针
    node->prev = NULL;   // 设置节点的前一个节点指针为 NULL
    return node;         // 返回新节点的指针
}

<三>查找

<三> 1. 根据位置查找

遍历链表进行查找,同时需要注意边界情况。

// 根据节点位置查找链表中的节点
Node *findNodeByLocation(linkedList *head, int location)
{
    while (--location)
    {
        if (!head)
        {
            return head; // 如果位置超出链表长度,返回NULL
        }
        head = head->next; // 移动到下一个节点
    }
    return head; // 返回找到的节点
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<三> 2. 根据值查找

遍历链表查找匹配的值,找到匹配的节点后返回,找不到就返回 NULL

// 根据节点值查找链表中的节点
Node *findNodeByValue(linkedList *head, elementType value)
{
    Node *node = head;
    while (node)
    {
        if (node->value == value)
        {
            return node; // 返回找到的节点
        }
        node = node->next; // 移动到下一个节点
    }
    return NULL; // 如果未找到匹配的节点,则返回NULL
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<三> 3. 检查链表中是否存在特定的值

调用 findNodeByValue 函数并进行逻辑判断即可。

// 检查链表中是否存在特定的值
bool containsValueInLinkedList(linkedList *head, elementType value)
{
    return findNodeByValue(head, value) != NULL; // 如果找到匹配的节点,返回true,否则返回false
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

<四>插入

双链表的插入操作本质上与单链表的插入差异不大,只是需要多注意前驱指针(在本文中为 prev)。

<四> 1. 插入头部

通常来说,需要接收两个参数:链表头节点(如果不是全局变量)和值。
首先创建一个节点,使这个节点的 next 指针指向当前的头节点(如果存在),然后更新头指针以指向新建的节点即可。

<四> 1.1 不带返回值
// 在链表头部插入节点
void insertNodeAtHead(linkedList **head, elementType value)
{
    Node *newNode = createNode(value, *head); // 创建新节点
    if (*head)
    {
        (*head)->prev = newNode; // 更新原头节点的前一个节点指针
    }
    *head = newNode; // 更新链表的头节点指针
}
  • 时间复杂度: O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<四> 1.2 带返回值
// 在链表头部插入节点,并返回新的头节点
Node *insertNodeAtHead(linkedList **head, elementType value)
{
    Node *newNode = createNode(value, *head); // 创建新节点
    if (*head)
    {
        (*head)->prev = newNode; // 更新原头节点的前一个节点指针
    }
    *head = newNode; // 更新链表的头节点指针
    return *head; // 返回新的头节点
}

  • 时间复杂度: O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<四> 2. 插入尾部

遍历链表直到尾节点,同时创建新节点(newNode)。使当前的尾节点的 next 指针指向新建的节点,而 newNodeprev 指针指向尾节点。需要特别注意处理空链表和仅包含一个节点的链表情况。

<四> 2.1 不带返回值
// 在链表尾部插入节点
void insertNodeAtTail(linkedList **head, elementType value)
{
    Node *newNode = createNode(value, NULL); // 创建新节点
    Node *currentNode = *head;

    if (!currentNode)
    {
        *head = newNode; // 如果链表为空,将新节点设置为头节点
    }
    else
    {
        while (currentNode->next)
        {
            currentNode = currentNode->next; // 移动到下一个节点,直到找到最后一个节点
        }
        currentNode->next = newNode; // 更新最后一个节点的下一个节点指针
        newNode->prev = currentNode; // 更新新节点的前一个节点指针
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<四> 2.2 带返回值
// 在链表尾部插入节点,并返回新的尾节点
Node *insertNodeAtTail(linkedList **head, elementType value)
{
    Node *newNode = createNode(value, NULL); // 创建新节点
    Node *currentNode = *head;

    if (!currentNode)
    {
        *head = newNode; // 如果链表为空,将新节点设置为头节点
    }
    else
    {
        while (currentNode->next)
        {
            currentNode = currentNode->next; // 移动到下一个节点,直到找到最后一个节点
        }
        currentNode->next = newNode; // 更新最后一个节点的下一个节点指针
        newNode->prev = currentNode; // 更新新节点的前一个节点指针
    }
    return newNode; // 返回新的尾节点
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<四> 3. 一般插入

执行插入操作时需要指定位置,因此我们假定链表的头节点位置为0。
这样,我们可以明确定位链表中每个节点的位置。
当要在指定位置(假设为 location)插入节点时,我们必须确保它是合法的,即

1 ≤ location ≤ ( 1 + numberOfNodesIn ( list ) ) 1 \leq \text{location} \leq (1 + \text{numberOfNodesIn}(\text{list})) 1location(1+numberOfNodesIn(list))

如果 l o c a t i o n location location 不合法,需要进行异常处理。
在假定 l o c a t i o n location location 合法的前提下,我们找到位置 ( l o c a t i o n − 1 ) (location-1) (location1) ( l o c a t i o n + 1 ) (location+1) (location+1) 对应的节点(前驱节点 prevNode 和后继节点 nextNode)。
此外,需要创建新节点(newNode)。
接下来,只需合理更新 prevNodenextNodenewNodeprevnext 指针即可。

<四> 3.1 不带返回值
// 在链表的指定位置插入节点
void insertNodeAtLocation(linkedList **head, elementType value, int location)
{
    if (location < 1)
    {
        printf("错误:insertNodeAtLocation(...int location):位置太小\n");
    }
    else if (location == 1)
    {
        insertNodeAtHead(head, value); // 在链表头部插入节点
    }
    else
    {
        Node *prevNode = findNodeByLocation(*head, location - 1);
        if (prevNode)
        {
            Node *newNode = createNode(value, prevNode->next); // 创建新节点
            if (prevNode->next)
            {
                prevNode->next->prev = newNode; // 更新下一个节点的前一个节点指针
            }
            prevNode->next = newNode; // 更新前一个节点的下一个节点指针
            newNode->prev = prevNode; // 更新新节点的前一个节点指针
        }
        else
        {
            printf("错误:insertNodeAtLocation(...int location):位置太大\n");
        }
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<四> 3.2 带返回值
// 在链表的指定位置插入节点,并返回新的头节点
Node *insertNodeAtLocation(linkedList **head, elementType value, int location)
{
    if (location < 1)
    {
        printf("错误:insertNodeAtLocation(...int location):位置太小\n");
        return *head;
    }
    else if (location == 1)
    {
        return insertNodeAtHead(head, value); // 在链表头部插入节点,并返回新的头节点
    }
    else
    {
        Node *prevNode = findNodeByLocation(*head, location - 1);
        if (prevNode)
        {
            Node *newNode = createNode(value, prevNode->next); // 创建新节点
            if (prevNode->next)
            {
                prevNode->next->prev = newNode; // 更新下一个节点的前一个节点指针
            }
            prevNode->next = newNode; // 更新前一个节点的下一个节点指针
            newNode->prev = prevNode; // 更新新节点的前一个节点指针
            return *head; // 返回新的头节点
        }
        else
        {
            printf("错误:insertNodeAtLocation(...int location):位置太大\n");
            return *head;
        }
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

<五>删除

<五> 1. 删除头部

需要考虑两种情况。

  1. 空链表,无需执行删头操作。
  2. 非空链表:
    • 标记当前头节点
    • 更新头节点
    • 如果更新头头节点仍然存在,prev指针指向空。
    • 释放原头节点的内存
<五> 1.1 不带返回值
// 删除链表头部的节点
void deleteNodeAtHead(linkedList **head)
{
    if (*head)
    {
        Node *node = *head;
        *head = (*head)->next; // 更新链表的头指针为下一个节点
        if (*head)
        {
            (*head)->prev = NULL; // 如果新的头节点存在,更新其前一个节点指针为NULL
        }
        free(node); // 释放原头节点的内存
    }
    else
    {
        printf("错误:deleteNodeAtHead(linkedList **head):链表为空,无法执行删除头节点。\n");
    }
}
  • 时间复杂度: O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<五> 1.2 带返回值
// 删除链表头部的节点,并返回新的头节点
Node *deleteNodeAtHead(linkedList **head)
{
    if (*head)
    {
        Node *node = *head;
        *head = (*head)->next; // 更新链表的头指针为下一个节点
        if (*head)
        {
            (*head)->prev = NULL; // 如果新的头节点存在,更新其前一个节点指针为NULL
        }
        free(node); // 释放原头节点的内存
        return *head; // 返回新的头节点
    }
    else
    {
        printf("错误:deleteNodeAtHead(linkedList **head):链表为空,无法执行删除头节点。\n");
        return NULL;
    }
}
  • 时间复杂度: O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<五> 2. 删除尾部
  1. 空链表不执行操作
  2. 非空链表:
    • 标记尾节点(通过遍历链表)
    • 通过prev指针判断是否原链表为单节点
    • 如果单节点,更新链表头指针为NULL
    • 否则,断开尾节点与其前驱节点的联系
    • 释放尾节点内存
<五> 2.1 不带返回值
// 删除链表尾部的节点
void deleteNodeAtTail(linkedList **head)
{
    if (*head)
    {
        Node *tailNode = *head;
        while (tailNode->next)
        {
            tailNode = tailNode->next; // 移动到下一个节点,直到找到最后一个节点
        }
        if (tailNode->prev)
        {
            tailNode->prev->next = NULL; // 更新倒数第二个节点的下一个节点指针为NULL
        }
        else
        {
            *head = NULL; // 如果链表中只有一个节点,更新链表头指针为NULL
        }
        free(tailNode); // 释放尾节点的内存
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<五> 2.2 带返回值
// 删除链表尾部的节点,并返回新的尾节点
Node *deleteNodeAtTail(linkedList **head)
{
    if (*head)
    {
        Node *tailNode = *head;
        while (tailNode->next)
        {
            tailNode = tailNode->next; // 移动到下一个节点,直到找到最后一个节点
        }
        if (tailNode->prev)
        {
            tailNode->prev->next = NULL; // 更新倒数第二个节点的下一个节点指针为NULL
        }
        else
        {
            *head = NULL; // 如果链表中只有一个节点,更新链表头指针为NULL
        }
        free(tailNode); // 释放尾节点的内存
        return *head; // 返回新的尾节点
    }
    else
    {
        printf("错误:deleteNodeAtTail(linkedList **head):链表为空,无法执行删除尾节点。\n");
        return NULL;
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<五> 3. 一般删除

首先确保 l o c a t i o n location location合法即

1 ≤ location ≤ numberOfNodesIn ( list ) 1 \leq \text{location} \leq \text{numberOfNodesIn}(\text{list}) 1locationnumberOfNodesIn(list)

标记 l o c a t i o n location location处的节点(如有),不妨记为currentNode。另记currentNode的前驱节点和后继节点为prevNdoenextNode.

只需断开currentNodeprevNodenextNode的联系,另将prevNodenextNode直接关联即可,并释放currentNode的内存即可。

特殊的,对于
l o c a t i o n = 1 location=1 location=1 l o c a t i o n = n u m b e r O f N o d e s I n ( l i s t ) location=numberOfNodesIn(list) location=numberOfNodesIn(list)
即头节点或尾节点,需要做特殊处理,不妨借鉴前文所提到的deleteNodeAtHeaddeleteNodeAtTail函数。

<五> 3.1 不带返回值
// 从链表的指定位置删除节点
void deleteNodeAtLocation(linkedList **head, int location)
{
    if (location < 1)
    {
        printf("错误:deleteNodeAtLocation(...int location):位置太小\n");
    }
    else if (location == 1)
    {
        deleteNodeAtHead(head); // 在链表头部删除节点
    }
    else
    {
        Node *currentNode = findNodeByLocation(*head, location);
        if (currentNode)
        {
            currentNode->prev->next = currentNode->next; // 更新前一个节点的下一个节点指针
            if (currentNode->next)
            {
                currentNode->next->prev = currentNode->prev; // 更新下一个节点的前一个节点指针
            }
            free(currentNode); // 释放当前节点的内存
        }
        else
        {
            printf("错误:deleteNodeAtLocation(...int location):位置太大。\n");
        }
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<五> 3.2 带返回值
// 从链表的指定位置删除节点,并返回新的头节点
Node *deleteNodeAtLocation(linkedList **head, int location)
{
    if (location < 1)
    {
        printf("错误:deleteNodeAtLocation(...int location):位置太小\n");
        return *head;
    }
    else if (location == 1)
    {
        return deleteNodeAtHead(head); // 在链表头部删除节点,并返回新的头节点
    }
    else
    {
        Node *currentNode = findNodeByLocation(*head, location);
        if (currentNode)
        {
            currentNode->prev->next = currentNode->next; // 更新前一个节点的下一个节点指针
            if (currentNode->next)
            {
                currentNode->next->prev = currentNode->prev; // 更新下一个节点的前一个节点指针
            }
            free(currentNode); // 释放当前节点的内存
            return *head; // 返回新的头节点
        }
        else
        {
            printf("错误:deleteNodeAtLocation(...int location):位置太大。\n");
            return *head;
        }
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

<六>修改值

此处我们希望通过 l o c a t i o n location location来修改指定位置的节点(如有)的 值。
我们仍然要确保 l o c a t i o n location location合法即

1 ≤ location ≤ numberOfNodesIn ( list ) 1 \leq \text{location} \leq \text{numberOfNodesIn}(\text{list}) 1locationnumberOfNodesIn(list)

// 修改链表中某个节点的值
void modifyNodeValue(linkedList **head, int location, elementType newValue)
{
    Node *node = findNodeByLocation(*head, location);
    if (node)
    {
        node->value = newValue; // 修改节点的值
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

<七>遍历

遍历是链表中最基本的操作,在前文中多个函数中都多次出现。

不同的是,根据 DRY 原则,为了提高函数的灵活性,我们使 traverseLinkedList 函数允许接收 void 类型的函数指针。

这种方式提高了函数的通用性和可重用性,允许我们在不更改 traverseLinkedList 函数的情况下,根据需要执行不同的操作。这种设计方式常用于编写更具扩展性的代码。

DRY (Don’t repeat yourself),是敏捷开发的核心设计原则之一。

这个函数指针允许我们传递一个自定义的操作(函数)作为参数给 traverseLinkedList

这样,我们可以定义任何操作,将其应用到链表的每个节点上,而不必修改 traverseLinkedList 函数本身。

相信看到这里,聪明的你会发现前文中出现的插入、查找、删除等操作都可以在主文件中写个函数,应用到这个 traverseLinkedList 函数来实现,似乎违背了 DRY 原则,但是我们不能过度的追求灵活性。

在自定义头文件中实现需求量大的操作如插入、删除等,可以减少程序的 bug 量,让别人或以后的自己的程序更鲁棒!

traverseLinkedList 函数如下:

// 遍历链表并执行操作
void traverseLinkedList(linkedList *head, void (*operation)(Node *))
{
    while (head)
    {
        operation(head);   // 执行传入的操作函数
        head = head->next; // 移动到下一个节点
    }
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

如果你想要遍历链表并打印每个节点的值,创建一个打印函数,将其传递给 traverseLinkedList,它将在遍历链表时对每个节点调用这个函数来执行打印操作。

打印函数见下面:

void print(linkedList *node)
{
    if (node)
    {
        int value = node->value;
        if (!node->next && !node->prev)
        {
            printf("null <- %d -> null", value);
        }
        else if (!node->prev)
        {
            printf("null <- %d <-> ", value);
        }
        else if (node->next && node->prev)
        {
            printf("%d <-> ", value);
        }
        else
        {
            printf("%d -> null\n", value);
        }
    }
}

在主函数主应用

traverseLinkedList(list, print);

运行结果大抵如下:

0.png

<八>翻转

双链表的翻转明显要比单链表的翻转要复杂一些。在递归和迭代两种方法中,需要考虑的细节较多。建议不必急于迅速掌握,而应当深刻理解这些代码的原理。

<八> 1. 递归实现
// 递归方式反转链表
Node *reverseLinkedListRecursively(linkedList *head)
{
    if (!head || !head->next)
    {
        return head; // 如果链表为空或只有一个节点,直接返回
    }
    
    Node *newHead = reverseLinkedListRecursively(head->next); // 递归反转后续链表

    // 更新节点指针
    head->next->prev = head->next->next;
    head->next->next = head;

    // 如果是头节点,更新前一个节点的指针
    if (!head->prev)
    {
        head->prev = head->next;
        head->next = NULL; // 将头节点的下一个节点指针设为NULL
    }

    return newHead; // 返回反转后的链表
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)
<八> 2. 迭代实现
// 迭代方式反转链表
Node *reverseLinkedListIteratively(linkedList *head)
{
    Node *prevNode = NULL;
    Node *currentNode = head;
    Node *nextNode;

    while (currentNode)
    {
        nextNode = currentNode->next; // 保存下一个节点的指针
        currentNode->next = prevNode;  // 反转节点指针

        // 更新前一个节点和当前节点
        prevNode = currentNode;
        currentNode = nextNode;
    }

    return prevNode; // 返回反转后的链表
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

<>九 清空链表

清空链表无论是双链表还是单链表,实现的逻辑大都一般无二。
如果你对递归有浅显的理解,相信不难理解下面的代码。
如果读者感兴趣,不妨试着用迭代实现。

// 清空链表并释放内存
void clearLinkedList(linkedList **head)
{
    if (!(*head))
    {
        return; // 如果链表为空,直接返回
    }
    clearLinkedList(&(*head)->next); // 递归清空下一个节点
    *head = NULL;                    // 更新当前节点为NULL
    free(*head);                     // 释放当前节点的内存
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

<十>检查是否是合理的双向链表

根据双链表的定义实现即可,不难。
下面是迭代实现的一个实例。
如果读者感兴趣,不妨试着再用递归实现下。

// 检查双向链表是否有效
bool isDoublyLinkedListValid(linkedList *head)
{
    if (!head)
    {
        return true; // 空链表被认为是有效的
    }

    linkedList *current = head;
    linkedList *prev = NULL;

    while (current)
    {
        if (current->prev != prev)
        {
            return false; // 前一个节点指针不正确
        }

        prev = current;
        current = current->next;
    }

    // 确保最后一个节点的下一个节点指针是NULL
    return (prev->next == NULL);
}
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

三、函数接口(注释详细)

// D:\A_Shun_Exclusive\life_ofuniversity\code_collection\C\VSCODE\include\DataStructure\my_double_basic_linklist.h
#pragma once

#include 
#include 
#include 

typedef struct linkedListNode linkedList;
typedef linkedList Node;
typedef int elementType;

struct linkedListNode
{
    elementType value;
    linkedList *next;
    linkedList *prev;
};

// 创造节点
Node *createNode(elementType value, Node *next);

// 根据节点值查找链表中的节点
Node *findNodeByValue(linkedList *head, elementType value);

// 根据节点位置查找链表中的节点
Node *findNodeByLocation(linkedList *head, int location);

// 检查链表中是否存在特定的值
bool containsValueInLinkedList(linkedList *head, elementType value);

// 获取链表的长度
int getLengthOfLinkedList(linkedList *head);

// 在链表的指定位置插入节点
void insertNodeAtLocation(linkedList **head, elementType value, int location);

// 在链表头部插入节点
void insertNodeAtHead(linkedList **head, elementType value);

// 在链表尾部插入节点
void insertNodeAtTail(linkedList **head, elementType value);

// 从链表的指定位置删除节点
void deleteNodeAtLocation(linkedList **head, int location);

// 删除链表头部的节点
void deleteNodeAtHead(linkedList **head);

// 删除链表尾部的节点
void deleteNodeAtTail(linkedList **head);

// 修改链表中某个节点的值
void modifyNodeValue(linkedList **head, int location, elementType newValue);

// 遍历链表并执行操作
void traverseLinkedList(linkedList *head, void (*operation)(Node *));

// 反转链表,递归实现
Node *reverseLinkedList(linkedList *head);

// 清空链表并释放内存
void clearLinkedList(linkedList **head);

// 检查是否合理双链表
bool isDoublyLinkedListValid(linkedList *head);


四、函数实现(注释详细)

//D:\A_Shun_Exclusive\life_ofuniversity\code_collection\C\VSCODE\include\DataStructure\my_double_basic_linklist.c

#include "my_double_basic_linklist.h"

// 创建一个新节点并返回其指针
Node *createNode(elementType value, Node *next)
{
    Node *node = (Node *)malloc(sizeof(Node));
    if (node == NULL)
    {
        // 处理内存分配失败(例如,打印错误消息)
        printf("错误:在 createNode 中内存分配失败。\n");
        exit(1); // 或返回 NULL 或执行其他错误处理
    }
    node->value = value; // 设置节点的值
    node->next = next;   // 设置节点的下一个节点指针
    node->prev = NULL;   // 设置节点的前一个节点指针为NULL
    return node;         // 返回新节点的指针
}

// 根据节点值查找链表中的节点
Node *findNodeByValue(linkedList *head, elementType value)
{
    Node *node = head;
    while (node)
    {
        if (node->value == value)
        {
            return node; // 返回找到的节点
        }
        node = node->next; // 移动到下一个节点
    }
    return NULL; // 如果未找到匹配的节点,则返回NULL
}

// 根据节点位置查找链表中的节点
Node *findNodeByLocation(linkedList *head, int location)
{
    while (--location)
    {
        if (!head)
        {
            return head; // 如果位置超出链表长度,返回NULL
        }
        head = head->next; // 移动到下一个节点
    }
    return head; // 返回找到的节点
}

// 检查链表中是否存在特定的值
bool containsValueInLinkedList(linkedList *head, elementType value)
{
    return findNodeByValue(head, value) != NULL; // 如果找到匹配的节点,返回true,否则返回false
}

// 获取链表的长度
int getLengthOfLinkedList(linkedList *head)
{
    int length = 0;
    while (head)
    {
        head = head->next; // 移动到下一个节点
        length++;          // 增加长度计数
    }
    return length; // 返回链表的长度
}

// 在链表的指定位置插入节点
void insertNodeAtLocation(linkedList **head, elementType value, int location)
{
    if (location < 1)
    {
        printf("错误:insertNodeAtLocation(...int location):位置太小\n");
    }
    else if (location == 1)
    {
        insertNodeAtHead(head, value); // 在链表头部插入节点
    }
    else
    {
        Node *prevNode = findNodeByLocation(*head, location - 1);
        if (prevNode)
        {
            Node *newNode = createNode(value, prevNode->next); // 创建新节点
            if (prevNode->next)
            {
                prevNode->next->prev = newNode; // 更新下一个节点的前一个节点指针
            }
            prevNode->next = newNode; // 更新前一个节点的下一个节点指针
            newNode->prev = prevNode; // 更新新节点的前一个节点指针
        }
        else
        {
            printf("错误:insertNodeAtLocation(...int location):位置太大\n");
        }
    }
}

// 在链表头部插入节点
void insertNodeAtHead(linkedList **head, elementType value)
{
    Node *newNode = createNode(value, *head); // 创建新节点
    if (*head)
    {
        (*head)->prev = newNode; // 更新原头节点的前一个节点指针
    }
    *head = newNode; // 更新链表的头节点指针
}

// 在链表尾部插入节点
void insertNodeAtTail(linkedList **head, elementType value)
{
    Node *newNode = createNode(value, NULL); // 创建新节点
    Node *currentNode = *head;

    if (!currentNode)
    {
        *head = newNode; // 如果链表为空,将新节点设置为头节点
    }
    else
    {
        while (currentNode->next)
        {
            currentNode = currentNode->next; // 移动到下一个节点,直到找到最后一个节点
        }
        currentNode->next = newNode; // 更新最后一个节点的下一个节点指针
        newNode->prev = currentNode; // 更新新节点的前一个节点指针
    }
}

// 从链表的指定位置删除节点
void deleteNodeAtLocation(linkedList **head, int location)
{
    if (location < 1)
    {
        printf("错误:deleteNodeAtLocation(...int location):位置太小\n");
    }
    else if (location == 1)
    {
        deleteNodeAtHead(head); // 在链表头部删除节点
    }
    else
    {
        Node *currentNode = findNodeByLocation(*head, location);
        if (currentNode)
        {
            currentNode->prev->next = currentNode->next; // 更新前一个节点的下一个节点指针
            if (currentNode->next)
            {
                currentNode->next->prev = currentNode->prev; // 更新下一个节点的前一个节点指针
            }
            free(currentNode); // 释放当前节点的内存
        }
        else
        {
            printf("错误:deleteNodeAtLocation(...int location):位置太大。\n");
        }
    }
}

// 删除链表头部的节点
void deleteNodeAtHead(linkedList **head)
{
    if (*head)
    {
        Node *node = *head;
        *head = (*head)->next; // 更新链表的头指针为下一个节点
        if (*head)
        {
            (*head)->prev = NULL; // 如果新的头节点存在,更新其前一个节点指针为NULL
        }
        free(node); // 释放原头节点的内存
    }
    else
    {
        printf("错误:deleteNodeAtHead(linkedList **head):链表为空,无法执行删除头节点。\n");
    }
}

// 删除链表尾部的节点
void deleteNodeAtTail(linkedList **head)
{
    if (*head)
    {
        Node *tailNode = *head;
        while (tailNode->next)
        {
            tailNode = tailNode->next; // 移动到下一个节点,直到找到最后一个节点
        }
        if (tailNode->prev)
        {
            tailNode->prev->next = NULL; // 更新倒数第二个节点的下一个节点指针为NULL
        }
        else
        {
            *head = NULL; // 如果链表中只有一个节点,更新链表头指针为NULL
        }
        free(tailNode); // 释放尾节点的内存
    }
}

// 修改链表中某个节点的值
void modifyNodeValue(linkedList **head, int location, elementType newValue)
{
    Node *node = findNodeByLocation(*head, location);
    if (node)
    {
        node->value = newValue; // 修改节点的值
    }
}

// 遍历链表并执行操作
void traverseLinkedList(linkedList *head, void (*operation)(Node *))
{
    while (head)
    {
        operation(head);   // 执行传入的操作函数
        head = head->next; // 移动到下一个节点
    }
}

// 反转链表,递归实现
Node *reverseLinkedList(linkedList *head)
{
    if (!head || !head->next)
    {
        return head; // 如果链表为空或只有一个节点,直接返回
    }
    Node *node = reverseLinkedList(head->next); // 递归反转后续链表

    head->next->prev = head->next->next; // 更新下一个节点的前一个节点指针
    head->next->next = head;             // 更新下一个节点的下一个节点指针
    if (!head->prev)
    {
        head->prev = head->next; // 如果是头节点,更新前一个节点的指针
        head->next = NULL;       // 将头节点的下一个节点指针设为NULL
    }
    return node; // 返回反转后的链表
}

// 清空链表并释放内存
void clearLinkedList(linkedList **head)
{
    if (!(*head))
    {
        return; // 如果链表为空,直接返回
    }
    clearLinkedList(&(*head)->next); // 递归清空下一个节点
    *head = NULL;                    // 更新当前节点为NULL
    free(*head);                     // 释放当前节点的内存
}

// 检查双向链表是否有效
bool isDoublyLinkedListValid(linkedList *head)
{
    if (!head)
    {
        return true; // 空链表被认为是有效的
    }

    linkedList *current = head;
    linkedList *prev = NULL;

    while (current)
    {
        if (current->prev != prev)
        {
            return false; // 前一个节点指针不正确
        }

        prev = current;
        current = current->next;
    }

    // 确保最后一个节点的下一个节点指针是NULL
    return (prev->next == NULL);
}

五、主文件(贼拉详细)

//D :\A_Shun_Exclusive\life_ofuniversity\code_collection\C\VSCODE\My_Code\C\test\Ten\T_10_first.c

#include 
#include "DataStructure/my_double_basic_linklist.h"

// ANSI_COLOR_RED 是一个宏定义,用于设置控制台文本颜色为红色
#define ANSI_COLOR_RED "\x1b[31m"

// ANSI_COLOR_RESET 是一个宏定义,用于重置控制台文本颜色为默认值
#define ANSI_COLOR_RESET "\x1b[0m"

    void
    print(linkedList *node)
{
    if (node)
    {
        int value = node->value;
        if (!node->next && !node->prev)
        {
            printf("null<- %d ->null", value);
        }
        else if (!node->prev)
        {
            printf("null<- %d <-> ", value);
        }
        else if (node->next && node->prev)
        {
            printf("%d <-> ", value);
        }
        else
        {
            printf("%d ->null\n", value);
        }
    }
}

// Function to generate a random integer between min and max
int randomInt(int min, int max)
{
    return min + rand() % (max - min + 1);
}

// Function to print a menu for user selection
void printMenu()
{
    printf("\n╭────────────────────────────╮\n");
    printf("│ Linked List Test Menu     │\n");
    printf("╰────────────────────────────╯\n");
    printf("1. 插入节点至链表头部\n");
    printf("2. 插入节点至链表尾部\n");
    printf("3. 插入节点至指定位置\n");
    printf("4. 删除链表头部的节点\n");
    printf("5. 删除链表尾部的节点\n");
    printf("6. 删除指定位置的节点\n");
    printf("7. 修改节点的值\n");
    printf("8. 根据值查找节点\n");
    printf("9. 根据位置查找节点\n");
    printf("10. 检查值是否存在\n");
    printf("11. 获取链表长度\n");
    printf("12. 反转链表\n");
    printf("13. 清空链表\n");
    printf("14. 查看链表\n");
    printf("15. 随机测试\n");
    printf("0. 退出\n");
    printf("请选择操作: _ \b\b");
}

// 插入节点至链表头部
void insertNodeAtHead_case(linkedList **head)
{
    int N;
    printf("正在执行插头操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("Error:执行操作的次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int value;
        printf("请输入要插入的值: __\b\b");
        scanf("%d", &value);
        insertNodeAtHead(head, value);
        printf("节点%d已插入至链表头部。\n", (*head)->value);
        if (!N)
        {
            printf("插头操作完成.\n链表如下:\n");
        }
        traverseLinkedList(*head, print);
    }
}

void insertNodeAtTail_case(linkedList **head)
{
    int N;
    printf("正在执行插尾操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:插入次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int value;
        printf("请输入要插入的值: ");
        scanf("%d", &value);
        insertNodeAtTail(head, value);
        printf("节点%d已插入至链表尾部。\n", (*head)->value);
        if (!N)
        {
            printf("插尾操作完成.\n链表如下:\n");
        }
        traverseLinkedList(*head, print);
    }
}

void insertNodeAtLocation_case(linkedList **head)
{
    int N;
    printf("正在执行一般插入操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:插入次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int location, value;
        printf("请输入要插入的位置: ");
        scanf("%d", &location);
        printf("请输入要插入的值: ");
        scanf("%d", &value);
        insertNodeAtLocation(head, value, location);
        printf("节点已插入至指定位置。\n");
        if (!N)
        {
            printf("操作完成:\n");
        }
        traverseLinkedList(*head, print);
    }
}

void deleteNodeAtHead_case(linkedList **head)
{
    int N;
    printf("正在执行删除头操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:删除次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        deleteNodeAtHead(head);
        printf("已删除链表头部的节点。\n");
        if (!N)
        {
            printf("删除头操作完成.\n链表如下:\n");
        }
        traverseLinkedList(*head, print);
    }
}

void deleteNodeAtTail_case(linkedList **head)
{
    int N;
    printf("正在执行删除尾操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:删除次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        deleteNodeAtTail(head);
        printf("已删除链表尾部的节点。\n");
        if (!N)
        {
            printf("删除尾操作完成.\n链表如下:\n");
        }
        traverseLinkedList(*head, print);
    }
}

void deleteNodeAtLocation_case(linkedList **head)
{
    int N;
    printf("正在执行删除位置操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:删除次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int location;
        printf("请输入要删除的位置: ");
        scanf("%d", &location);
        deleteNodeAtLocation(head, location);
        printf("已删除指定位置的节点。\n");
        if (!N)
        {
            printf("删除位置操作完成.\n链表如下:\n");
        }
        traverseLinkedList(*head, print);
    }
}

void modifyNodeValue_case(linkedList **head)
{
    int N;
    printf("正在执行修改值操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:修改次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int location, value;
        printf("请输入要修改的位置: ");
        scanf("%d", &location);
        printf("请输入新的值: ");
        scanf("%d", &value);
        modifyNodeValue(head, location, value);
        printf("节点的值已修改。\n");
        if (!N)
        {
            printf("修改值操作完成.\n");
        }
        traverseLinkedList(*head, print);
    }
}

void findNodeByValue_case(linkedList *head)
{
    int N;
    printf("正在执行按值查找操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:查找次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int value;
        printf("请输入要查找的值: ");
        scanf("%d", &value);
        Node *foundNode = findNodeByValue(head, value);
        if (foundNode)
        {
            printf("已找到节点,位置为:%d\n", foundNode->value);
        }
        else
        {
            printf("未找到节点。\n");
        }
    }
}

void findNodeByLocation_case(linkedList *head)
{
    int N;
    printf("正在执行按位置查找操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:查找次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int location;
        printf("请输入要查找的位置: ");
        scanf("%d", &location);
        Node *nodeAtLocation = findNodeByLocation(head, location);
        if (nodeAtLocation)
        {
            printf("位置 %d 的值为:%d\n", location, nodeAtLocation->value);
        }
        else
        {
            printf("未找到位置。\n");
        }
    }
}

void containsValueInLinkedList_case(linkedList *head)
{
    int N;
    printf("正在执行值检查操作...\n");

    do
    {
        printf("执行此操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:操作次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int value;
        printf("请输入要检查的值: ");
        scanf("%d", &value);
        if (containsValueInLinkedList(head, value))
        {
            printf("值存在于链表中。\n");
        }
        else
        {
            printf("值不存在于链表中。\n");
        }
    }
}

// 获取链表长度
void getLengthOfLinkedList_case(linkedList *head)
{
    printf("链表长度: %d\n", getLengthOfLinkedList(head));
}

// 反转链表
void reverseLinkedList_case(linkedList **head)
{
    *head = reverseLinkedList(*head);
    printf("链表已反转。\n");
    printf("此时链表如下:\n");
    traverseLinkedList(*head, print);
}

// 清空链表
void clearLinkedList_case(linkedList **head)
{

    clearLinkedList(head);
    printf("链表已清空。\n");
}

// 随机测试
void randomTest_case(linkedList **head)
{
    int N;

    do
    {
        printf("执行随机操作的次数 (正整数): ");
        scanf("%d", &N);

        if (N <= 0)
        {
            printf("错误:操作次数必须是正整数。\n");
        }
    } while (N <= 0);

    while (N--)
    {
        int operation = randomInt(1, 12); // 生成一个随机操作码

        switch (operation)
        {
        case 1:
        {
            int value = randomInt(1, 100);
            insertNodeAtHead(head, value);
            printf("节点 %d 已插入头部.\n", value);
        }
        break;

        case 2:
        {
            int value = randomInt(1, 100);
            insertNodeAtTail(head, value);
            printf("节点 %d 已插入尾部.\n", value);
        }
        break;

        case 3:
        {
            int value = randomInt(1, 100);
            int location = randomInt(1, getLengthOfLinkedList(*head) + 1);
            insertNodeAtLocation(head, value, location);
            printf("节点 %d 已插入位置 %d.\n", value, location);
        }
        break;

        case 4:
            deleteNodeAtHead(head);
            printf("已删除链表头部的节点.\n");
            break;

        case 5:
            deleteNodeAtTail(head);
            printf("已删除链表尾部的节点.\n");
            break;

        case 6:
        {
            int location = randomInt(1, getLengthOfLinkedList(*head) + 1);
            deleteNodeAtLocation(head, location);
            printf("已删除位置 %d 的节点.\n", location);
        }
        break;

        case 7:
        {
            int location = randomInt(1, getLengthOfLinkedList(*head) + 1);
            int value = randomInt(1, 100);
            modifyNodeValue(head, location, value);
            printf("位置 %d 的节点的值已修改为 %d.\n", location, value);
        }
        break;

        case 8:
        {
            int value = randomInt(1, 100);
            Node *foundNode = findNodeByValue(*head, value);
            if (foundNode)
            {
                printf("已找到节点,位置为:%d\n", foundNode->value);
            }
            else
            {
                printf("未找到节点。\n");
            }
        }
        break;

        case 9:
        {
            int location = randomInt(1, getLengthOfLinkedList(*head) + 1);
            Node *nodeAtLocation = findNodeByLocation(*head, location);
            if (nodeAtLocation)
            {
                printf("位置 %d 的值为:%d\n", location, nodeAtLocation->value);
            }
            else
            {
                printf("未找到位置。\n");
            }
        }
        break;

        case 10:
        {
            int value = randomInt(1, 100);
            if (containsValueInLinkedList(*head, value))
            {
                printf("值 %d 存在于链表中.\n", value);
            }
            else
            {
                printf("值 %d 不存在于链表中.\n", value);
            }
        }
        break;

        case 11:
            printf("链表长度: %d\n", getLengthOfLinkedList(*head));
            break;

        case 12:
            *head = reverseLinkedList(*head);
            printf("链表已反转.\n");
            break;
        }
        if (!isDoublyLinkedListValid(*head))
        {
            printf(ANSI_COLOR_RED "Error: 双向链表结构异常!\n!!\n!!!" ANSI_COLOR_RESET "\n");
            printf("按任意键继续...");
            getchar();
        }
        printf("链表如下:\n");
        traverseLinkedList(*head, print);
    }
}

int main(void)
{
    linkedList *head = NULL;
    srand(time(NULL));

    int choice;

    do
    {
        printMenu();
        scanf("%d", &choice);

        switch (choice)
        {
        case 1:
            insertNodeAtHead_case(&head);
            break;
        case 2:
            insertNodeAtTail_case(&head);
            break;
        case 3:
            insertNodeAtLocation_case(&head);
            break;
        case 4:
            deleteNodeAtHead_case(&head);
            break;
        case 5:
            deleteNodeAtTail_case(&head);
            break;
        case 6:
            deleteNodeAtLocation_case(&head);
            break;
        case 7:
            modifyNodeValue_case(&head);
            break;
        case 8:
            findNodeByValue_case(head);
            break;
        case 9:
            findNodeByLocation_case(head);
            break;
        case 10:
            containsValueInLinkedList_case(head);
            break;
        case 11:
            getLengthOfLinkedList_case(head);
            break;
        case 12:
            reverseLinkedList_case(&head);
            break;
        case 13:
            clearLinkedList_case(&head);
            break;
        case 14:
            traverseLinkedList(head, print);
            break;
        case 15:
            randomTest_case(&head);
            break;
        case 0:
            break;
        default:
            printf("无效的选择,请重试。\n");
        }
        if (!isDoublyLinkedListValid(head))
        {
            printf(ANSI_COLOR_RED "Error: 双向链表结构异常!\n!!\n!!!" ANSI_COLOR_RESET "\n");
        }
        getchar();
        printf("请输入任意键继续...");
        getchar();
    } while (choice != 0);

    return 0;
}

六、演示

轻松掌握C语言双链表编程:从零到高手,实用示例全解析!_第1张图片

轻松掌握C语言双链表编程:从零到高手,实用示例全解析!_第2张图片

轻松掌握C语言双链表编程:从零到高手,实用示例全解析!_第3张图片

轻松掌握C语言双链表编程:从零到高手,实用示例全解析!_第4张图片
轻松掌握C语言双链表编程:从零到高手,实用示例全解析!_第5张图片

七、结语

在探索C语言中的双链表编程之旅中,你已经学到了如何定义、插入、删除、修改和查找双链表中的元素。通过详实的示例和鲁棒的函数实现,你现在具备了编写强大代码的技能。另外,提供的自定义头文件和实现代码将帮助你更轻松地应用这些知识。无论是初学者还是有经验的开发者,都可以从这篇文章中受益匪浅。现在,你可以信心十足地使用双链表来解决各种编程挑战。继续前进,掌握更多强大的编程技巧吧!

你可能感兴趣的:(C语言之旅,初识数据结构,c语言,开发语言,数据结构)