线性表之顺序表入门:顺序表从原理到实现「增删改查」

对于编程初学者来说,线性表是数据结构的入门基石,而顺序表作为线性表最基础的实现形式,直接依托数组实现,理解它的原理和操作,能帮我们建立对数据结构 "增删改查" 核心逻辑的基本认知。本文将从顺序表的基本概念出发,结合完整的 C 语言代码,手把手讲解顺序表的初始化、追加、插入、删除、遍历和查找操作,所有代码均做了详细注释,零基础也能轻松看懂。

一、什么是顺序表?

线性表是由n 个具有相同类型的数据元素 组成的有限序列,特点是元素之间存在 "一对一" 的线性逻辑关系。而顺序表 是线性表的顺序存储结构,简单来说,就是用一段连续的内存空间(数组)来存储线性表的元素,并通过一个变量记录当前表中的有效元素个数。

顺序表的核心特点

  1. 元素存储在连续的内存地址中,支持随机访问(通过下标直接找到元素);
  2. 插入、删除操作需要移动元素,因为要保证内存的连续性;
  3. 需提前定义最大容量,属于静态顺序表(初学者先掌握静态,后续可拓展动态顺序表)。

顺序表的结构组成

顺序表主要包含两部分:

  1. 数据数组:用于存储实际的元素,提前定义最大容量;
  2. 有效长度:记录当前表中实际存储的元素个数,区别于数组的最大容量(避免访问无意义的空值)。

用 C 语言的结构体可以完美定义顺序表,这也是我们后续代码的核心基础:

cpp 复制代码
// 定义顺序表的最大容量
#define MAXSIZE 100
// 定义元素类型(方便后续修改,比如改成char、float)
typedef int ElemType;
// 顺序表结构体定义
typedef struct {
    ElemType data[MAXSIZE]; // 存储数据的连续数组
    int length;             // 顺序表当前的有效元素个数
} SeqList;

二、顺序表的核心操作:从原理到代码

顺序表的所有操作都是围绕数组有效长度 展开的,核心操作包括:初始化、尾部追加元素、遍历、指定位置插入、指定位置删除、元素查找。接下来我们逐个讲解,每个操作都包含原理说明带详细注释的代码,并说明关键注意事项。

操作 1:初始化顺序表

原理 :顺序表的初始化就是将其有效长度置为 0,表示这是一个空表,此时数组中还没有存储任何有效元素。关键 :因为要修改原顺序表的内容,所以需要传址调用(传递顺序表的指针),而不是传值调用(仅修改副本,原表无变化)。

cpp 复制代码
// 初始化顺序表
void initList(SeqList *L) {
    // L是指向顺序表的指针,通过->访问结构体成员,有效长度置0表示空表
    L->length = 0;
}

操作 2:尾部追加元素

原理 :在顺序表的最后一个有效元素后添加新元素,相当于直接给数组的length下标赋值(因为数组下标从 0 开始,length正好是最后一个有效元素的下一个位置),然后将有效长度加 1。关键 :操作前要做判满检查,如果有效长度等于数组最大容量,说明表已满,无法再添加元素。

cpp 复制代码
// 在尾部添加元素,成功返回1,失败返回0
int appendElem(SeqList *L, ElemType e) {
    // 判满检查:有效长度达到最大容量,无法追加
    if (L->length >= MAXSIZE) {
        printf("顺序表已满\n");
        return 0; // 0表示操作失败
    }
    // 核心赋值:新元素写入数组的length下标(最后一个有效元素的下一位)
    L->data[L->length] = e;
    L->length++; // 有效长度加1,完成追加
    return 1; // 1表示操作成功
}

操作 3:遍历顺序表

原理 :遍历就是依次打印顺序表中的所有有效元素,只需通过 for 循环遍历数组的0 ~ length-1下标(仅遍历有效元素,跳过未使用的数组空间)。关键 :循环条件是i < L->length,而不是i < MAXSIZE,避免打印无意义的空值。

cpp 复制代码
// 遍历并打印顺序表所有有效元素
void listElem(SeqList *L){
    // 遍历从下标0到有效长度length-1的所有元素
    for (int i = 0; i < L->length; i++)
    {
        // 打印当前元素,空格分隔,优化输出格式
        printf("%d ", L->data[i]);
    }
    // 遍历结束后换行,避免后续输出连在一起
    printf("\n");
}

操作 4:指定位置插入元素

原理 :在顺序表的指定逻辑位置插入元素,需要先为新元素腾出位置 (将插入位置及后面的元素依次向后移动一位),再将新元素写入腾出的位置,最后将有效长度加 1。关键注意事项

  1. 先判满:表满则无法插入;
  2. 位置合法性检查:插入的逻辑位置需在1 ~ length之间(初学者先按此限制,后续可拓展到1 ~ length+1支持表尾插入);
  3. 元素后移要从后往前,避免覆盖未移动的元素。
cpp 复制代码
// 向指定逻辑位置插入元素,成功返回1,失败返回0
// pos:插入位置(从1开始计数,符合用户使用习惯)
// e:待插入的元素值
int insertElem(SeqList *L, int pos, ElemType e)
{
    // 1. 边界检查:顺序表已满,无法插入
    if (L->length >= MAXSIZE)
    {
        printf("表已经满了\n");
        return 0;
    }

    // 2. 合法性检查:插入位置需在1~当前有效长度之间
    if (pos < 1 || pos > L->length)
    {
        printf("插入位置错误\n");
        return 0;
    }

    // 3. 核心插入逻辑:从后往前移动元素,为新元素腾出位置
    if (pos <= L->length)
    {
        // 从最后一个有效元素开始,到插入位置的数组下标,依次后移
        // pos是逻辑位置,转数组下标需-1
        for (int i = L->length - 1; i >= pos - 1; i--)
        {
            L->data[i + 1] = L->data[i]; // 元素后移一位
        }
        L->data[pos - 1] = e; // 新元素写入腾出的位置
        L->length++;          // 有效长度加1,完成插入
    }

    return 1;
}

操作 5:指定位置删除元素

原理 :删除顺序表指定逻辑位置的元素,需要先保存被删除的元素值(方便后续查看),再将删除位置后面的元素依次向前移动一位 (覆盖被删除的元素),最后将有效长度减 1。关键注意事项

  1. 先判空:空表没有元素可删;
  2. 位置合法性检查:删除的逻辑位置需在1 ~ length之间;
  3. 元素前移要从前往后,仅当删除的不是最后一个元素时,才需要移动。
cpp 复制代码
// 删除指定逻辑位置的元素,成功返回1,失败返回0
// pos:删除位置(从1开始计数)
// *e:指针,用于保存被删除的元素值(带出删除值)
int deleteElem(SeqList *L, int pos, ElemType *e)
{
    // 检查1:顺序表为空,无元素可删
    if (L->length == 0)
    {
        printf("空表\n");
        return 0;
    }

    // 检查2:删除位置不合法,需在1~当前有效长度之间
    if (pos < 1 || pos > L->length)
    {
        printf("删除数据位置有误\n");
        return 0;
    }

    // 保存被删除元素的值:逻辑位置转数组下标需-1
    // 通过指针解引用,将值存入外部变量,方便后续查看
    *e = L->data[pos-1];

    // 如果删除的不是最后一个元素,需要将后面的元素依次前移
    if (pos < L->length)
    {
        // 从删除位置的数组下标开始,到最后一个有效元素,依次前移
        for (int i = pos; i < L->length; i++)
        {
            L->data[i-1] = L->data[i]; // 元素前移一位,覆盖被删除元素
        }
    }

    L->length--; // 有效长度减1,完成删除
    return 1;
}

操作 6:查找指定元素

原理 :顺序表的查找是顺序查找 (遍历),依次比较数组中的有效元素与目标元素,找到则返回其逻辑位置 (从 1 开始,符合用户习惯),未找到则返回 0(0 是无效位置,用于区分 "未找到")。关键:只需遍历有效元素,找到后立即返回,无需遍历整个数组,提升效率。

cpp 复制代码
// 查找指定元素,找到返回逻辑位置(从1开始),未找到返回0
// e:要查找的目标元素值
int findElem(SeqList *L, ElemType e) {
    // 遍历所有有效元素,仅检查0~length-1下标
    for (int i = 0; i < L->length; i++) {
        // 核心判断:当前元素与目标元素是否匹配
        if (L->data[i] == e) {
            // 找到匹配元素,数组下标转逻辑位置(+1)并返回
            return i + 1;
        }
    }
    // 遍历结束未找到,返回0表示查找失败
    return 0;
}

三、顺序表完整测试:所有操作联调

上面的每个操作都是独立的,接下来我们通过主函数将所有操作联调,完成一次完整的顺序表测试,看看每个操作的实际执行效果。测试流程为:初始化→打印初始状态→尾部追加元素→遍历→指定位置插入→遍历→指定位置删除→遍历→查找元素

cpp 复制代码
// 主函数:测试顺序表的所有核心操作
int main() {
    // 定义一个顺序表变量,在栈上开辟连续内存(data[100]+length,共404字节)
    SeqList list;
    // 初始化顺序表
    initList(&list);
    
    // 打印初始化后的状态
    printf("初始化后顺序表长度:%d\n", list.length);
    printf("顺序表数据区占用内存:%zu 字节\n", sizeof(list.data));
   
    // 尾部追加5个元素:88、67、40、8、23
    appendElem(&list, 88);
    appendElem(&list, 67);
    appendElem(&list, 40);
    appendElem(&list, 8);
    appendElem(&list, 23);
    printf("尾部追加元素后:");
    listElem(&list);

    // 在第1个位置插入元素18
    insertElem(&list, 1, 18);
    printf("第1位插入18后:");
    listElem(&list);

    // 定义变量保存被删除的元素值
    ElemType delData;
    // 删除第2个位置的元素
    deleteElem(&list, 2, &delData);
    printf("被删除的数据为:%d\n", delData);
    printf("删除第2位元素后:");
    listElem(&list);
    
    // 查找元素40,打印其逻辑位置
    printf("元素40在顺序表的第%d个位置\n", findElem(&list, 40));

    return 0;
}

测试运行结果

复制代码
初始化后顺序表长度:0
顺序表数据区占用内存:400 字节
尾部追加元素后:88 67 40 8 23 
第1位插入18后:18 88 67 40 8 23 
被删除的数据为:88
删除第2位元素后:18 67 40 8 23 
元素40在顺序表的第3个位置

从结果可以清晰看到每个操作对顺序表的修改,完全符合我们的预期,说明所有操作的逻辑都是正确的。

四、顺序表的优缺点与适用场景

学完顺序表的实现,我们需要明白它的适用场景,这也是数据结构学习的核心 ------根据需求选择合适的结构

优点

  1. 随机访问性好:通过数组下标可以直接访问任意元素,时间复杂度为 O (1);
  2. 存储密度高:元素之间紧密存储,没有额外的内存开销(仅需一个变量记录长度);
  3. 实现简单:依托数组实现,代码逻辑直观,适合初学者入门。

缺点

  1. 静态顺序表容量固定:提前定义最大容量,若容量过小会出现溢出现象,过大则造成内存浪费(可通过动态顺序表解决,后续拓展);
  2. 插入 / 删除效率低:插入和删除需要移动大量元素,最坏情况下时间复杂度为 O (n)(n 为有效元素个数);
  3. 内存空间连续:对内存的要求较高,需要分配一段连续的内存地址。

适用场景

  1. 元素个数固定,无需频繁插入、删除操作;
  2. 需要频繁进行随机访问(根据位置查找元素)的场景;
  3. 对内存空间使用效率要求较高的场景。

五、初学者学习要点总结

  1. 区分 "逻辑位置" 和 "数组下标":用户习惯从 1 开始计数(逻辑位置),而数组下标从 0 开始,所有操作都需要做好二者的转换(±1),这是顺序表代码的核心易错点;
  2. 理解 "传址调用" 的意义:修改顺序表的操作(初始化、追加、插入、删除)都需要传递指针,因为要直接修改原表的内容,而遍历、查找仅需访问表,可传值也可传址(传址更节省内存);
  3. 牢记 "边界检查":所有修改操作前都要做合法性检查(判满、判空、位置合法),这是保证程序健壮性的关键,避免数组越界等错误;
  4. 理解元素移动的方向 :插入时元素从后往前 移,避免覆盖;删除时元素从前往后移,仅需移动删除位置后的元素。

六、后续拓展方向

本文实现的是静态顺序表,初学者掌握后可以继续学习以下内容,深化对顺序表的理解:

  1. 动态顺序表:通过动态内存分配(malloc、realloc)实现容量的动态扩容,解决静态顺序表容量固定的问题;
  2. 顺序表的修改操作:根据位置或元素值修改顺序表中的元素;
  3. 有序顺序表:实现有序顺序表的插入、查找(可使用二分查找,提升查找效率);
  4. 对比链表:学习线性表的另一种实现形式 ------ 链表,对比顺序表和链表的优缺点,理解 "连续存储" 和 "链式存储" 的本质区别。

写在最后

顺序表作为数据结构的入门内容,看似简单,但其中包含的 "存储结构""增删改查""边界检查""传址 / 传值" 等思想,会贯穿整个数据结构的学习过程。初学者一定要亲手敲一遍代码,运行并调试,理解每一行代码的意义,而不是死记硬背。只有打好这个基础,后续学习链表、栈、队列等更复杂的数据结构时,才能事半功倍。

希望本文能帮你轻松入门顺序表,迈出数据结构学习的第一步!

相关推荐
I_LPL1 小时前
day52 代码随想录算法训练营 图论专题6
java·数据结构·算法·图论
lxl13072 小时前
C++算法(11)字符串
开发语言·c++·算法
passxgx2 小时前
12.3 多维高斯分布与加权最小二乘法
线性代数·算法·最小二乘法
少许极端2 小时前
算法奇妙屋(三十)-递归、回溯与剪枝的综合问题 3
算法·深度优先·剪枝·数独·n皇后
WBluuue2 小时前
数据结构与算法:01分数规划
c++·算法
小鸡吃米…2 小时前
Python线程同步
开发语言·数据结构·python
七七肆十九2 小时前
PTA 习题9-1 时间换算
c语言·算法
XW01059992 小时前
5-6统计工龄
数据结构·python·算法
EQUINOX12 小时前
倍增优化dp,P10976 统计重复个数
算法·数学建模·动态规划