顺序表
概念
顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组存储。如图1:
顺序表和数组有什么区别?
顺序表的底层是用数组实现的,是对数组的封装,实现了增删查改等接口。
分类
静态顺序表
概念:使用定长数组存储数据
缺陷:空间给少了不够⽤,给多了造成空间浪费
动态顺序表
动态顺序表顾名思义长度是可以灵活改变的,按需申请空间
动态顺序表的实现
先明确顺序表所需的函数
// 顺序表初始化
void SeqListInit(SeqList* ps);
// 检查顺序表是否满了,满了需要扩容
void SLCheckCapacity(SeqList* ps)
// 顺序表的销毁
void SeqListDestroy(SeqList* ps);
// 顺序表的输出
void SeqListPrint(SeqList* ps);
// 顺序表尾插
void SeqListPushBack(SeqList* ps, SLDateType x);
// 顺序表头插
void SeqListPushFront(SeqList* ps, SLDateType x);
// 顺序表头删
void SeqListPopFront(SeqList* ps);
// 顺序表尾删
void SeqListPopBack(SeqList* ps);
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在任意位置插入数据
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除任意位置的值
void SeqListErase(SeqList* ps, int pos);
顺序表的初始化
顺序表需要先进行初始化,如果不初始化,内存内容不确定
-
顺序表中的元素会包含内存中的随机值(垃圾值)
-
这些值是不可预测的,可能导致程序行为不一致
还会有安全隐患
-
未初始化的内存可能包含敏感信息(如果之前存储过)
-
可能被恶意利用(如信息泄露)
程序错误
-
逻辑错误:使用未初始化的值进行计算会导致错误结果
-
崩溃风险:如果值被当作指针使用可能导致段错误
-
不可重现的bug:在不同环境/时间运行可能表现不同
性能影响
-
某些情况下,初始化内存可以优化后续访问性能
-
现代CPU对初始化的内存访问可能有更好的预取行为
// 顺序表数组初始化
void SeqListInit(SeqList* ps)
{
//ps->arr = NULL;
//ps->size = ps->capacity = 0;
ps->arr = (SLDateType*)malloc(sizeof(SLDateType) * 4);
if (ps->arr == NULL)
{
printf("空间开辟失败\n");
exit(-1);
}
ps->size = 0;
ps->capacity = 4;
}
检查顺序表空间是否需要扩容
当有效数据大于空间时需要对数组进行扩容,扩容通常进行2倍扩容
1. 均摊时间复杂度优化
-
均摊O(1)插入:2倍扩容可以保证在多次插入操作中,扩容的均摊时间复杂度为O(1)
-
数学证明:经过n次插入后,总复制次数约为n + n/2 + n/4 + ... ≈ 2n
2. 内存使用效率
-
空间浪费与复制的平衡:2倍扩容在空间浪费和复制次数之间取得了较好平衡
-
过小扩容因子(如1.1倍)会导致频繁复制
-
过大扩容因子(如3倍)会导致过多内存浪费
3. 内存分配器的特性
-
许多内存分配器对2的幂次大小有优化
-
2倍扩容减少内存碎片,便于内存池管理
4. 实际实现中的变体
-
不同语言/库可能有不同选择:
-
Java ArrayList:初始容量10,扩容1.5倍
-
Python list:初始空或根据元素,扩容约1.125倍
-
C++ vector:标准未规定,通常实现为2倍
-
5. 为什么不是更大的倍数?
-
内存浪费问题:3倍或更大倍数会导致显著的内存空间闲置
-
局部性原理:过大的扩容可能使数据分散在不同内存页
数学视角
假设每次扩容因子为k:
-
扩容次数为O(logₖn)
-
总复制量为n(1 + 1/k + 1/k² + ...) = n×k/(k-1)
-
k=2时,总复制量≈2n
-
k=1.5时,总复制量≈3n
因此2倍是在复制次数和内存利用率之间的一个合理折中。
void SLCheckCapacity(SeqList* ps)
{
// 判断数组空间是否满了
if (ps->size >= ps->capacity)
{
ps->capacity *= 2;
SLDateType* tmp = (SLDateType*)realloc(ps->arr, sizeof(SLDateType) * ps->capacity);
if (tmp == NULL)
{
printf("空间开辟失败\n");
exit(-1);
}
ps->arr = tmp;
}
}
顺序表的销毁
1. 内存资源释放
-
防止内存泄漏:动态分配的内存必须显式释放,否则会随着程序运行不断累积
-
系统资源回收:将不再使用的内存归还给操作系统,供其他程序使用
-
长期运行程序的关键:对于服务器、后台服务等长期运行程序尤为重要
2. 数据结构生命周期管理
-
明确所有权:销毁操作明确了谁负责释放资源(创建者负责销毁)
-
状态重置:通过销毁操作将数据结构标记为"无效状态",防止后续误用
3. 程序健壮性保障
-
避免悬垂指针:规范的销毁操作会将被释放指针置NULL(如
ps->arr = NULL
) -
防御性编程:良好的销毁函数应包含NULL指针检查等安全措施
4. 系统稳定性
-
防止内存碎片:及时释放不再使用的内存块有助于减少内存碎片
-
资源竞争预防:在多线程环境中,明确的销毁操作可以避免资源竞争
void SeqListDestroy(SeqList* ps)
{
assert(ps); // 检查空指针
if (ps->arr) // 先释放内部数组
free(ps->arr);
ps->arr = NULL; // 避免悬垂指针
ps->capacity = ps->size = 0;
}
顺序表的输出
便于观察顺序标中的数据
void SeqListPrint(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
顺序表的插入
尾插
void SeqListPushBack(SeqList* ps, SLDateType x)
{
assert(ps); // 确保ps不为空指针
SLCheckCapacity(ps); // 检查空间是否需要扩容
ps->arr[ps->size] = x; // 将x直接插入最后一个位置
ps->size++; // 而后size自增
}
头插
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
SLCheckCapacity(ps);
for (int i = ps->size; i > 0 ; i--)
{
// 将数组从最后一位开始往后移
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
++ps->size;
}
任意位置插入
void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos <= ps->size && pos >= 0);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
++ps->size;
}
优化头插尾插
void SeqListPushBack(SeqList* ps, SLDateType x)
{
SeqListInsert(ps, ps->size, x);
}
void SeqListPushFront(SeqList* ps, SLDateType x)
{
SeqListInsert(ps, 0, x);
}
顺序表的删除
尾删
void SeqListPopBack(SeqList* ps)
{
assert(ps && ps->size);
--ps->size;
}
头删
void SeqListPopFront(SeqList* ps)
{
assert(ps && ps->size);
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
--ps->size;
}
任意位置删除
void SeqListErase(SeqList* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
int start = pos;
while (start < ps->size)
{
ps->arr[start] = ps->arr[start + 1];
start++;
}
--ps->size;
}
优化尾删头删
void SeqListPopBack(SeqList* ps)
{
SeqListErase(ps, ps->size);
}
void SeqListPopFront(SeqList* ps)
{
SeqListErase(ps, 0);
}
顺序表查找
int SeqListFind(SeqList* ps, SLDateType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i; // 找到数据返回下标
}
}
return -1; // 如果没有数据返回-1
}