对于编程初学者来说,线性表是数据结构的入门基石,而顺序表作为线性表最基础的实现形式,直接依托数组实现,理解它的原理和操作,能帮我们建立对数据结构 "增删改查" 核心逻辑的基本认知。本文将从顺序表的基本概念出发,结合完整的 C 语言代码,手把手讲解顺序表的初始化、追加、插入、删除、遍历和查找操作,所有代码均做了详细注释,零基础也能轻松看懂。
一、什么是顺序表?
线性表是由n 个具有相同类型的数据元素 组成的有限序列,特点是元素之间存在 "一对一" 的线性逻辑关系。而顺序表 是线性表的顺序存储结构,简单来说,就是用一段连续的内存空间(数组)来存储线性表的元素,并通过一个变量记录当前表中的有效元素个数。
顺序表的核心特点
- 元素存储在连续的内存地址中,支持随机访问(通过下标直接找到元素);
- 插入、删除操作需要移动元素,因为要保证内存的连续性;
- 需提前定义最大容量,属于静态顺序表(初学者先掌握静态,后续可拓展动态顺序表)。
顺序表的结构组成
顺序表主要包含两部分:
- 数据数组:用于存储实际的元素,提前定义最大容量;
- 有效长度:记录当前表中实际存储的元素个数,区别于数组的最大容量(避免访问无意义的空值)。
用 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 ~ length之间(初学者先按此限制,后续可拓展到1 ~ length+1支持表尾插入); - 元素后移要从后往前,避免覆盖未移动的元素。
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 ~ length之间; - 元素前移要从前往后,仅当删除的不是最后一个元素时,才需要移动。
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个位置
从结果可以清晰看到每个操作对顺序表的修改,完全符合我们的预期,说明所有操作的逻辑都是正确的。
四、顺序表的优缺点与适用场景
学完顺序表的实现,我们需要明白它的适用场景,这也是数据结构学习的核心 ------根据需求选择合适的结构。
优点
- 随机访问性好:通过数组下标可以直接访问任意元素,时间复杂度为 O (1);
- 存储密度高:元素之间紧密存储,没有额外的内存开销(仅需一个变量记录长度);
- 实现简单:依托数组实现,代码逻辑直观,适合初学者入门。
缺点
- 静态顺序表容量固定:提前定义最大容量,若容量过小会出现溢出现象,过大则造成内存浪费(可通过动态顺序表解决,后续拓展);
- 插入 / 删除效率低:插入和删除需要移动大量元素,最坏情况下时间复杂度为 O (n)(n 为有效元素个数);
- 内存空间连续:对内存的要求较高,需要分配一段连续的内存地址。
适用场景
- 元素个数固定,无需频繁插入、删除操作;
- 需要频繁进行随机访问(根据位置查找元素)的场景;
- 对内存空间使用效率要求较高的场景。
五、初学者学习要点总结
- 区分 "逻辑位置" 和 "数组下标":用户习惯从 1 开始计数(逻辑位置),而数组下标从 0 开始,所有操作都需要做好二者的转换(±1),这是顺序表代码的核心易错点;
- 理解 "传址调用" 的意义:修改顺序表的操作(初始化、追加、插入、删除)都需要传递指针,因为要直接修改原表的内容,而遍历、查找仅需访问表,可传值也可传址(传址更节省内存);
- 牢记 "边界检查":所有修改操作前都要做合法性检查(判满、判空、位置合法),这是保证程序健壮性的关键,避免数组越界等错误;
- 理解元素移动的方向 :插入时元素从后往前 移,避免覆盖;删除时元素从前往后移,仅需移动删除位置后的元素。
六、后续拓展方向
本文实现的是静态顺序表,初学者掌握后可以继续学习以下内容,深化对顺序表的理解:
- 动态顺序表:通过动态内存分配(malloc、realloc)实现容量的动态扩容,解决静态顺序表容量固定的问题;
- 顺序表的修改操作:根据位置或元素值修改顺序表中的元素;
- 有序顺序表:实现有序顺序表的插入、查找(可使用二分查找,提升查找效率);
- 对比链表:学习线性表的另一种实现形式 ------ 链表,对比顺序表和链表的优缺点,理解 "连续存储" 和 "链式存储" 的本质区别。
写在最后
顺序表作为数据结构的入门内容,看似简单,但其中包含的 "存储结构""增删改查""边界检查""传址 / 传值" 等思想,会贯穿整个数据结构的学习过程。初学者一定要亲手敲一遍代码,运行并调试,理解每一行代码的意义,而不是死记硬背。只有打好这个基础,后续学习链表、栈、队列等更复杂的数据结构时,才能事半功倍。
希望本文能帮你轻松入门顺序表,迈出数据结构学习的第一步!