顺序表实战:从定义到增删改查

大家好,我是你们的嵌入式老司机。今天咱们来聊聊顺序表——这个数据结构里最基础、最实在的东西。

说实话,我刚开始学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周期。嵌入式系统里,能省一点是一点。

小技巧: 初始化时只置length为0,后续插入时直接覆盖数据。这样既快又干净。

顺序表的插入操作

插入是顺序表的核心操作。说白了,就是在指定位置塞一个元素,后面的元素统统往后挪。

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循环,是从后往前挪的。为什么?如果你从前往后挪,后面的数据会被覆盖掉。我曾经见过一个新手同事,从前往后挪,结果整个数组变成了同一个数——调试了一下午才发现。

避坑指南: 插入时一定要检查位置合法性。我曾经在项目里忘了检查pos > length,结果用户插入了空位,导致中间出现「空洞」,后续查找全乱套了。

顺序表的删除操作

删除和插入是镜像操作——把元素删掉,后面的往前挪。

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-- 按位置:O(1)直接访问 按值:遍历O(n) 返回位置或-1 检查位置合法性 直接赋值 时间复杂度O(1) 核心:插入/删除需挪动元素(O(n)),查找/修改可直接访问(O(1)) 适用于频繁查找、少量插入删除的场景

实战中的避坑指南

最后分享几个我踩过的坑:

  • 忘记更新length:插入后length不加,删除后length不减。结果就是数据混乱,查半天查不出来。
  • 位置参数搞混:用户传的是逻辑位置(第几个元素)还是数组下标?我习惯统一用下标,从0开始,并在接口文档里写清楚。
  • 边界条件遗漏:插入到末尾(pos == length)和删除最后一个元素(pos == length-1)是特殊情况,要单独测试。
  • 返回值不检查:调用插入函数后不检查返回值,结果插入失败还不知道,继续用错误的数据往下算。

我曾经在一个通信协议栈项目里,因为没检查插入返回值,导致数据包顺序错乱,整个系统重启了三次才定位到问题。从那以后,我写代码必检查返回值,成了强迫症。

好了,顺序表的内容就聊到这儿。记住一句话:顺序表是数组的「升级版」,它帮你管理了长度和容量,让你更专注于业务逻辑。下一节咱们聊链表,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321