02 线性结构——数组(特性、优缺点、基本使用、可变长的动态数组)

news2024/10/26 13:25:05

目录

1 数组基础知识

1.1 认识数组

1.2 数组的声明

1.3 数组的特性

2 数组的优缺点

2.1 优点

2.1.1 查找容易

2.1.2 高效的访问和修改

2.2 缺点

2.2.1 插入和删除效率低

2.2.2 扩展相对繁琐

3 数组的基本使用

3.1 遍历数组

3.2 修改数组元素

4 可变长的动态数组

4.1 实现原理

4.1.1 分配内存(malloc)

 4.1.2 分配内存并初始化为零(calloc)

4.1.3 重新分配内存(realloc)

4.1.4 释放内存(free)

4.2 功能定义

4.3 功能实现


1 数组基础知识

1.1 认识数组

        数组是一种最常见、最简单的数据结构,由相同类型的数据元素组成数组需要先声明才能使用,且其长度一旦确定就不能改变数组的下标从零开始,即第一个元素的下标为 0,最后一个元素的下标为数组长度减 1。

1.2 数组的声明

        在不同的编程语言中,数组的声明和初始化方式有所不同。以下是一些常见编程语言的示例:

java:

int[] array = new int[10];  // 声明一个长度为 10 的整数数组
array[0] = 1;               // 第一个元素赋值为 1
array[9] = 10;              // 最后一个元素赋值为 10
System.out.println(array[0]);  // 输出第一个元素,结果为 1
System.out.println(array[9]);  // 输出最后一个元素,结果为 10

C:

int array[10];  // 声明一个长度为 10 的整数数组
array[0] = 1;   // 第一个元素赋值为 1
array[9] = 10;  // 最后一个元素赋值为 10
printf("%d\n", array[0]);  // 输出第一个元素,结果为 1
printf("%d\n", array[9]);  // 输出最后一个元素,结果为 10

1.3 数组的特性

        固定长度:数组的长度在声明时确定,之后不能改变。例如,声明一个长度为 10 的数组后,不能再增加或减少数组的长度。

        相同类型数组中的所有元素必须是相同的数据类型。例如,一个整数数组的所有元素都是整数。

        下标从零开始数组的下标从 0 开始计数。对于长度为 10 的数组,第一个元素的下标是 0,最后一个元素的下标是 9。

问题:为什么数组的下标从 0 开始计数?

        数组下标从 0 开始的主要原因是为了与指针的偏移量保持一致。在内存中,数组元素是连续存储的,下标 0 代表数组起始位置(即第一个元素),没有偏移。从 0 开始计数可以简化内存地址的计算,使得通过指针访问数组元素时,只需将下标值乘以元素大小并加到数组起始地址上即可得到目标元素的地址。


2 数组的优缺点

2.1 优点

2.1.1 查找容易

        时间复杂度为O(1):通过下标可以直接访问数组中的任意元素,查找操作非常高效。

        不需要额外申请或删除空间数组的存储空间是预先分配好的,访问和修改元素时不需要额外的内存操作

2.1.2 高效的访问和修改

        使用下标索引:通过下标位置索引可以非常高效地访问和修改任意元素,操作速度快。

2.2 缺点

2.2.1 插入和删除效率低

        插入操作:在数组中插入元素时,需要移动大量元素以保持元素空间的连续性。平均情况下,插入操作需要移动 n/2 个元素,时间复杂度为 O(n)

问题:为什么平均情况下,插入操作需要移动 n/2 个元素?

        假设我们在一个长度为 n 的数组中随机选择一个位置插入一个新元素。我们可以考虑所有可能的插入位置,并计算每种情况下需要移动的元素数量,然后取平均值。

1. 插入位置和移动元素的数量

  • 如果在位置 0 插入,需要移动 n 个元素。
  • 如果在位置 1 插入,需要移动 n−1 个元素。
  • 如果在位置 2 插入,需要移动 n−2 个元素。
  • ...
  • 如果在位置 n−1 插入,需要移动 1 个元素。
  • 如果在位置 n 插入,需要移动 0 个元素。

2. 总移动次数

  • 总移动次数是所有可能插入位置上移动次数的总和:n+(n−1)+(n−2)+⋯+1+0
  • 这是一个等差数列的求和公式,总和为:

3. 平均移动次数:

  • 由于有 n+1 个可能的插入位置(从 0 到 n),平均移动次数为:

        因此,平均情况下,插入操作需要移动 n/2​ 个元素。这表明在数组中插入一个元素的平均时间复杂度为 O(n)。

        删除操作:在数组中删除元素时,同样需要移动大量元素以填补空缺。平均情况下,删除操作需要移动 (n−1)/2 个元素,时间复杂度为 O(n)

问题:为什么平均情况下,删除操作需要移动 (n−1)/2 个元素?

        假设我们在一个长度为 n 的数组中随机选择一个位置删除一个元素。我们可以考虑所有可能的删除位置,并计算每种情况下需要移动的元素数量,然后取平均值。

1. 删除位置和移动元素的数量:

  • 如果在位置 0 删除,需要移动 n−1 个元素。
  • 如果在位置 1 删除,需要移动 n−2 个元素。
  • 如果在位置 2 删除,需要移动 n−3 个元素。
  • ...
  • 如果在位置 n−2 删除,需要移动 1 个元素。
  • 如果在位置 n−1 删除,需要移动 0 个元素。

2. 总移动次数

  • 总移动次数是所有可能删除位置上移动次数的总和:(n−1)+(n−2)+(n−3)+⋯+1+0
  • 这是一个等差数列的求和公式,总和为:

3. 平均移动次数:

  • 由于有 n 个可能的删除位置(从 0 到 n−1),平均移动次数为:

        因此,平均情况下,删除操作需要移动 (n−1)/2 个元素。这表明在数组中删除一个元素的平均时间复杂度为 O(n)。

2.2.2 扩展相对繁琐

        需要连续内存空间:扩展数组时,需要确保能够提供更大区域的连续内存空间。如果内存碎片较多,可能难以找到足够大的连续内存块。

        数据复制扩展数组时,需要将原有数据复制到新的数组中,这会带来额外的时间和空间开销。


3 数组的基本使用

3.1 遍历数组

#include <stdio.h>

// 定义数组的大小
#define N 10

int main()
{
    // 初始化一个包含10个整数的数组
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 正向遍历数组
    puts("正向遍历数组");
    for (int i = 0; i < N; i++)
    {
        printf("%d  ", arr[i]);
    }
    printf("\n"); // 换行

    // 逆向遍历数组
    puts("逆向遍历数组");
    for (int i = N - 1; i >= 0; i--)
    {
        printf("%d  ", arr[i]);
    }
    printf("\n"); // 换行

    return 0;
}

        输出结果如下所示:

3.2 修改数组元素

        定义一个整型数组并赋初值,遍历数组让每个索引位置的数组元素都除以首元素,得到的结果作为该索引位置的新值。最后输出处理后的数组。

#include <stdio.h>
#define N 7

int main()
{
    // 定义并初始化一个包含 7 个整数的数组
    int arr[] = {12, 43, 65, 53, -38, 64, 2};

    // 获取数组的首元素
    int firstElement = arr[0];

    // 遍历并打印处理前的数组
    puts("处理前的数组:");
    for (int i = 0; i < N; i++)
    {
        printf("%d  ", arr[i]);
    }
    printf("\n");

    // 遍历数组,将每个元素除以首元素
    // 这种方法是错误的!
    // 这个循环遍历数组,并将每个元素除以 arr[0]。在每次迭代中,arr[0] 都会被重新读取。
    // 这意味着如果 arr[0] 在循环中被修改,后续的除法操作将使用新的 arr[0] 值(即 1)。
    // for (int i = 0; i < N; i++)
    // {
    //     arr[i] = arr[i] / arr[0];  // 错误行为 !!!
    // }

    // 解决方案 1
    // 使用临时变量先将首元素数据保存起来
    // 后面直接除以临时变量即可
    // for (int i = 0; i < N; i++)
    // {
    //     arr[i] = arr[i] / firstElement;
    // }

    // 解决方案 2
    // 倒着遍历数组
    for (int i = N - 1; i >= 0; i--)
    {
        arr[i] = arr[i] / arr[0];
    }

    // 遍历并打印处理后的数组
    puts("将每个元素除以首元素,处理后的数组:");
    for (int i = 0; i < N; i++)
    {
        printf("%d  ", arr[i]);
    }

    printf("\n");

    return 0;
}

        输出结果如下所示:


4 可变长的动态数组

4.1 实现原理

        “可变长的动态数组” 是一种数据结构,它允许在运行时根据需要动态地调整数组的大小,而不需要提前指定固定的大小。这种动态数组通常被称为动态数组动态分配数组动态增长数组动态内存数组

        在 C 语言中,动态数组是通过使用指针和内存分配函数来实现的,常见的内存分配函数包括 malloc、realloc 和 free。以下是一些相关的概念和操作:

4.1.1 分配内存(malloc)

函数原型:

        malloc() 函数用于在程序运行时动态分配一块连续的内存空间。这是 C 语言中常用的动态内存分配函数之一,通常与 free() 函数一起使用,以确保内存的正确管理和释放。

#include <stdlib.h>
void *malloc(size_t size);
  • size:要分配的内存块的大小,以字节为单位
  • 如果内存分配成功,返回一个 void 指针,指向新分配内存块的起始地址
  • 如果内存分配失败(例如内存不足),返回一个空指针 NULL

动态分配整型数据的空间:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    // 在栈区直接创建局部变量
    int num = 120;
 
    int *p = NULL;
    // 动态分配整型数据的空间
    // malloc(sizeof(int)) 请求分配一个 int 类型大小的内存块
    // (int *) 是显式类型转换,将 void 指针转换为 int 指针
    // p 指向新分配内存块的起始地址
    p = (int *)malloc(sizeof(int));
 
    // 检查内存是否分配成功
    if (p == NULL)
    {
        printf("内存分配失败\n");
        return 1; // 退出程序
    }
 
    // p = &num; 不要这样操作,这相当于修改了指针 p 的指向,就没有用到上面动态分配的空间
 
    // 使用解引用赋值并输出
    *p = num;
    printf("p指向的地址(堆区):%p\n", (void *)p);
    printf("局部变量num的地址(栈区):%p\n", (void *)&num);
    printf("p指向的值:%d\n", *p); // 120
 
    // 释放分配的内存,避免内存泄漏
    free(p);
    p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
 
    return 0;
}

        输出结果如下所示:

        错误做法:

动态分配数组空间:
        在 C 语言中,malloc() 函数不仅可用于分配单个变量的内存,还可以用于动态分配数组的内存(指针的偏移)。以下是一个示例,展示了如何使用 malloc() 函数动态分配整型数组的内存,并对其进行操作。

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

int main()
{
    int *p = NULL; // 定义整型指针
    int n = 5;     // 定义数组长度
    // int array[n];  错误,表达式必须含有常量值

    // 动态分配内存,将地址赋给指针 p
    // malloc(n * sizeof(int)) 请求分配一个大小为 n * sizeof(int) 的内存块,即 n 个 int 类型的内存
    // (int *) 是显式类型转换,将 void 指针转换为 int 指针
    // p 指向新分配内存块的起始地址
    p = (int *)malloc(n * sizeof(int));

    // 判断是否分配成功
    if (p == NULL)
    {
        printf("内存分配失败\n");
        return 1; // 退出程序
    }

    // 给数组元素赋值
    for (int i = 0; i < n; i++)
    {
        // p[i] = i * 10;  // p[i] 等价于 *(p + i)
        *(p + i) = i * 10;
    }

    // 输出数组的元素
    for (int i = 0; i < n; i++)
    {
        // printf("p[%d] = %d\n", i, p[i]); // p[i] 等价于 *(p + i)
        printf("p[%d] = %d\n", i, *(p + i));
    }

    // 释放分配的内存,避免内存泄漏
    free(p);
    p = NULL; // 释放后将指针设为 NULL,避免悬挂指针

    return 0;
}

        输出结果如下所示:

 4.1.2 分配内存并初始化为零(calloc)

        calloc() 函数用于在程序运行时动态分配内存,并将分配的内存初始化为零。这是 C 语言中常用的动态内存分配函数之一,通常与 free() 函数一起使用,以确保内存的正确管理和释放。

#include <stdlib.h>
void *calloc(size_t numElements, size_t sizeOfElement);
  • numElements要分配的元素的数量
  • sizeOfElement每个元素的大小(以字节为单位)
  • 如果内存分配成功,返回一个 void 指针,指向新分配内存块的起始地址
  • 如果内存分配失败(例如内存不足),返回一个空指针 NULL

基本用法:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int *p = NULL; // 定义整型指针
    int n = 5;     // 定义数组长度
 
    // 动态分配内存并初始化为零,将地址赋给指针 p
    // calloc(n, sizeof(int)) 请求分配一个大小为 n * sizeof(int) 的内存块,并将每个字节初始化为零
    // (int *) 是显式类型转换,将 void 指针转换为 int 指针
    p = (int *)calloc(n, sizeof(int));
 
    // 判断是否分配成功
    if (p == NULL)
    {
        printf("内存分配失败\n");
        return 1; // 退出程序
    }
 
    // 输出数组的元素的值
    for (int i = 0; i < n; i++)
    {
        printf("p[%d] = %d\n", i, p[i]); // 全是 0
    }
 
    // 给数组元素赋值
    for (int i = 0; i < n; i++)
    {
        p[i] = i * 10;
    }
 
    // 输出数组的元素
    for (int i = 0; i < n; i++)
    {
        printf("p[%d] = %d\n", i, p[i]); // 0 10 20 30 40
    }
 
    // 释放分配的内存,避免内存泄漏
    free(p); // 简单处理
    p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
 
    return 0;
}

        输出结果如下所示:

4.1.3 重新分配内存(realloc)

        realloc() 函数用于重新分配 malloc() 或 calloc() 函数所获得的内存块的大小。这在需要动态调整内存大小时非常有用。

#include <stdlib.h>
void *realloc(void *ptr, size_t size);
  • ptr要重新分配的内存块的指针
  • size新的内存块的大小(以字节为单位)
  • 返回一个指向重新分配内存块的指针。如果内存重新分配成功,返回的指针可能与原始指针相同,也可能不同
  • 如果内存分配失败,返回一个空指针 NULL
  • 如果在原内存块上进行缩减,通常返回的地址与原来的地址相同

基本用法:

        以下是一个示例代码,展示了如何使用 realloc() 函数动态调整内存大小,并使用 _msize() 函数获取指定内存块的大小:

        _msize() 函数用于获取指定内存块的大小,但请注意,这个函数不是标准 C 库的一部分,而是特定于某些平台(如 Windows)。在其他平台上,可能需要使用其他方法来获取内存块的大小。

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
 
int main()
{
    // 声明指针
    int *p = NULL;
 
    // 分配内存
    // 使用 malloc() 函数分配初始内存,大小为 100 * sizeof(int)
    p = (int *)malloc(sizeof(int) * 100);
    if (p == NULL)
    {
        printf("初始内存分配失败\n");
        return 1;
    }
    // 使用 _msize() 函数获取分配的内存大小,并输出指针地址和内存大小
    printf("p=%p, size:%zu 字节\n", p, _msize(p)); // 400
 
    // 调整内存大小
    p = (int *)realloc(p, sizeof(int) * 2000);
    if (p == NULL)
    {
        printf("内存重新分配失败\n");
        return 1;
    }
    // 使用 _msize() 函数获取分配的内存大小,并输出指针地址和内存大小
    printf("p=%p, size:%zu 字节\n", p, _msize(p)); // 8000
 
    // 再次调整内存大小
    // 如果在原内存块上进行缩减,通常返回的地址与原来的地址相同
    p = (int *)realloc(p, sizeof(int) * 200);
    if (p == NULL)
    {
        printf("内存重新分配失败\n");
        return 1;
    }
    // 使用 _msize() 函数获取分配的内存大小,并输出指针地址和内存大小
    printf("p=%p, size:%zu 字节\n", p, _msize(p)); // 800
 
    // 释放分配的内存,避免内存泄漏
    free(p); // 简单处理
    p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
 
    return 0;
}

        输出结果如下所示:

4.1.4 释放内存(free)

        内存泄漏是指在程序运行过程中,动态分配的内存空间没有被正确释放,导致系统中的可用内存逐渐减少,直到耗尽系统可用的内存资源。内存泄漏不仅会影响程序的性能,还可能导致程序崩溃或系统不稳定。

        free() 函数用于释放动态分配的内存,以便将内存返回给操作系统,防止内存泄漏

#include <stdlib.h>
void free(void *ptr);
  • ptr指向要释放的内存块的指针ptr 必须是通过 malloc()、calloc() 或 realloc() 动态分配的内存块地址

基本用法:

        以下是一个示例代码,展示了如何正确使用 malloc() 和 free() 函数:

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

int main() {
    int *p = NULL;  // 定义整型指针

    // 动态分配内存
    p = (int *)malloc(sizeof(int));
    if (p == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 使用分配的内存
    *p = 120;
    printf("p指向的地址:%p\n", (void *)p);
    printf("p指向的值:%d\n", *p);

    free(p);
    p = NULL;  // 释放后将指针设为 NULL

    return 0;
}

4.2 功能定义

        动态数组允许在运行时根据需要动态地调整数组的大小,而不需要提前指定固定的大小。以下是需要实现的主要函数:

功能函数签名描述
初始化动态数组void initDynamicArray(DynamicArray *array, size_t initialCapacity)初始化动态数组,设置初始容量。
释放动态数组内存void destroyDynamicArray(DynamicArray *array)释放动态数组占用的内存。
调整动态数组内存大小void resizeDynamicArray(DynamicArray *array, size_t newCapacity)调整动态数组的容量。
获取动态数组长度size_t getLength(const DynamicArray *array)获取动态数组中元素的个数。
在指定位置插入新元素void insertAt(DynamicArray *array, size_t index, int element)在指定位置插入新元素(假设是 int 类型)。
在末尾插入新元素void insertEnd(DynamicArray *array, int element)在动态数组的末尾插入新元素(假设是 int 类型)。
删除指定位置的元素int deleteAt(DynamicArray *array, size_t index)删除指定位置的元素并返回被删除的元素假设是 int 类型)。
删除末尾的元素int deleteEnd(DynamicArray *array)删除动态数组末尾的元素并返回被删除的元素假设是 int 类型)。
遍历所有元素void print(DynamicArray *array)遍历并打印动态数组中的所有元素。
  • size_t 是 C 标准库中定义的一种无符号整数类型,通常用于表示对象的大小或数组的索引
  • 上述功能是针对元素类型为 int 的数组定义的。在实际编程中,可以根据具体需求对元素类型进行调整。

4.3 功能实现

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

// 动态数组结构体
typedef struct
{
    int *data;       // 指向动态数组的指针,以 int 类型数组为例
    size_t size;     // 当前数组中的元素个数
    size_t capacity; // 当前数组的容量(可以容纳的最大元素个数)
} DynamicArray;

// 初始化动态数组
void initDynamicArray(DynamicArray *array, size_t initialCapacity)
{
    array->data = (int *)malloc(initialCapacity * sizeof(int)); // 分配初始内存
    array->size = 0;                                            // 初始化元素个数为 0
    array->capacity = initialCapacity;                          // 设置初始容量
    printf("成功初始化动态数组^_^容量为:%zu\n", initialCapacity);
}

// 释放动态数组内存
void destroyDynamicArray(DynamicArray *array)
{
    free(array->data);   // 释放动态数组内存
    array->data = NULL;  // 将指针设为 NULL,避免悬挂指针
    array->size = 0;     // 重置元素个数为 0
    array->capacity = 0; // 重置容量为 0
    puts("成功释放动态数组内存^_^");
}

// 调整动态数组内存大小
void resizeDynamicArray(DynamicArray *array, size_t newCapacity)
{
    // 这里不用讨论新容量 newCapacity 的合法性
    // 因为是在扩容时,才会调用此函数,而传递过来的容量都是正数
    array->data = (int *)realloc(array->data, newCapacity * sizeof(int)); // 调整数组内存大小
    array->capacity = newCapacity;                                        // 更新容量

    // 如果新容量小于当前元素个数
    if (newCapacity < array->size)
    {
        // 输出一条错误信息到标准错误流 stderr,这条信息表示新容量小于当前元素个数,数据将被截断
        // fprintf(stderr, "New capacity is less than current size, truncating data\n");

        // 1.可以截断数据
        // 2.或者选择抛出异常
        // 3.或者返回一个错误码,让调用者决定如何处理
        // 4.或者拒绝缩小容量,保持当前容量不变
        // 处理方式视实际情况而定!
        puts("新容量小于当前元素个数,将截断数据!");
        array->size = newCapacity; // 这里我们选择方式:1.数据截断(减少元素个数)
    }
    printf("成功调整动态数组内存大小^_^,新容量为:%zu\n", newCapacity);
}

// 获取动态数组长度(元素个数)
size_t getLength(const DynamicArray *array)
{
    return array->size; // 返回数组中的元素个数
}

// 在指定位置插入新元素(需重点理解 !!!)
void insertAt(DynamicArray *array, size_t index, int element)
{
    // 需要判断指定插入位置的合法性,合法范围:[0,array->size]
    // 注意:在位置 array->size 插入元素,表示末尾插入
    if (index < 0 || index > array->size)
    {
        puts("指定的插入位置不合法,插入失败!");
        return; // 忽略无效的插入位置
    }

    // 如果内部存储数据的数组存满了
    if (array->size >= array->capacity)
    {
        // 容量不足,需要扩容
        size_t newCapacity = array->capacity * 2; // 扩大 1 倍,也可以使用左移运算符: << 1
        puts("数组已满,正在扩容,请稍后重新尝试!");
        // 这里传入 array 时,不要在前面加取地址符 &,因为在这个函数里面,它已然是一个指针
        resizeDynamicArray(array, newCapacity); // 调整动态数组内存大小
    }

    // 插入元素的具体操作
    // 将 index 及其后面的元素依次后移

    // 下面这段代码会报错
    // 因为 array->size 初始值为 0,执行减一操作后为 -1(补码表示很大的数),所以循环会出问题
    // 如果 array->size 初始值不是 0,那么下面的循环就是正确的
    // for (size_t i = array->size - 1; i >= index; i--)
    // {
    //     array->data[i + 1] = array->data[i]; // 后移元素以腾出插入位置
    // }

    // 下面这段代码是正确的
    // 如果 array->size 初始值不是 0,上面那个循环与这个循环等价
    for (size_t i = array->size; i > index; i--)
    {
        array->data[i] = array->data[i - 1]; // 后移元素以腾出插入位置
    }

    // 修改 index 位置数据
    array->data[index] = element; // 在指定位置插入新元素
    array->size++;                // 更新元素个数
    printf("成功在指定位置插入新元素^_^,元素内容:%d\n", element);
}

// 在末尾插入新元素
void insertEnd(DynamicArray *array, int element)
{
    // 这里传入 array 时,不要在前面加取地址符 &,因为在这个函数里面,它已然是一个指针
    // 在尾部插入,应该是最后一个元素后面的一个位置,所以传入索引位置:array->size
    insertAt(array, array->size, element); // 在末尾插入新元素
}

// 删除指定位置的元素并返回被删除的元素(需重点理解 !!!)
int deleteAt(DynamicArray *array, size_t index)
{
    // 需要判断删指定删除位置的合法性,合法范围:[0,array->size -1],不能取到 array->size
    if (index < 0 || index >= array->size)
    {
        return -12345678; // 返回一个指定码
    }

    // 如果数组此时为空,即 array->size = 0
    // 在上面的  if (index < 0 || index >= array->size) 就被拦截了

    int deletedElement = array->data[index]; // 获取被删除的元素

    // 将 index 后面的元素依次前移
    for (size_t i = index + 1; i <= array->size - 1; i++)
    {
        array->data[i - 1] = array->data[i]; // 前移元素以填补删除位置
    }
    // 与上面的循环等价
    // for (size_t i = index; i < array->size - 1; i++)
    // {
    //     array->data[i] = array->data[i + 1]; // 前移元素以填补删除位置
    // }
    array->size--; // 更新元素个数

    puts("成功在指定位置删除元素^_^");
    return deletedElement; // 返回被删除的元素
}

// 删除末尾的元素并返回被删除的元素
int deleteEnd(DynamicArray *array)
{
    // 这里传入 array 时,不要在前面加取地址符 &,因为在这个函数里面,它已然是一个指针
    // 删除末尾的元素,所以传入索引位置:array->size - 1
    return deleteAt(array, array->size - 1); // 删除末尾的元素
}

// 遍历所有的元素
void print(DynamicArray *array)
{
    puts("成功遍历数组,数组内容如下所示:");
    for (int i = 0; i < array->size; i++)
    {
        printf("%d  ", array->data[i]);
    }
    printf("\n");
}

int main()
{
    DynamicArray myArray; // 声明动态数组(结构体变量)

    // 初始化动态数组
    initDynamicArray(&myArray, 2);

    // 向动态数组尾部插入元素
    insertEnd(&myArray, 1); // 插入整数 1
    insertEnd(&myArray, 2); // 插入整数 2
    // 打印动态数组当前长度
    printf("动态数组当前长度(元素个数):%zu\n", getLength(&myArray));
    // 打印动态数组的内容
    print(&myArray);
    puts("------------------------------------");

    // 在索引 0 的位置插入元素 3
    insertAt(&myArray, 0, 3);
    // 再次打印动态数组当前长度
    printf("动态数组当前长度:%zu\n", getLength(&myArray));
    // 打印动态数组的内容
    print(&myArray);
    puts("------------------------------------");

    // 在索引 100 的位置插入元素 999(错误插入)
    insertAt(&myArray, 100, 999); // 错误插入
    puts("------------------------------------");

    // 删除索引 1 的元素
    int returnCode = deleteAt(&myArray, 1);
    if (returnCode = -12345678)
    {
        puts("指定的删除位置不合法,删除失败!");
    }
    else
    {
        printf("删除索引 1 的元素,该元素是:%d\n", returnCode);
    }
    // 再次打印动态数组当前长度
    printf("动态数组当前长度:%zu\n", getLength(&myArray));
    // 打印动态数组的内容
    print(&myArray);
    puts("------------------------------------");

    // 删除动态数组末尾元素
    printf("删除动态数组末尾元素,该元素是%d\n", deleteEnd(&myArray));
    // 再次打印动态数组当前长度
    printf("动态数组当前长度:%zu\n", getLength(&myArray));
    // 打印动态数组的内容
    print(&myArray);
    puts("------------------------------------");

    // 删除索引 666 的元素(错误删除)
    returnCode = deleteAt(&myArray, 666); // 错误删除
    if (returnCode = -12345678)
    {
        puts("指定的删除位置不合法,删除失败!");
    }
    else
    {
        printf("删除索引 666 的元素,该元素是:%d\n", returnCode);
    }
    puts("------------------------------------");

    // 释放动态数组内存
    destroyDynamicArray(&myArray);
    printf("动态数组内存释放完成\n");

    return 0;
}

4.3.1 注意事项

        在插入元素时后移元素和在删除元素时前移元素的循环需要特别注意,如下所示:

插入元素时后移元素

// 插入元素的具体操作
// 将 index 及其后面的元素依次后移

// 下面这段代码会报错
// 因为 array->size 初始值为 0,执行减一操作后为 -1(补码表示很大的数),所以循环会出问题
// 如果 array->size 初始值不是 0,那么下面的循环就是正确的
// for (size_t i = array->size - 1; i >= index; i--)
// {
//     array->data[i + 1] = array->data[i]; // 后移元素以腾出插入位置
// }

// 下面这段代码是正确的
// 如果 array->size 初始值不是 0,上面那个循环与这个循环等价
for (size_t i = array->size; i > index; i--)
{
    array->data[i] = array->data[i - 1]; // 后移元素以腾出插入位置
}

// 修改 index 位置数据
array->data[index] = element; // 在指定位置插入新元素
array->size++;                // 更新元素个数

删除元素时前移元素

// 将 index 后面的元素依次前移
// for (size_t i = index + 1; i <= array->size - 1; i++)
// {
//     array->data[i - 1] = array->data[i]; // 前移元素以填补删除位置
// }

// 与上面的循环等价
for (size_t i = index; i < array->size - 1; i++)
{
    array->data[i] = array->data[i + 1]; // 前移元素以填补删除位置
}
array->size--; // 更新元素个数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2210425.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Nature 正刊丨突变与ecDNA形成尿路上皮癌症演变的相互作用

01摘要 晚期癌症是一种常见的致死性疾病&#xff0c;其特征是明显的遗传异质性1。在这项研究中&#xff0c;我们研究了由内源性和外源性诱变过程引起的基因组特征的进化及其与复杂结构变异&#xff08;SV&#xff09;的相互作用。我们叠加了癌症患者匹配序列肿瘤的突变特征和系…

设计模式之组合模式(Composite)

一、组合模式介绍 组合模式(Composite Pattern) 的定义是&#xff1a;将对象组合成树形结构以表示整个部分的层 次结构。组合模式可以让用户统一对待单个对象和对象的组合。 如在windows操作系统中的目录结构&#xff0c;其实就是树形目录结构&#xff0c;可以通过 tree /f 命令…

81.【C语言】数据结构之空间复杂度

目录 1.定义 2.例题 计算下列代码中BubbleSort函数的空间复杂度 解: 3.练习 1.求下列代码的空间复杂度 解: 2.求下列代码的空间复杂度 解: 1.定义 对一个算法在运行过程中临时占用存储空间大小的量度,不是程序占用了多少bytes的空间,通常用多少个变量来衡量,也使用大…

公开课学习:软件测试面试3大难题

1.验证码机制的处理&#xff1a;自动化遇到验证码怎么办?怎么测试? 流程&#xff1a;先识别元素&#xff0c;再对元素进行操作。实际上&#xff0c;验证码无法用自动化技术操作解决&#xff0c;都是由开发给万能码&#xff0c;或者屏蔽验证码去解决&#xff01;那如果不能屏…

2.1 HTML5 - Canvas标签

文章目录 引言Canvas标签概述定义实例&#xff1a;创建画布 理解Canvas坐标系概述实例&#xff1a;获取Canvas坐标 获取Canvas环境上下文概述实例&#xff1a;获取Canvas上下文设置渐变色效果 结语 引言 大家好&#xff0c;今天我们要一起探索HTML5中一个非常有趣且强大的特性…

一文通透OpenAI o1:从CoT、Self-Correct/STaR、Self-play RL、MCST等技术细节到工程复现

前言 注意&#xff0c;本文自10.12日起&#xff0c;正在每天更新的过程中.. 可能是去年写或讲的关于ChatGPT原理的文章和课程&#xff0c;影响力太大了 导致​​自从OpenAI o1出来后&#xff0c;每过两天&#xff0c;就有朋友问&#xff0c;“校长 o1啥时候出解读或课程”&…

ESP-01S 通过MQTT控制舵机

一、硬件准备 1. ESP-01S 模块 负责控制和联网的单片机为 ESP-01S,其核心是乐鑫科技(ESPRESSIF)设计的 ESP8266,也是大部分嵌入式开发发烧友常用的 WiFi 模块。 外观和引脚分布如下图: 具体引脚定义如下表: 脚序 名称 功能说明 1 GND 接地 2 IO2 GPIO…

IP地址如何支持远程办公?

由于当今社会经济的飞速发展&#xff0c;各个方向的业务都不免接触到跨省、跨市以及跨国办公的需要&#xff0c;随之而来的远程操作的不方便&#xff0c;加载缓慢&#xff0c;传输文件时间过长等困难&#xff0c;如何在万里之外实现远程办公呢&#xff1f;我们以以下几点进行阐…

【NLP自然语言处理】探索注意力机制:解锁深度学习的语言理解新篇章

目录 &#x1f354; 注意力机制介绍 1.1 注意力概念 1.2 注意力计算规则 1.3 常见的注意力计算规则 &#x1f354; 什么是注意力机制 &#x1f354; 注意力机制的作用 &#x1f354; 注意力机制实现步骤 4.1 步骤 4.2 代码实现 &#x1f354; 小结 学习目标 &#x1…

C++求日期差值题目

C日期差值题目&#xff08;牛客网&#xff09;题目超链接 仅个人思路不是最优解 仔细阅读地题目&#xff0c;要求输入连续的两串数字表示两个日期 所以我感觉日期类不太方便&#xff08;也许是我实力不允许&#xff09; cin使用起来就不太方便&#xff0c;我这里选择使用sca…

ubuntu服务器监控程序崩溃自动重启

环境&#xff1a;监控程序运行情况分为两种情况&#xff0c;一种带界面&#xff0c;一种控制台程序&#xff0c;带界面程序采用脚本监控方式&#xff0c;不带界面采用Supervisor工具监控。 1. 自动重启带界面程序&#xff1a; #!/bin/sh while true; do processExistps aux | …

Android 未来可能支持 Linux 应用,Linux 终端可能登陆 Android 平台

近日&#xff0c;根据 android authority 的消息&#xff0c;Google 正在开发适用于 Android 的 Linux 终端应用&#xff0c;而终端应用可以通过开发人员选项启用&#xff0c;并将 Debian 安装在虚拟机中。 在几周前&#xff0c;Google 的工程师开始为 Android 开发新的 Termi…

2021年新版Go工程师大厂面试题Go面试题容器技术

Go面试题--容器技术 1、为什么需要 DevOps 在当今&#xff0c;软件开发公司在软件新版本发布方面&#xff0c;多尝试通过发布一系列以小的 特性改变集为目标的新软件版本&#xff0c;代替发布一个大特性改变集的新软件版本的 方式。这种方式有许多优点&#xff0c;诸如&#x…

分布式数据库的进度管理:TiDB 备份恢复工具 PiTR 的原理与实践

导读 对于一款企业级数据库产品而言&#xff0c;数据的安全性和可恢复性是至关重要的。PiTR&#xff08;Point in Time Restore&#xff09;作为 TiDB 备份工具的核心功能之一&#xff0c;提供了一种精细的数据恢复能力&#xff0c;允许用户将数据库集群恢复到过去的任意时间点…

通过多元蒙特卡罗模拟来预测股票价格的日内波动性

作者&#xff1a;老余捞鱼 原创不易&#xff0c;转载请标明出处及原作者。 写在前面的话&#xff1a; 日内价格波动对交易策略的重要性不言而喻&#xff0c;尤其是美跨式交易策略&#xff08;The American straddle&#xff09;。由于无法预测所有影响股价的因素&#x…

【原创】java+springboot+mysql法律咨询网系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

loadlocale.c:130: _nl_intern_locale_data: failed已放弃 (核心已转储)]问题

在进行交叉编译cortex-a9架构时&#xff0c;出现以上错误。 问题描述&#xff1a; 在使用 arm-none-linux-gnueabi-gdb 进行 Cortex-A9 架构交叉编译调试时&#xff0c;出现如下错误&#xff1a; arm-none-linux-gnueabi-gdb: loadlocale.c:130: _nl_intern_locale_data: As…

新型物联网电力数据采集器 智能网关通讯协议有哪些?

随着智能化技术的快速发展&#xff0c;电气监测与管理在各个域的应用愈发重要&#xff0c;在物联网&#xff08;IoT&#xff09;应用的发展中&#xff0c;网关扮演着至关重要的角色。它作为连接设备与云平台或数据中心的桥梁&#xff0c;负责数据的收集、处理和传输。网关不仅支…

鸿蒙开发之ArkUI 界面篇 三十四 容器组件Tabs二 常用属性

barPosition&#xff1a;位置开头或结尾,vertical 水平或者垂直,scrollable手势滑动切换,animationDuration 滑动动画时间。BarPosition.Start 效果如下图&#xff1a; BarPosition.End 效果如下图&#xff1a; 如果显示在左边&#xff0c;使用的是vertical属性,下图&#xff0…

Chrome(谷歌)浏览器 数据JSON格式美化 2024显示插件安装和使用

文章目录 目录 文章目录 安装流程 小结 概要安装流程技术细节小结 概要 没有美化的格式浏览器展示 美化之后效果图 安装流程 下载地址 https://github.com/gildas-lormeau/JSONVue 点击下载 下载成功&#xff0c;如图所示 解压文件 添加成功&#xff0c;如图所示 通过浏览器…