目录

一、前言:聚焦动态数组的删除操作

大家好,我是 Hello_Embed。上一篇我们实现了动态数组 “中间添加节点” 的功能,本次笔记将围绕动态数组的删除操作展开 —— 这是动态数组核心操作的另一半,主要包含三种场景:删除尾部元素删除全部元素删除中间元素,同时会讲清楚 “删除全部元素” 与 “释放动态数组” 的本质区别,避免内存操作误区。

二、尾部删除:最简单的删除方式

2.1 核心逻辑:为什么无需赋值 0?

尾部删除是三种删除方式中最简单的,核心操作只有 “将已存数据数量(nCount)减 1”,无需给末尾元素赋值 0,原因如下:动态数组的所有操作(添加、遍历、输出)都以 “nCount” 为依据 —— 比如遍历数据时,循环条件是 “i < nCount”,只要nCount减 1,后续操作就不会访问到 “原末尾元素”,相当于 “逻辑删除”,赋值 0 不仅多余,还会增加不必要的计算开销。

2.2 代码实现与测试验证

尾部删除函数仅需两步:参数合法性检测(空指针判断)、nCount自减:

// 动态数组尾部删除函数:删除最后一个元素
void Delate_Last(struct SZ *pSZ)
{
    // 容错:判断结构体指针是否为空
    if(NULL == pSZ)
    {
        printf("NULL!\n");
        return;
    }
    // 核心操作:已存数据数量减1(逻辑删除尾部元素)
    pSZ->nCount--;
}
主函数测试逻辑

先通过 “末尾添加” 和 “中间添加” 初始化数组,再调用两次Delate_Last,对比删除前后的输出结果:

int main(void)
{
    struct SZ SZ1;//定义动态数组结构体
    SZInit(&SZ1);//初始化

    // 步骤1:初始化数组数据,最终数组为 [1,2,100,3,4,5,6,200]
    Add_Last(&SZ1, 1);
    Add_Last(&SZ1, 2);
    Add_Last(&SZ1, 3);
    Add_Last(&SZ1, 4);
    Add_Last(&SZ1, 5);
    Add_Last(&SZ1, 6);
    Add_Middle(&SZ1, 100, 2);  // 下标2插入100
    Add_Middle(&SZ1, 200, 10); // 下标过大,插入末尾

    printf("删除前:\n");
    Output(&SZ1);//输出删除前的数组

    // 步骤2:调用两次尾部删除,删除最后两个元素(6和200)
    Delate_Last(&SZ1);
    Delate_Last(&SZ1);

    printf("\n删除后:\n");
    Output(&SZ1);//输出删除后的数组

    free(SZ1.pSDZ);//释放内存
    return 0;
}
测试结果

如下图所示,删除后数组末尾两个元素(6、200)消失,已存数量(Count)从 8 变为 6,符合预期:
请添加图片描述

2.3 讨论:是否需要缩减容量?

nCount减小到远小于nSize(比如容量 25,仅存 6 个数据)时,是否需要缩减容量?答案是不需要,原因如下:

  • 效率优先:如果每次nCount减小都缩减容量,需要重新申请小空间、复制数据、释放旧空间 —— 后续若再添加数据,又要重新扩容,反复操作会大幅降低程序效率;
  • 行业惯例:大多数算法中,动态数组只 “扩容” 不 “缩容”,优先保证操作速度,少量冗余空间对现代计算机影响可忽略。

三、全部删除:快速清空数据

3.1 核心逻辑:效率优先,无需逐个赋值 0

“全部删除” 的目标是清空数组中的所有数据,核心逻辑依然是 “操作nCount”—— 直接将nCount赋值为 0,无需遍历数组给每个元素赋值 0,原因和尾部删除一致:

  • 赋值 0 会遍历整个数组,若数据量大会浪费大量时间(效率低);
  • nCount赋值为 0 后,所有操作都会认为数组 “无数据”,达到 “逻辑清空” 的目的。

3.2 代码实现与测试验证

全部删除函数仅需 “空指针检测 +nCount置 0”:

// 动态数组全部删除函数:逻辑清空所有数据
void Delate_All(struct SZ *pSZ)
{
    // 容错:判断结构体指针是否为空
    if(NULL == pSZ)
    {
        printf("NULL!\n");
        return;
    }
    // 核心操作:已存数据数量置0(逻辑清空所有数据)
    pSZ->nCount = 0;
}
测试逻辑

在 “尾部删除” 的测试代码后,添加Delate_All调用:

// 尾部删除后的代码继续添加
printf("\n全部删除后:\n");
Delate_All(&SZ1);
Output(&SZ1);
测试结果

如下图所示,全部删除后Count变为 0,数据无显示,但Size(容量)仍保留,说明仅清空了 “逻辑数据”,未改变内存容量:
请添加图片描述

四、释放动态数组:彻底回收内存

4.1 关键区别:“全部删除” vs “内存释放”

很多人会混淆 “全部删除” 和 “释放动态数组”,两者本质不同:

  • 全部删除(Delate_All):仅逻辑清空(nCount=0),数组的容量(nSize)、首地址(pSDZ)仍有效,后续可继续添加数据;
  • 内存释放(Relase):彻底回收动态数组占用的内存,需将nSize置 0、pSDZ置空,并调用free释放pSDZ指向的内存 —— 释放后数组无法直接使用,需重新初始化。

4.2 代码实现与测试验证

释放动态数组需四步:空指针检测、nSize置 0、nCount置 0、pSDZ置空 +free

// 动态数组内存释放函数:彻底回收内存,数组不可再用
void Relase(struct SZ *pSZ)
{
    // 容错:判断结构体指针是否为空
    if(NULL == pSZ)
    {
        printf("NULL!\n");
        return;
    }
    // 1. 容量置0
    pSZ->nSize = 0;
    // 2. 已存数量置0
    pSZ->nCount = 0;
    // 3. 释放动态内存(避免内存泄漏)
    free(pSZ->pSDZ);
    // 4. 指针置空(避免野指针,防止误操作)
    pSZ->pSDZ = NULL;
}
测试逻辑

在 “全部删除” 的测试代码后,添加Relase调用:

// 全部删除后的代码继续添加
printf("\n释放数组后:\n");
Relase(&SZ1);
Output(&SZ1);
测试结果

如下图所示,释放后SizeCount均为 0,数据无显示,数组彻底失效,需重新调用SZInit才能使用:
请添加图片描述

五、中间删除:需数据前移的复杂操作

5.1 核心逻辑:对比 “中间插入”,方向相反

中间删除比尾部、全部删除复杂,核心是 “数据前移”—— 对比 “中间插入” 的 “数据后移”,两者方向相反:

  • 中间插入:从最后一个元素开始,往后挪 1 位,腾出目标下标位置;
  • 中间删除:从目标下标开始,往前填 1 位(用后一个元素覆盖当前元素),覆盖待删除元素。

函数参数仅需 “结构体指针 + 目标下标”(无需数据参数,因为是删除已有元素),同时要处理 “下标过大” 的容错(提示错误,不执行删除)。

5.2 代码实现(含下标容错)

注意下标容错和数据前移逻辑:

// 动态数组中间删除函数:删除指定下标处的元素
void Delate_Middle(struct SZ *pSZ, unsigned int Index)
{
    // 容错:1.指针为空;2.下标过大(超过已存数据范围)
    if(NULL == pSZ || Index > pSZ->nCount)
    {
        printf("NULL 或 下标过大!\n");
        return;
    }
    // 核心操作:数据前移(从目标下标开始,用后一个元素覆盖当前元素)
    for(unsigned int i = Index; i < pSZ->nCount - 1; i++)
    {
        pSZ->pSDZ[i] = pSZ->pSDZ[i+1];
    }
    // 数据前移后,已存数量减1
    pSZ->nCount--;
}

5.3 循环条件解析:为什么要 “nCount-1”?

循环条件i < pSZ->nCount - 1是关键,避免数组越界,原因如下:假设nCount=3(数据下标 0、1、2),删除下标 1 的元素:

  • 若循环条件为i < nCounti会遍历到 2,此时i+1=3,而pSDZ[3]超出数组已存数据范围(越界);
  • 改为i < nCount - 1i仅遍历到 1(nCount-1=2),i+1=2,刚好是最后一个元素,无越界风险。

5.4 测试验证

主函数测试逻辑

初始化简单数组,中间插入数据后,删除指定下标元素:

int main(void)
{
    struct SZ SZ1;//定义动态数组结构体
    SZInit(&SZ1);//初始化

    // 步骤1:初始化数组为 [1,2]
    Add_Last(&SZ1, 1);
    Add_Last(&SZ1, 2);
    printf("初始数组:\n");
    Output(&SZ1);

    // 步骤2:中间插入数据,数组变为 [1,2,100,200]
    Add_Middle(&SZ1, 100, 2);  // 下标2插入100
    Add_Middle(&SZ1, 200, 10); // 下标过大,插入末尾
    printf("\n插入数据后:\n");
    Output(&SZ1);

    // 步骤3:删除下标2处的元素(即100)
    Delate_Middle(&SZ1, 2);
    printf("\n删除下标2后:\n");
    Output(&SZ1);

    free(SZ1.pSDZ);//释放内存
    return 0;
}
测试结果

如下图所示,下标 2 处的元素 100 被成功删除,数组变为[1,2,200]Count从 4 变为 3,符合预期:
请添加图片描述

六、数组特性总结与链表对比

动态数组的核心特性(优缺点)与后续要学的 “链表” 有本质区别,这里提前对比,为后续学习铺垫:

特性 动态数组(顺序表) 链表(后续学习)
内存存储 连续内存空间 非连续内存空间(节点通过指针连接)
访问方式 随机访问(下标访问,效率高) 顺序访问(需遍历前序节点,效率低)
增删效率 尾部增删快,中间增删慢(需移动数据) 中间增删快(仅需改指针),尾部增删慢(需遍历)
空间利用率 可能有冗余空间(仅扩容不缩容) 无冗余空间(按需申请节点)

核心结论:选择哪种结构,需结合场景 —— 若频繁 “随机访问”,选动态数组;若频繁 “中间增删”,选链表。

七、下一篇预告

动态数组的核心操作(创建、添加、删除、释放)已全部讲完,下一篇我们将用动态数组的 “下标特性” 做一个实践案例 ——实现 99 乘法表,通过案例巩固数组下标的使用,同时练习循环逻辑,为后续更复杂的数据结构应用铺垫。

八、总结

本次笔记围绕动态数组的删除操作展开,核心收获包括:

  1. 三种删除方式的核心逻辑:
    • 尾部删除:nCount--(逻辑删除,无需赋值 0);
    • 全部删除:nCount=0(效率优先,不遍历赋值);
    • 中间删除:数据前移(从目标下标开始)+ nCount--(注意循环越界);
  2. 关键区别:“全部删除” 是逻辑清空,“内存释放” 是彻底回收(需free和指针置空);
  3. 数组特性:连续内存 + 随机访问(优点),中间增删需移动数据(缺点),对比链表明确适用场景;
  4. 实践建议:动态数组操作的核心是 “围绕nCount和下标”,避免内存越界和泄漏,一定要动手敲代码验证逻辑。

动态数组是线性表的基础,掌握其增删操作后,后续学习链表、栈、队列会更轻松。我是 Hello_Embed,数据结构系列笔记持续更新中!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐