33_顺序表(待完善)

核心摘要: 本文使用C语言从零实现动态顺序表,重点讲解内存分配策略、增删改查的核心算法以及边界条件处理,解决"数组越界"和"扩容无效"两大常见问题。


一、顺序表的基本概念与结构设计

1. 逻辑结构与物理结构对比

(1) 逻辑结构:线性表(元素之间具有一对一的前后关系)。

(2) 物理结构:连续的内存地址(与数组一致,但支持动态扩展)。

2. 静态数组 vs 动态顺序表

(1) 静态数组的致命缺陷:容量固定,编译时确定,易溢出。

(2) 动态顺序表的优势:运行时扩容,内存利用率高。

3. 结构体定义(C语言描述)

采用 SeqList 结构体封装三个必要属性:

c 复制代码
typedef int SLDataType;  // 方便后续修改数据类型(如改为char或double)

typedef struct SeqList
{
    SLDataType* a;      // 指向动态开辟数组的指针
    int size;           // 有效数据个数(当前长度)
    int capacity;       // 当前最大容量(总空间大小)
} SeqList;

a. 为什么需要 capacity ------ 区分"已用空间"和"总空间",是扩容判断的依据。

b. 为什么用 typedef 定义数据类型? ------ 提高代码复用性,一行修改即可适配int/float/struct。


二、核心操作的实现(增删改查)

1. 初始化与销毁(内存管理)

(1) 初始化 :将指针置 NULLsizecapacity 置 0。

c 复制代码
void SLInit(SeqList* psl);

(2) 销毁 :释放 psl->a,重新调用初始化(防止野指针)。

c 复制代码
void SLDestroy(SeqList* psl);

2. 扩容机制(核心难点)

(1) 触发条件size == capacity

(2) 扩容策略

a. 小容量场景:capacity 为 0 时,开辟 4 个元素大小。

b. 大容量场景:新容量 = 原容量 × 2(指数增长,减少 realloc 次数)。

(3) 扩容函数实现

c 复制代码
void SLCheckCapacity(SeqList* psl)
{
   if (psl->size == psl->capacity)
   {
       int newCapacity = (psl->capacity == 0 ? 4 : psl->capacity * 2);
       SLDataType* tmp = (SLDataType*)realloc(psl->a, newCapacity * sizeof(SLDataType));
       if (tmp == NULL)
       {
           perror("realloc fail");
           return;
       }
       psl->a = tmp;
       psl->capacity = newCapacity;
   }

3. 增(头插 / 尾插 / 任意位置插入)

(1) 尾插 :先检查容量,再在 psl->a[psl->size] 处赋值,最后 size++。时间复杂度 O(1)。

(2) 头插 :从后往前移动元素(for 循环从 size1),腾出 [0] 位置。时间复杂度 O(n)。

(3) 任意位置插入 :将 [pos, size-1] 全部后移一位,再插入。必须判断 pos 合法性(0 ≤ pos ≤ size)。

4. 删(头删 / 尾删 / 任意位置删除)

(1) 尾删 :直接 size--(注意:不需要真正清除内存,下次插入会覆盖)。

(2) 头删 :从前往后移动元素(for 循环从 1size-1),最后 size--

(3) 任意位置删除 :将 [pos+1, size-1] 前移覆盖 pos 位置。

⚠️ 删除操作的三重检查:

⓵ 断言 psl 不为空。

⓶ 断言 psl->size > 0(空表不能删)。

⓷ 检查 pos 范围:0 ≤ pos < size。

5. 查与改

(1) 按值查找:遍历数组,返回第一个匹配的下标(找不到返回 -1)。

(2) 按位置修改 :直接赋值 psl->a[pos] = x前提是 pos < size

(3) 按值修改:先查找位置,再修改(查找逻辑复用)。


三、打印与调试辅助函数

1. 打印顺序表

遍历 size 次,输出 psl->a[i],格式统一为 [1, 2, 3, 4]

2. 菜单交互示例(用于控制台测试)

提供 0-7 的数字菜单:

⓵ 尾插   ⓶ 头插   ⓷ 尾删   ⓸ 头删

⓹ 任意插入   ⓺ 任意删除   ⓻ 查找   ⓼ 打印   ⓿ 退出


四、常见错误与避坑指南(实用性重点)

1. 扩容时的野指针问题

(1) 错误写法 :直接用 realloc(psl->a, newSize) 且不判断返回值。

(2) 正确写法 :用临时指针 tmp 接收 realloc 返回值,成功后再赋值给 psl->a

原因realloc 失败返回 NULL,直接赋值会丢失原有内存地址,导致内存泄漏且无法恢复。

2. 删除/插入时的越界访问

(1) 头插移动元素时,必须从后往前移动for (i = size; i > pos; i--))。

(2) 头删移动元素时,必须从前往后移动for (i = pos; i < size-1; i++))。

口诀:插入向后挪,删除向前盖;反向操作必越界。

3. 结构体传值 vs 传址

(1) 错误 :函数参数写 void func(SeqList sl),修改的是临时副本。

(2) 正确 :一律传指针 void func(SeqList* psl),用 -> 访问成员。

4. 缩容的误区

不建议在删除元素时立即缩容(频繁缩容会导致性能抖动)。业界通用做法:只扩不缩 ,或仅在剩余空间占比极低时(如 size < capacity/4)考虑缩容。


五、完整代码文件结构(项目组织)

1. 分文件编写(工业级规范)

(1) SeqList.h ------ 头文件:结构体定义、函数声明、宏定义。

(2) SeqList.c ------ 源文件:所有函数的具体实现。

(3) test.c ------ 测试文件:main 函数和菜单交互。

2. 头文件中的防重复包含

c 复制代码
#ifndef _SEQLIST_H_
#define _SEQLIST_H_

// 所有声明放在这里

#endif

3. 编译命令示例(gcc)

bash 复制代码
gcc -o test SeqList.c test.c -std=c99 -Wall

六、顺序表的性能总结与适用场景

1. 时间复杂度汇总

操作 时间复杂度 备注
尾插 / 尾删 O(1) 平均情况(考虑扩容均摊后仍为 O(1))
头插 / 头删 / 任意位置插入删除 O(n) 需要移动大量元素
按位置访问 O(1) 随机访问是顺序表的最大优势
按值查找 O(n) 无序情况下必须遍历

2. 适用场景(什么时候用顺序表)

⓵ 需要频繁随机访问(如通过下标取第 i 个元素)。

⓶ 插入/删除操作只在尾部进行(栈结构)。

⓷ 元素个数可预估,或对内存连续有硬性要求(如 DMA 传输)。

3. 不适用场景(什么时候该换链表)

⓵ 频繁在头部或中间插入/删除。

⓶ 元素个数极其不稳定,且单个元素体积很大(扩容拷贝成本高)。

相关推荐
汀、人工智能3 小时前
[特殊字符] 第26课:环形链表
数据结构·算法·链表·数据库架构··环形链表
smj2302_796826523 小时前
解决leetcode第3883题统计满足数位和数组的非递减数组数目
python·算法·leetcode
点灯小铭3 小时前
基于单片机的多路温湿度采集与WIFI智能报警控制系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
小比特_蓝光3 小时前
算法篇二----二分查找
java·数据结构·算法
嵌入式×边缘AI:打怪升级日志3 小时前
MX6ULL 的 GPIO 操作方法(保姆级教程)
stm32·单片机·嵌入式硬件
田梓燊3 小时前
leetcode 56
java·算法·leetcode
wwj888wwj3 小时前
Ansible基础(复习1)
linux·运维·ansible
点灯小铭3 小时前
基于单片机的球类比赛专用计分与暂停管理系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
yj_xqj3 小时前
Linux network启动报错 && nmcli 的使用
linux·运维·服务器