首页 > 其他分享 >03 线性结构——链表(逻辑与物理结构、分类、时间复杂度分析、相关功能定义与代码实现)

03 线性结构——链表(逻辑与物理结构、分类、时间复杂度分析、相关功能定义与代码实现)

时间:2024-10-15 11:47:55浏览次数:9  
标签:03 结点 复杂度 元素 list 链表 NULL 指针

目录

1 链表是什么

1.1 逻辑结构

1.2 物理结构

1.3 相关概念

2 链表的分类

2.1 单链表

2.2 双链表

2.3 循环单链表

2.4 循环双链表

2.5 静态链表

3 链表的优缺点

3.1 优点

3.2 缺点

3.3 与数组的对比

4 功能定义

5 功能实现

5.1 初始化链表

5.2 返回链表的长度

5.3 在指定位置插入元素

5.4 在末尾插入元素

5.5 删除指定位置的元素并返回被删除的元素

5.6 删除末尾元素

5.7 获取指定位置的元素

5.8 修改指定位置的元素

5.9 遍历链表

5.10 释放链表内存

5.11 完整代码

6 测试题


1 链表是什么

1.1 逻辑结构

        链表是一种线性数据结构,由一系列结点(Node)组成。每个结点包含两个主要部分:

  • 数据域(Data):存储结点的实际数据。
  • 指针域(Next):存储指向下一个结点的指针。

        所有结点通过指针相连,形成一个链式结构。链表中的第一个结点称为头结点(Head Node)。

1.2 物理结构

        与数组不同,链表中的结点可以分散在内存的任意位置。每个结点包含两个主要部分:

  • 数据域(Data):存储结点的实际数据。
  • 指针域(Next):存储指向下一个结点的指针。

        通过指针将各个结点连接起来,形成一个链式结构。这种方式使得链表的结点无需连续存储,提供了更大的灵活性。

1.3 相关概念

  • 结点(Node):链表的基本单位,包含数据域和指针域。
  • 数据域(Data):存储结点的实际数据。
  • 指针域(Next):存储指向下一个结点的地址。
  • 头结点(Head Node):链表的第一个结点,通常用一个特殊的指针(头指针)指向它。
  • 尾结点(Tail Node):链表的最后一个结点,其指针域通常为 None,表示链表的结束。
  • 前驱结点(Predecessor Node):对于给定结点,其前驱结点是链表中位于该结点之前的结点。
  • 后续结点(Successor Node):对于给定结点,其后续结点是链表中位于该结点之后的结点。

        在链表中,n 个结点离散分配,彼此通过指针相连

        确定一个链表只需头指针,通过头指针可以访问和遍历整个链表


2 链表的分类

        链表可以根据其结构和特性分为以下几种类型:

2.1 单链表

  • 每个结点包含一个数据域和一个指针域,指针域指向下一个结点
  • 结点只能向前遍历,不能向后遍历
  • 每个结点:只有一个前驱结点和一个后续结点。
  • 头结点:没有前驱结点,但有一个后续结点(即第二个结点)。
  • 尾结点:没有后续结点,但有一个前驱结点(即倒数第二个结点)。
  • 遍历:从头指针开始,通过每个结点的 next 指针逐个访问结点,直到 next 为 NULL。

2.2 双链表

  • 每个结点包含一个数据域和两个指针域,一个指向前一个结点,另一个指向后一个结点
  • 支持双向遍历,便于在链表中前后移动
  • 每个结点:有一个前驱结点和一个后续结点。
  • 头结点:没有前驱结点,但有一个后续结点(即第二个结点)。
  • 尾结点:没有后续结点,但有一个前驱结点(即倒数第二个结点)。
  • 遍历
    • 正向遍历:从头指针开始,通过每个结点的 next 指针逐个访问结点,直到 next 为 NULL。
    • 反向遍历:从尾结点开始,通过每个结点的 prev 指针逐个访问结点,直到 prev 为 NULL。

2.3 循环单链表

  • 单链表的最后一个结点的指针指向头结点,形成一个环
  • 适用于需要从任意位置开始遍历整个链表的场景。
  • 每个结点:有一个前驱结点和一个后续结点。
  • 头结点:有一个前驱结点(即尾结点)和一个后续结点(即第二个结点)。
  • 尾结点:有一个前驱结点(即倒数第二个结点)和一个后续结点(即头结点)。
  • 遍历:从头指针开始,通过每个结点的 next 指针逐个访问结点,直到再次回到头结点(即 next 指向头结点)。

2.4 循环双链表

  • 双链表的首尾相接,形成一个闭环
  • 支持双向遍历,并且可以从任意位置开始遍历整个链表。
  • 每个结点:有一个前驱结点和一个后续结点。
  • 头结点:有一个前驱结点(即尾结点)和一个后续结点(即第二个结点)。
  • 尾结点:有一个前驱结点(即倒数第二个结点)和一个后续结点(即头结点)。
  • 遍历
    • 正向遍历:从头指针开始,通过每个结点的 next 指针逐个访问结点,直到再次回到头结点(即 next 指向头结点)。
    • 反向遍历:从尾结点开始,通过每个结点的 prev 指针逐个访问结点,直到再次回到头结点(即 prev 指向头结点)。

2.5 静态链表

  • 使用数组来模拟链表的行为,每个数组元素代表一个结点,包含数据和指向下一个结点的索引
  • 适用于内存受限或需要固定大小的链表场景。
  • 每个结点:有一个前驱结点和一个后续结点(通过索引表示)。
  • 头结点:没有前驱结点,但有一个后续结点(即第二个结点)。
  • 尾结点:没有后续结点,但有一个前驱结点(即倒数第二个结点)。
  • 遍历:从头指针开始,通过每个结点的 next 索引逐个访问结点,直到 next 为 -1(表示链表结束)。

3 链表的优缺点

3.1 优点

1. 插入和删除操作效率高

        在链表中插入和删除结点的时间复杂度为 O(1),前提是已经找到了要操作的结点位置。这比数组的插入和删除操作(时间复杂度为 O(n))更高效。

插入操作:

        已知位置:假设已经找到了要插入位置的前一个结点(记为 prev_node),只需要执行以下步骤:

  1. 创建新结点 new_node。
  2. 将 new_node 的 next 指针指向 prev_node 的下一个结点。
  3. 将 prev_node 的 next 指针指向 new_node。
  4. 注意:2 和 3 的顺序不能颠倒。

        这些操作都是常数时间内完成的,因此时间复杂度为 O(1)。

删除操作:

        已知位置:假设已经找到了要删除的结点(记为 node_to_delete),只需要执行以下步骤:

  1. 找到 node_to_delete 的前一个结点(记为 prev_node)。
  2. 将 prev_node 的 next 指针指向 node_to_delete 的下一个结点。

        这些操作也是常数时间内完成的,因此时间复杂度为 O(1)。

2. 动态扩展性能好

        链表不需要预先指定固定的大小,可以随时动态增长或缩小。这种灵活性使得链表非常适合处理不确定大小的数据集合,避免了数组需要重新分配内存和复制数据的问题。

3.2 缺点

1. 查找效率低

        由于链表中的结点不是连续存储的,无法像数组那样通过索引直接访问结点。查找特定结点时,必须从头结点开始逐个遍历,直到找到目标结点。这导致链表的随机访问效率较低,时间复杂度为 O(n)。

  • 最坏情况:如果目标结点是链表的最后一个结点,或者目标结点不存在,需要遍历整个链表。在这种情况下,时间复杂度为 O(n),其中 n 是链表的长度。
  • 平均情况:即使在平均情况下,也需要遍历链表的一半结点,时间复杂度仍然为 O(n)。
  • 最好情况:如果目标结点恰好是头结点,时间复杂度为 O(1)。但这只是特殊情况,不具有普遍性。

2. 额外的存储空间

        每个结点除了存储实际数据外,还需要存储指向下一个结点的指针。这会占用额外的存储空间,使得链表在存储相同数量的数据元素时,需要比数组更多的内存。

3.3 与数组的对比

特性链表数组
数据结构非连续存储,通过指针连接连续存储,通过索引访问
初始化需要手动创建头结点和结点定义时指定大小,自动分配内存
存储方式分散在内存中,每个结点包含数据和指针连续存储在内存中,每个元素紧邻
插入操作在已知位置插入结点时间复杂度为 O(1)在任意位置插入元素时间复杂度为 O(n)(需要移动元素)
删除操作在已知位置删除结点时间复杂度为 O(1)在任意位置删除元素时间复杂度为 O(n)(需要移动元素)
查找操作查找特定元素时间复杂度为 O(n)(需要遍历)查找特定元素时间复杂度为 O(1)(通过索引直接访问)
动态扩展可以随时动态增长或缩小,不需要重新分配内存需要预先指定大小,动态扩展需要重新分配内存和复制数据
内存使用每个结点需要额外的指针空间,总体内存使用较高每个元素只占用数据所需的空间,总体内存使用较低
访问方式通过指针逐个访问结点通过索引直接访问元素
适用场景适合频繁插入和删除操作,不确定大小的数据集合适合频繁查找操作,固定大小或预先知道大小的数据集合
实现复杂度实现较为复杂,需要管理指针和结点实现较为简单,直接使用数组即可
空间复杂度O(n)(每个结点需要额外的指针空间)O(n)(每个元素只占用数据所需的空间)
时间复杂度插入/删除:O(1)(已知位置),查找:O(n)插入/删除:O(n),查找:O(1)

4 功能定义

功能函数签名描述
初始化链表void initLinkedList(LinkedList *list)初始化链表,设置头指针为 NULL
返回链表的长度size_t getLength(const LinkedList *list)返回链表的长度。
在指定位置插入元素void insertAt(LinkedList *list, size_t index, int element)在指定位置插入元素(假设是 int 类型)。
在末尾插入元素void insertEnd(LinkedList *list, int element)在链表末尾插入元素(假设是 int 类型)。
删除指定位置的元素并返回被删除的元素int deleteAt(LinkedList *list, size_t index)删除指定位置的元素并返回被删除的元素(假设是 int 类型)。
删除末尾元素int deleteEnd(LinkedList *list)删除链表末尾的元素并返回被删除的元素(假设是 int 类型)。
获取指定位置的元素int getElementAt(const LinkedList *list, size_t index)获取指定位置的元素(假设是 int 类型)。
修改指定位置的元素void modifyAt(LinkedList *list, size_t index, int newValue)修改指定位置的元素(假设是 int 类型)。
遍历链表void printLinkedList(const LinkedList *list)遍历链表(打印内容)。
释放链表内存void destroyLinkedList(LinkedList *list)释放链表内存。
  • 上述功能是针对元素类型为 int 的链表定义的。在实际编程中,可以根据具体需求对元素类型进行调整。

5 功能实现

5.1 初始化链表

        明确: 在包含虚拟头结点的情况下,首个保存数据的结点的索引为 0!

#include <stdio.h>

// 定义存储数据的结构体
// 存储数据:每个 Node 结构体实例代表链表中的一个结点,包含实际存储的数据(data)。
// 链接结点:每个 Node 结构体包含一个指针 next,用于指向下一个结点,从而形成链表的链接结构。
typedef struct Node
{
    int data;          // 结点存储的数据,以 int 类型数据为例
    struct Node *next; // 指向下一个结点的指针
} Node;

// 定义虚拟头结点的结构体
// 管理链表:LinkedList 结构体用于管理整个链表,包含链表的基本信息。
// 头指针:head 指针指向链表的第一个结点,作为链表的入口点。
// 结点计数:size 字段记录链表中结点的数量,便于快速获取链表长度。
typedef struct
{
    size_t size; // 链表中的结点个数(单链表中存储数据的个数)
    Node *head;  // 链表头结点指针,指向保存数据的首元素。由于上面使用了 typedef 起了别名,所以 Node 前面 可以不加 struct
} LinkedList;

// 明确: 在包含虚拟头结点的情况下,首个保存数据的结点的索引为 0!

/**
 * @brief 初始化链表
 *
 * 设置链表的初始状态,将头指针设置为空,结点个数设置为 0。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要初始化的链表
 */
void initLinkedList(LinkedList *list)
{
    // 可选的对传入的参数做合法性校验
    if (list == NULL)
    {
        puts("Error:无效输入,列表指针为空。");
        return;
    }

    list->size = 0;
    list->head = NULL;
    puts("Success:初始化链表成功^_^");
}

int main()
{
    LinkedList list; // 声明链表
    puts("----------------------1.初始化链表----------------------");
    initLinkedList(&list); // 初始化链表
    initLinkedList(NULL);  // 错误测试

    return 0;
}

        输出结果如下所示:

为什么需要两个结构体?

1. 分离职责

  • Node 结构体专注于表示单个结点,包含数据和指向下一个结点的指针。
  • LinkedList 结构体专注于管理整个链表,包括头指针和结点计数。

2. 提高可读性和可维护性

  • 通过分离结点和链表的管理,代码更加清晰和易于理解。
  • 维护和扩展链表相关功能时,可以集中在一个结构体中进行,减少代码冗余。

3. 增强功能

  • LinkedList 结构体中的 size 字段提供了一种快速获取链表长度的方法,而不需要每次遍历链表。
  • 通过 head 指针,可以方便地进行链表的遍历、插入、删除等操作。

5.2 返回链表的长度

/**
 * @brief 获取链表的长度
 *
 * 返回链表中结点的数量。如果传入的链表指针为 NULL,则返回 0 并打印错误消息。
 * 同时,如果成功获取链表长度,还会打印链表的长度信息。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要获取长度的链表
 * @return 返回链表的长度,如果传入的指针为 NULL,则返回 0
 */
size_t getLength(const LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return 0; // 如果是空指针,则返回 0
    }
    return list->size;
}

5.3 在指定位置插入元素

/**
 * @brief 在链表指定位置插入一个新元素。
 *
 * 此函数接收一个指向 LinkedList 类型的指针、一个索引值和一个要插入的元素。
 * 它会在链表的指定位置插入一个新的结点,其中包含提供的元素。
 * 如果提供的索引值非法或链表指针为空,函数将输出错误信息并返回。
 * 如果一切正常,函数将成功插入新结点,并增加链表的大小。
 *
 * @param list 指向 LinkedList 类型的指针,表示要操作的链表。
 * @param index 要插入的新结点的位置(从 0 开始计数)。
 * @param element 要插入的新元素。
 */
void insertAt(LinkedList *list, size_t index, int element)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }
    // 检查传入的位置是否合法
    if (index < 0 || index > list->size)
    {
        printf("Error: 输入的位置:%zu 非法\n", index);
        return; // 如果位置非法,则退出函数
    }

    // 创建新的结点
    Node *newNode = (Node *)malloc(1 * sizeof(Node));
    if (newNode == NULL) // 检查内存分配是否成功
    {
        puts("Error: 内存分配失败");
        return;
    }
    newNode->data = element;

    // 插入新结点
    if (index == 0)
    {
        // 插入在第一个位置(虚拟头结点指向的下一个元素的地址,索引为 0)
        // 注意:这里的顺序不要搞错了
        newNode->next = list->head; // 1. 将原来虚拟头结点指向的下一个元素(即第一个存储数据的元素)的地址给到新结点中的 next
        list->head = newNode;       // 2. 再将新结点的地址给头虚拟头结点的 head
    }
    else
    {
        // 插入中间或尾部
        // 需要找到要插入位置的前一个结点
        // 1. 从第一个存储数据的元素结点(索引为 0)开始寻找
        Node *prevNode = list->head;
        // 注意循环次数
        for (size_t i = 1; i < index; i++)
        {
            prevNode = prevNode->next;
        }
        // 2. 将前面一个结点原本指向的位置给到新结点中的 next
        newNode->next = prevNode->next;
        // 3. 再将新结点的地址给到前面一个结点的 next
        prevNode->next = newNode;
    }
    list->size++; // 增加链表的大小
    printf("Success: 成功在指定位置(%zu)插入元素(%d)\n", index, element);
}

5.4 在末尾插入元素

/**
 * @brief 在链表末尾插入一个新元素。
 *
 * 此函数接收一个指向 LinkedList 类型的指针和一个要插入的元素。
 * 它会在链表的末尾插入一个新的结点,其中包含提供的元素。
 * 如果提供的链表指针为空,函数将输出错误信息并返回。
 * 如果一切正常,函数将调用 insertA` 函数在链表末尾插入新结点,并增加链表的大小。
 *
 * @param list 指向 LinkedList 类型的指针,表示要操作的链表。
 * @param element 要插入的新元素。
 */
void insertEnd(LinkedList *list, int element)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }
    insertAt(list, list->size, element); // 在链表末尾插入元素
}

5.5 删除指定位置的元素并返回被删除的元素

/**
 * @brief 删除指定位置的元素并返回被删除的元素
 *
 * 该函数删除链表中指定位置的元素,并返回被删除的元素。如果传入的链表指针为 NULL 或者指定位置非法,
 * 则返回错误码 -1008611。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @param index 要删除的元素的索引位置(从 0 开始计数)
 * @return 返回被删除的元素,如果操作失败则返回 -1008611
 */
int deleteAt(LinkedList *list, size_t index)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return -1008611; // 如果是空指针,则退出函数
    }

    // 检查 index 是否合法
    if (index >= list->size)
    {
        printf("Error: 输入的位置:%zu 非法\n", index);
        return -1008611; // 退出函数
    }

    int deletedElement; // 保存要删除的结点中存储的数据

    // 删除第一个存储数据的元素结点
    if (index == 0)
    {
        // 1. 定义临时结点将要删除的结点保存起来备用
        Node *temp = list->head;
        // 2. 再将要删除的结点的下一个结点地址给到虚拟头指针
        list->head = temp->next;
        // 3. 取到要删除的结点中所保存的数据
        deletedElement = temp->data;
        // 4. 释放被删除节点的内存
        free(temp);

        printf("Success: 成功删除指定位置(%zu)的结点\n", index);
    }
    else
    {
        // 删除中间或尾部
        // 需要找到要删除位置的前一个结点
        // 1. 从第一个存储数据的元素结点(索引为 0)开始寻找
        Node *prevNode = list->head;

        for (size_t i = 1; i < index; i++)
        {
            prevNode = prevNode->next;
        }
        // 2. 定义临时结点将要删除的结点保存起来备用
        Node *temp = prevNode->next;
        // 3. 再将要删除的结点的下一个结点地址给到前一个结点的 next
        prevNode->next = temp->next;
        // 4. 取到要删除的结点中所保存的数据
        deletedElement = temp->data;
        // 5. 释放被删除节点的内存
        free(temp);

        printf("Success: 成功删除指定位置(%zu)的结点\n", index);
    }

    list->size--; // 更新节点个数
    return deletedElement;
}

5.6 删除末尾元素

/**
 * @brief 删除链表末尾的元素并返回被删除的元素
 *
 * 该函数删除链表末尾的元素,并返回被删除的元素。如果传入的链表指针为 NULL 或者链表为空,
 * 则返回错误码 -1008611。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @return 返回被删除的元素,如果操作失败则返回 -1008611
 */
int deleteEnd(LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return -1008611; // 如果是空指针,则退出函数
    }
    return deleteAt(list, list->size - 1); // 删除链表末尾的元素
}

5.7 获取指定位置的元素

/**
 * @brief 获取链表中指定位置的元素
 *
 * 该函数返回链表中指定位置的元素。如果传入的链表指针为 NULL 或者指定位置非法,
 * 则返回错误码 -1008611。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @param index 要获取的元素的索引位置(从 0 开始计数)
 * @return 返回指定位置的元素,如果操作失败则返回 -1008611
 */
int getElementAt(const LinkedList *list, size_t index)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return -1008611; // 如果是空指针,则退出函数
    }

    // 检查 index 是否合法
    if (index >= list->size)
    {
        return -1008611; // 返回无效的索引
    }

    // 从第一个元素结点开始找
    Node *currentNode = list->head;

    // 循环找到指定位置的节点
    // 注意这里的循环次数,不是找到前面一个结点,而是当前节点
    // 所以相对于前面的插入、删除操作中的循环,这里会多循环一次
    for (size_t i = 0; i < index; i++)
    {
        currentNode = currentNode->next;
    }

    return currentNode->data; // 返回指定位置的元素
}

5.8 修改指定位置的元素

/**
 * @brief 修改链表中指定位置的元素
 *
 * 该函数修改链表中指定位置的元素。如果传入的链表指针为 NULL 或者指定位置非法,
 * 则忽略此次修改操作。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @param index 要修改的元素的索引位置(从 0 开始计数)
 * @param newValue 新的元素值
 */
void modifyAt(LinkedList *list, size_t index, int newValue)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }

    // 检查 index 是否合法
    if (index >= list->size)
    {
        return; // 忽略无效的修改位置
    }

    // 从第一个元素结点开始找
    Node *currentNode = list->head;

    // 循环找到指定位置的节点
    // 注意这里的循环次数,不是找到前面一个结点,而是当前节点
    // 所以相对于前面的插入、删除操作中的循环,这里会多循环一次
    for (size_t i = 0; i < index; i++)
    {
        currentNode = currentNode->next;
    }

    // 修改节点的值
    currentNode->data = newValue;

    printf("Success:成功修改链表中指定位置(%zu)的元素,新元素为:%d\n", index, newValue);
}

5.9 遍历链表

/**
 * @brief 遍历链表并打印每个节点的内容
 *
 * 该函数遍历链表中的每个节点,并打印每个节点的数据。如果传入的链表指针为 NULL,
 * 则打印错误消息并退出函数。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要遍历的链表
 */
void printLinkedList(const LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }

    // 从第一个元素结点开始
    Node *currentNode = list->head;

    puts("开始遍历链表数据:");

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

5.10 释放链表内存

/**
 * @brief 释放链表所占用的内存
 *
 * 该函数会遍历整个链表,逐个释放每个节点的内存,最后将链表的头指针置为 NULL,
 * 并将链表的大小重置为 0。
 *
 * @param list 指向 LinkedList 结构体的指针,表示需要销毁的链表
 */
void destroyLinkedList(LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }

    // 从链表的第一个节点(虚拟头结点指向的下一个元素)开始
    Node *currentNode = list->head;

    // 循环遍历链表中的所有节点
    while (currentNode != NULL)
    {
        // 保存当前节点的指针
        Node *temp = currentNode;

        // 移动到下一个节点
        currentNode = currentNode->next;

        // 释放当前节点的内存
        free(temp);
    }

    // 将链表的头指针置为 NULL,表示链表已被清空
    list->head = NULL;
    // 将链表的大小重置为 0
    list->size = 0;

    // 可选:输出销毁成功的消息
    puts("Success: 链表已成功销毁");
}

5.11 完整代码

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

// 定义存储数据的结构体
// 存储数据:每个 Node 结构体实例代表链表中的一个结点,包含实际存储的数据(data)。
// 链接结点:每个 Node 结构体包含一个指针 next,用于指向下一个结点,从而形成链表的链接结构。
typedef struct Node
{
    int data;          // 结点存储的数据,以 int 类型数据为例
    struct Node *next; // 指向下一个结点的指针
} Node;

// 定义虚拟头结点的结构体
// 管理链表:LinkedList 结构体用于管理整个链表,包含链表的基本信息。
// 头指针:head 指针指向链表的第一个结点,作为链表的入口点。
// 结点计数:size 字段记录链表中结点的数量,便于快速获取链表长度。
typedef struct
{
    size_t size; // 链表中的结点个数(单链表中存储数据的个数)
    Node *head;  // 链表头结点指针,指向保存数据的首元素。由于上面使用了 typedef 起了别名,所以 Node 前面 可以不加 struct
} LinkedList;

// 明确: 在包含虚拟头结点的情况下,首个保存数据的结点的索引为 0!

/**
 * @brief 初始化链表
 *
 * 设置链表的初始状态,将头指针设置为空,结点个数设置为 0。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要初始化的链表
 */
void initLinkedList(LinkedList *list)
{
    // 可选的对传入的参数做合法性校验
    if (list == NULL)
    {
        puts("Error:无效输入,列表指针为空。");
        return;
    }

    list->size = 0;
    list->head = NULL;
    puts("Success:初始化链表成功^_^");
}

/**
 * @brief 获取链表的长度
 *
 * 返回链表中结点的数量。如果传入的链表指针为 NULL,则返回 0 并打印错误消息。
 * 同时,如果成功获取链表长度,还会打印链表的长度信息。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要获取长度的链表
 * @return 返回链表的长度,如果传入的指针为 NULL,则返回 0
 */
size_t getLength(const LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return 0; // 如果是空指针,则返回 0
    }
    return list->size;
}

/**
 * @brief 在链表指定位置插入一个新元素。
 *
 * 此函数接收一个指向 LinkedList 类型的指针、一个索引值和一个要插入的元素。
 * 它会在链表的指定位置插入一个新的结点,其中包含提供的元素。
 * 如果提供的索引值非法或链表指针为空,函数将输出错误信息并返回。
 * 如果一切正常,函数将成功插入新结点,并增加链表的大小。
 *
 * @param list 指向 LinkedList 类型的指针,表示要操作的链表。
 * @param index 要插入的新结点的位置(从 0 开始计数)。
 * @param element 要插入的新元素。
 */
void insertAt(LinkedList *list, size_t index, int element)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }
    // 检查传入的位置是否合法
    if (index < 0 || index > list->size)
    {
        printf("Error: 输入的位置:%zu 非法\n", index);
        return; // 如果位置非法,则退出函数
    }

    // 创建新的结点
    Node *newNode = (Node *)malloc(1 * sizeof(Node));
    if (newNode == NULL) // 检查内存分配是否成功
    {
        puts("Error: 内存分配失败");
        return;
    }
    newNode->data = element;

    // 插入新结点
    if (index == 0)
    {
        // 插入在第一个位置(虚拟头结点指向的下一个元素的地址,索引为 0)
        // 注意:这里的顺序不要搞错了
        newNode->next = list->head; // 1. 将原来虚拟头结点指向的下一个元素(即第一个存储数据的元素)的地址给到新结点中的 next
        list->head = newNode;       // 2. 再将新结点的地址给头虚拟头结点的 head
    }
    else
    {
        // 插入中间或尾部
        // 需要找到要插入位置的前一个结点
        // 1. 从第一个存储数据的元素结点(索引为 0)开始寻找
        Node *prevNode = list->head;
        // 注意循环次数
        for (size_t i = 1; i < index; i++)
        {
            prevNode = prevNode->next;
        }
        // 2. 将前面一个结点原本指向的位置给到新结点中的 next
        newNode->next = prevNode->next;
        // 3. 再将新结点的地址给到前面一个结点的 next
        prevNode->next = newNode;
    }
    list->size++; // 增加链表的大小
    printf("Success: 成功在指定位置(%zu)插入元素(%d)\n", index, element);
}

/**
 * @brief 在链表末尾插入一个新元素。
 *
 * 此函数接收一个指向 LinkedList 类型的指针和一个要插入的元素。
 * 它会在链表的末尾插入一个新结点,其中包含提供的元素。
 * 如果提供的链表指针为空,函数将输出错误信息并返回。
 * 如果一切正常,函数将调用 insertA` 函数在链表末尾插入新结点,并增加链表的大小。
 *
 * @param list 指向 LinkedList 类型的指针,表示要操作的链表。
 * @param element 要插入的新元素。
 */
void insertEnd(LinkedList *list, int element)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }
    insertAt(list, list->size, element); // 在链表末尾插入元素
}

/**
 * @brief 删除指定位置的元素并返回被删除的元素
 *
 * 该函数删除链表中指定位置的元素,并返回被删除的元素。如果传入的链表指针为 NULL 或者指定位置非法,
 * 则返回错误码 -1008611。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @param index 要删除的元素的索引位置(从 0 开始计数)
 * @return 返回被删除的元素,如果操作失败则返回 -1008611
 */
int deleteAt(LinkedList *list, size_t index)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return -1008611; // 如果是空指针,则退出函数
    }

    // 检查 index 是否合法
    if (index >= list->size)
    {
        printf("Error: 输入的位置:%zu 非法\n", index);
        return -1008611; // 退出函数
    }

    int deletedElement; // 保存要删除的结点中存储的数据

    // 删除第一个存储数据的元素结点
    if (index == 0)
    {
        // 1. 定义临时结点将要删除的结点保存起来备用
        Node *temp = list->head;
        // 2. 再将要删除的结点的下一个结点地址给到虚拟头指针
        list->head = temp->next;
        // 3. 取到要删除的结点中所保存的数据
        deletedElement = temp->data;
        // 4. 释放被删除节点的内存
        free(temp);

        printf("Success: 成功删除指定位置(%zu)的结点\n", index);
    }
    else
    {
        // 删除中间或尾部
        // 需要找到要删除位置的前一个结点
        // 1. 从第一个存储数据的元素结点(索引为 0)开始寻找
        Node *prevNode = list->head;

        for (size_t i = 1; i < index; i++)
        {
            prevNode = prevNode->next;
        }
        // 2. 定义临时结点将要删除的结点保存起来备用
        Node *temp = prevNode->next;
        // 3. 再将要删除的结点的下一个结点地址给到前一个结点的 next
        prevNode->next = temp->next;
        // 4. 取到要删除的结点中所保存的数据
        deletedElement = temp->data;
        // 5. 释放被删除节点的内存
        free(temp);

        printf("Success: 成功删除指定位置(%zu)的结点\n", index);
    }

    list->size--; // 更新节点个数
    return deletedElement;
}

/**
 * @brief 删除链表末尾的元素并返回被删除的元素
 *
 * 该函数删除链表末尾的元素,并返回被删除的元素。如果传入的链表指针为 NULL 或者链表为空,
 * 则返回错误码 -1008611。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @return 返回被删除的元素,如果操作失败则返回 -1008611
 */
int deleteEnd(LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return -1008611; // 如果是空指针,则退出函数
    }
    return deleteAt(list, list->size - 1); // 删除链表末尾的元素
}

/**
 * @brief 获取链表中指定位置的元素
 *
 * 该函数返回链表中指定位置的元素。如果传入的链表指针为 NULL 或者指定位置非法,
 * 则返回错误码 -1008611。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @param index 要获取的元素的索引位置(从 0 开始计数)
 * @return 返回指定位置的元素,如果操作失败则返回 -1008611
 */
int getElementAt(const LinkedList *list, size_t index)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return -1008611; // 如果是空指针,则退出函数
    }

    // 检查 index 是否合法
    if (index >= list->size)
    {
        return -1008611; // 返回无效的索引
    }

    // 从第一个元素结点开始找
    Node *currentNode = list->head;

    // 循环找到指定位置的节点
    // 注意这里的循环次数,不是找到前面一个结点,而是当前节点
    // 所以相对于前面的插入、删除操作中的循环,这里会多循环一次
    for (size_t i = 0; i < index; i++)
    {
        currentNode = currentNode->next;
    }

    return currentNode->data; // 返回指定位置的元素
}

/**
 * @brief 修改链表中指定位置的元素
 *
 * 该函数修改链表中指定位置的元素。如果传入的链表指针为 NULL 或者指定位置非法,
 * 则忽略此次修改操作。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要操作的链表
 * @param index 要修改的元素的索引位置(从 0 开始计数)
 * @param newValue 新的元素值
 */
void modifyAt(LinkedList *list, size_t index, int newValue)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }

    // 检查 index 是否合法
    if (index >= list->size)
    {
        return; // 忽略无效的修改位置
    }

    // 从第一个元素结点开始找
    Node *currentNode = list->head;

    // 循环找到指定位置的节点
    // 注意这里的循环次数,不是找到前面一个结点,而是当前节点
    // 所以相对于前面的插入、删除操作中的循环,这里会多循环一次
    for (size_t i = 0; i < index; i++)
    {
        currentNode = currentNode->next;
    }

    // 修改节点的值
    currentNode->data = newValue;

    printf("Success:成功修改链表中指定位置(%zu)的元素,新元素为:%d\n", index, newValue);
}

/**
 * @brief 遍历链表并打印每个节点的内容
 *
 * 该函数遍历链表中的每个节点,并打印每个节点的数据。如果传入的链表指针为 NULL,
 * 则打印错误消息并退出函数。
 *
 * @param list 指向 LinkedList 结构体的指针,表示要遍历的链表
 */
void printLinkedList(const LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }

    // 从第一个元素结点开始
    Node *currentNode = list->head;

    puts("开始遍历链表数据:");

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

/**
 * @brief 释放链表所占用的内存
 *
 * 该函数会遍历整个链表,逐个释放每个节点的内存,最后将链表的头指针置为 NULL,
 * 并将链表的大小重置为 0。
 *
 * @param list 指向 LinkedList 结构体的指针,表示需要销毁的链表
 */
void destroyLinkedList(LinkedList *list)
{
    // 检查传入的指针是否为空
    if (list == NULL)
    {
        puts("Error: 传入的链表指针为 NULL");
        return; // 如果是空指针,则退出函数
    }

    // 从链表的第一个节点(虚拟头结点指向的下一个元素)开始
    Node *currentNode = list->head;

    // 循环遍历链表中的所有节点
    while (currentNode != NULL)
    {
        // 保存当前节点的指针
        Node *temp = currentNode;

        // 移动到下一个节点
        currentNode = currentNode->next;

        // 释放当前节点的内存
        free(temp);
    }

    // 将链表的头指针置为 NULL,表示链表已被清空
    list->head = NULL;
    // 将链表的大小重置为 0
    list->size = 0;

    // 可选:输出销毁成功的消息
    puts("Success: 链表已成功销毁");
}

int main()
{
    LinkedList list; // 声明链表
    puts("----------------------1.初始化链表----------------------");
    initLinkedList(&list); // 初始化链表
    initLinkedList(NULL);  // 错误测试

    puts("----------------------2.获取链表的长度----------------------");
    size_t lenth = getLength(&list);
    printf("链表长度为:%zu\n", lenth);           // 一开始没有元素,长度为 0
    printf("链表长度为:%zu\n", getLength(NULL)); // 为 NULL 时,长度为 0

    puts("----------------------3.在指定位置插入元素----------------------");
    insertAt(&list, 0, 1); // 在位置 0 插入整形数据 1
    insertAt(&list, 1, 2); // 在位置 1 插入整形数据 2
    insertAt(&list, 0, 9); // 在位置 0 插入整形数据 9
    printf("链表长度为:%zu\n", getLength(&list));
    insertAt(&list, 998, 999); // 错误测试,在位置 998 插入整形数据 999
    insertAt(NULL, 1, 2);      // 错误测试
    printf("链表长度为:%zu\n", getLength(&list));
    printLinkedList(&list);

    puts("----------------------4.在末尾插入元素----------------------");
    insertEnd(&list, 66);
    insertEnd(&list, 99);
    printf("链表长度为:%zu\n", getLength(&list));
    printLinkedList(&list);

    puts("----------------------5.删除指定位置的元素并返回被删除的元素----------------------");
    deleteAt(NULL, 1);     // 错误测试
    deleteAt(&list, 9999); // 错误测试
    int deleteData1 = deleteAt(&list, 2);
    printf("删除的内容是:%d\n", deleteData1);
    int deleteData2 = deleteAt(&list, 0);
    printf("删除的内容是:%d\n", deleteData2);
    printf("链表长度为:%zu\n", getLength(&list));
    printLinkedList(&list);

    puts("----------------------6.删除末尾元素----------------------");
    deleteEnd(&list);
    printf("链表长度为:%zu\n", getLength(&list));
    printLinkedList(&list);

    puts("----------------------7.获取指定位置的元素----------------------");
    printf("第一个存储数据的元素结点中的内容是:%d\n", getElementAt(&list, 0));

    puts("----------------------8.修改指定位置的元素----------------------");
    modifyAt(&list, 0, 100);
    printLinkedList(&list);

    puts("----------------------9.释放链表内存----------------------");
    destroyLinkedList(&list);

    return 0;
}

        输出结果如下所示:


6 测试题

1. 比于数组,链表有什么优点和缺点?

【答案】

        优点:添加、删除不用移动元素。

        缺点:每个节点占用额外空间存储下个节点的地址;查找效率低。


2. 某单向链表中,有三个节点,A -->B-->C,现在需要再 B 和 C 之间插入一个 F 节点,我们需要怎么做? 

【答案】

        让 F 指向 C;让 B 指向 F。【顺序不能错】

F->next = B->next;
B->next = F;

3. 以下代码运行之后,数组会变成什么样子?

int arr[5] = {10, 20, 30, 40, 50};
for (int i = 4; i > 1; i--)
{
    arr[i] = arr[i - 1];
}
arr[1] = 250;

【答案】

        10 250 20 30 40

【解析】

        循环过程中:

  • arr[4] = 40
  • arr[3] = 30
  • arr[2] = 20

        循环结束后:

  • arr[1] = 250

4. 写出下面程序的运行结果。

int num1 = 100, num2 = 200;
int *ptr1 = &num1; 
int *ptr2 = ptr1;    
ptr1 = &num2; 
printf("%d", *ptr2);

【答案】

        100

【解析】

  • 初始状态

    • num1 的值是 100,地址假设为 0x1000
    • num2 的值是 200,地址假设为 0x2000
    • ptr1 指向 num1,即 ptr1 = 0x1000
    • ptr2 指向 ptr1 所指向的地址,即 ptr2 = 0x1000
  • 改变 ptr1 的值

    • ptr1 被重新赋值为指向 num2,即 ptr1 = 0x2000
    • ptr2 仍然指向 num1,即 ptr2 = 0x1000
  • 解引用 ptr2

    • *ptr2 解引用 ptr2,获取 num1 的值,即 100。

标签:03,结点,复杂度,元素,list,链表,NULL,指针
From: https://blog.csdn.net/qq_53139964/article/details/142905955

相关文章

  • REXROTH DKC03.3-100-7-FW+FWA-ECODR3-FGP-03VRS-MS驱动器
    适用行业DKC03.3-100-7-FW+FWA-ECODR3-FGP-03VRS-MS型号的驱动器是一款高性能的伺服驱动器,它属于德国博世力士乐(REXROTH)品牌的产品线。这种驱动器通常集成了先进的技术,能够提供精确的速度和位置控制,适用于对动态性能要求较高的应用场景。该类驱动器适用于多种行业,特别是那些......
  • 免费永久HTTPS(ssl)证书——Let's Encrypt来了
    什么是Let'sEncryptLet’sEncrypt是一个非盈利的,免费的CA,可以提供免费HTTPS认证服务。提供了一套完整的工具,基于这套工具,我们可以免费来搭建HTTPS网站。详情可参见它的官网:https://letsencrypt.org/ 为什么使用Let’sEncrypt国内有许多机构可以提供免费的SSL证书,但......
  • DAPLINK 之基于 AIR32F103 制作
    目录1资源2生成指定工程2.1Setup2.2生成工程3构建DAPLink3.1构建stm32f103xb_bl3.2编译stm32f103xb_stm32f103rb_if4测试DAPLINK参考附录:STM32丝印1资源1)官方仓库地址:https://github.com/ARMmbed/DAPLink.git2)硬件:AIR32F103CBT62生成指定工程这里以合宙生......
  • 《GESP1级2303 单选题判断题》 解析(附加编程题)
    描述一、单选题(每题2分,共30分)1.以下不属于计算机输入设备的有(B)。A、键盘B、音箱C、鼠标D、传感器这是一道关于计算机输入设备识别的问题。我们需要分析每个选项,确定它们是否属于计算机的输入设备。‌键盘(A选项)‌:键盘是计算机的一种基本输入设备,用于输入......
  • There is no getter for property named ‘xxxxx’ in ‘class com.xxx.xx.xx.xxxx'”
    原文链接:Thereisnogetterforpropertynamed‘xxxxx’in‘classcom.xxx.xx.xx.xxxx’”,–每天进步一点点(longkui.site) 0.背景SpringMVC架构,使用mybatis执行insert语句,然后开始报错:org.mybatis.spring.MyBatisSystemException:nestedexceptionisorg.apache.......
  • 微服务03 微服务sentinel, springcloudgateway
    6Sentinel6.1Sentinel介绍和工作机制6.1.1微服务流量治理组件介绍随着微服务的流行,服务和服务之间的调用导致服务的稳定性问题变得越来越重要。雪崩问题:微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,即雪崩。解决雪崩问题的常见方式有四种:1......
  • 题解:P10370 「LAOI-4」Mex Tower (Hard ver.)
    ProblemLink「LAOI-4」MexTower(Hardver.)题意给定一个长度为$n$的序列$a$,求序列的$\operatorname{Mex}$值是否大于等于其他所有长度为$n$的自然数序列的$\operatorname{Mex}$值。Solution不难发现,两个数或一个序列的$\operatorname{Mex}$一定是......
  • 算法题——合并两个已排序链表(不使用额外空间)
    题目如下:        输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。        数据范围: 0≤n≤10000≤n≤1000,−1000≤节点值≤1000−1000≤节点值≤1000要求:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)      ......
  • 第十期机器学习基础 03GPT的发展
    一:GPT-1---预测未来在自然语言中,大量的未标记文本语料库非常丰富,但是有标签的数据训练的效果比较好,如果想要在没有标签的数据集上训练出好的模型比较难。因此作者提出了一个想法,在无标签的数据上训练一个预训练模型,然后在这些有标签的子任务上训练一个微调模型。(当时之前是CV领......
  • 第03章 SpringBoot获取请求参数
    我们首先创建“SpringBootRequestDemo”工程。然后我们修改编码格式以及Maven仓库地址,我们省略这个过程了。接着我们再添加spring-boot-starter-parent,spring-boot-starter-web,spring-boot-starter-thymeleaf依赖库<?xmlversion="1.0"encoding="UTF-8"?><projectxm......