顺序表实战:从定义到增删改查
大家好,我是你们的嵌入式老司机。今天咱们来聊聊顺序表——这个数据结构里最基础、最实在的东西。
说实话,我刚开始学C语言那会儿,觉得数组就够用了,搞什么顺序表?后来做项目才发现,光用数组写代码,就像用螺丝刀拧所有螺丝——能拧,但效率低、容易滑丝。顺序表说白了,就是给数组穿上一件「智能外套」,让它知道自己有多大、装了多少东西、还能装多少。
顺序表的定义与初始化
先看定义。顺序表本质上就是数组,但多了两个关键信息:当前长度和总容量。我习惯这么定义:
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE]; // 存储数据的数组
int length; // 当前元素个数
} SeqList;
这里有个细节:length 表示实际存了多少个元素,不是数组下标。比如你插入了3个元素,length就是3,不是2。嗯,这个坑我踩过——当年写代码时把length当下标用,结果数组越界,程序跑飞了。
初始化就简单了:
void InitList(SeqList *L) {
L->length = 0;
// 数据区不用清零,因为length控制了有效范围
}
为什么不把data全清零?我个人习惯是不做多余操作。你想想看,如果MAX_SIZE是1000,每次初始化都清零,浪费CPU周期。嵌入式系统里,能省一点是一点。
顺序表的插入操作
插入是顺序表的核心操作。说白了,就是在指定位置塞一个元素,后面的元素统统往后挪。
int Insert(SeqList *L, int pos, int value) {
// 1. 检查表是否已满
if (L->length >= MAX_SIZE) {
return -1; // 表满了
}
// 2. 检查位置是否合法(0到length之间)
if (pos < 0 || pos > L->length) {
return -1; // 位置不合法
}
// 3. 从后往前挪元素
for (int i = L->length; i > pos; i--) {
L->data[i] = L->data[i-1];
}
// 4. 插入新元素
L->data[pos] = value;
L->length++;
return 0;
}
注意看那个for循环,是从后往前挪的。为什么?如果你从前往后挪,后面的数据会被覆盖掉。我曾经见过一个新手同事,从前往后挪,结果整个数组变成了同一个数——调试了一下午才发现。
顺序表的删除操作
删除和插入是镜像操作——把元素删掉,后面的往前挪。
int Delete(SeqList *L, int pos) {
// 1. 检查表是否为空
if (L->length == 0) {
return -1;
}
// 2. 检查位置是否合法
if (pos < 0 || pos >= L->length) {
return -1;
}
// 3. 从前往后挪元素
for (int i = pos; i < L->length - 1; i++) {
L->data[i] = L->data[i+1];
}
L->length--;
return 0;
}
这里挪元素的方向和插入相反,是从前往后。你想想看,删掉第pos个元素后,后面的元素要补上来,自然是从pos+1开始往前挪。
我记得有一次做数据采集系统,每秒要删除和插入上千次数据。当时没注意删除操作的效率,结果CPU占用率飙到90%。后来优化了一下——批量删除,一次挪多个位置,性能立马好了。
顺序表的查找与修改
查找分两种:按位置查找和按值查找。按位置查找最简单:
int GetElem(SeqList *L, int pos) {
if (pos < 0 || pos >= L->length) {
return -1; // 位置不合法
}
return L->data[pos];
}
按值查找需要遍历:
int LocateElem(SeqList *L, int value) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == value) {
return i; // 返回位置
}
}
return -1; // 没找到
}
修改操作其实就是「查找+赋值」:
int Modify(SeqList *L, int pos, int newValue) {
if (pos < 0 || pos >= L->length) {
return -1;
}
L->data[pos] = newValue;
return 0;
}
这里有个细节:修改操作不需要挪动元素,时间复杂度是O(1)。这是顺序表最大的优势——随机访问快。链表就没这么幸运了,改个元素得从头遍历。
- 插入/删除:需要挪动元素,时间复杂度O(n)
- 按位置查找/修改:直接通过下标访问,时间复杂度O(1)
- 按值查找:需要遍历,时间复杂度O(n)
知识体系总览
下面这张图把顺序表的操作串起来了,我画的时候特意把「增删改查」四个核心操作放在中间,方便你理解它们之间的关系:
实战中的避坑指南
最后分享几个我踩过的坑:
- 忘记更新length:插入后length不加,删除后length不减。结果就是数据混乱,查半天查不出来。
- 位置参数搞混:用户传的是逻辑位置(第几个元素)还是数组下标?我习惯统一用下标,从0开始,并在接口文档里写清楚。
- 边界条件遗漏:插入到末尾(pos == length)和删除最后一个元素(pos == length-1)是特殊情况,要单独测试。
- 返回值不检查:调用插入函数后不检查返回值,结果插入失败还不知道,继续用错误的数据往下算。
我曾经在一个通信协议栈项目里,因为没检查插入返回值,导致数据包顺序错乱,整个系统重启了三次才定位到问题。从那以后,我写代码必检查返回值,成了强迫症。
好了,顺序表的内容就聊到这儿。记住一句话:顺序表是数组的「升级版」,它帮你管理了长度和容量,让你更专注于业务逻辑。下一节咱们聊链表,那又是另一番天地了。