
目录
[1. 初始化](#1. 初始化)
[2. 销毁](#2. 销毁)
[1. 为什么用 realloc?](#1. 为什么用 realloc?)
[2. 为什么按 2 倍扩容?](#2. 为什么按 2 倍扩容?)
[3. 为什么用 tmp?](#3. 为什么用 tmp?)
[1. 尾插(最优操作)](#1. 尾插(最优操作))
[2. 头插](#2. 头插)
[3. 指定位置插入](#3. 指定位置插入)
[1. 尾删](#1. 尾删)
[2. 头删](#2. 头删)
[3. 指定位置删除](#3. 指定位置删除)
[1. 顺序表 vs 链表](#1. 顺序表 vs 链表)
[2. 为什么顺序表要扩容?](#2. 为什么顺序表要扩容?)
[3. 扩容为什么不用 +1?](#3. 扩容为什么不用 +1?)
一、整体认知
顺序表可以说是高级数组,通过接口设计可对数组实现增删查改等操作。
是重要的数据结构只是,现在让我们一起感受顺序表的魅力吧!
核心特征:
-
底层是一段连续内存(数组)
-
支持随机访问(O(1))
-
插入/删除需要挪动数据(O(n))
-
通过
realloc实现动态扩容
二、数据结构设计
cpp
typedef int SLDateType;
typedef struct SqeList
{
SLDateType* arr; // 指向动态数组
int size; // 当前有效元素个数
int capacity; // 当前容量
} SL;
面试要点
-
size≠capacity -
size:已使用空间 -
capacity:总可用空间 -
arr == NULL是初始化态
三、生命周期管理
1. 初始化
cpp
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
关键点:
-
不直接分配空间(懒加载思想)
-
第一次插入时再扩容
2. 销毁
cpp
void SLDestroy(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
面试关注点:
-
释放堆空间(避免内存泄漏)
-
指针置空(防止野指针)
-
状态重置(可复用)
四、扩容机制(核心)
cpp
void SLCheckCapacity(SL* ps)
{
if (ps->capacity == ps->size)
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDateType* tmp = (SLDateType*)realloc(ps->arr, newcapacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
深度理解(面试高频)
1. 为什么用 realloc?
-
自动处理:
-
原地扩容(可能)
-
或搬迁到新地址
-
-
避免手动
malloc + memcpy
2. 为什么按 2 倍扩容?
-
保证均摊时间复杂度 O(1)
-
避免频繁扩容
3. 为什么用 tmp?
SLDateType* tmp = realloc(...)
- 防止 realloc 失败导致原指针丢失(内存泄漏)
五、插入操作
1. 尾插(最优操作)
cpp
void SLPushBack(SL* ps, SLDateType x)
{
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
复杂度:
- 均摊 O(1)
2. 头插
cpp
void SLPushHead(SL* 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++;
}
关键点:
-
必须从后往前移动(避免覆盖)
-
时间复杂度 O(n)
3. 指定位置插入
cpp
void SLInsert(SL* ps, int pos, SLDateType x)
{
assert(ps->arr);
assert(pos <= ps->size - 1 && 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++;
}
代码问题(面试加分点)
assert(pos <= ps->size - 1 && ps >= 0);
❌ 错误:
-
ps >= 0没意义(指针比较) -
插入位置应该允许
pos == size
✅ 正确写法:
assert(pos >= 0 && pos <= ps->size);
六、删除操作
1. 尾删
cpp
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
ps->size--;
}
本质:
- 逻辑删除(不释放空间)
2. 头删
cpp
void SLPopHead(SL* ps)
{
assert(ps);
assert(ps->size);
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
--ps->size;
}
3. 指定位置删除
cpp
void SLErase(SL* ps, int pos)
{
assert(ps->arr);
assert(pos <= ps->size - 1 && pos >= 0);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
七、时间复杂度总结
| 操作 | 时间复杂度 |
|---|---|
| 尾插 | O(1)(均摊) |
| 头插 | O(n) |
| 任意插入 | O(n) |
| 尾删 | O(1) |
| 头删 | O(n) |
| 任意删除 | O(n) |
| 随机访问 | O(1) |
八、常见面试问题
1. 顺序表 vs 链表
| 维度 | 顺序表 | 链表 |
|---|---|---|
| 内存 | 连续 | 离散 |
| 访问 | O(1) | O(n) |
| 插入删除 | O(n) | O(1)(已知位置) |
| cache友好 | 高 | 低 |
2. 为什么顺序表要扩容?
-
数组大小固定
-
插入时可能溢出
-
必须动态扩展
3. 扩容为什么不用 +1?
如果每次 +1:
-
插入 n 次 → realloc n 次
-
时间复杂度退化为 O(n²)
九、可以继续优化的点(进阶)
-
支持缩容(shrink)
-
增加查找接口
-
封装为泛型(void*)
-
增加迭代器设计(C++方向)
十、一句话总结(面试结尾)
顺序表本质是一个基于动态扩容策略的连续内存结构,通过"空间换时间"实现高效访问,但在插入删除上存在整体搬移的性能代价。
如果你下一步想进阶,我建议你做两件事:
-
把这个结构改写成 C++ 的 vector(带构造/析构/拷贝)
-
对比实现一个 单链表,把两者的适用场景彻底打透
这两步做完,你的数据结构基础就已经达到面试可用级别了。