在数据结构的学习中,线性表是最基础也是最重要的结构之一,而顺序表作为线性表的两种存储实现方式(顺序存储、链式存储)之一,因其随机访问效率高的特点,在实际开发中有着广泛的应用。本文将从顺序表的基本概念出发,手把手教你用 C 语言实现顺序表的初始化、插入、删除、查找等核心操作,并深入分析其优缺点与适用场景,帮你彻底掌握这一基础数据结构。
一、什么是顺序表?
顺序表是用一段物理地址连续的存储单元依次存储线性表中数据元素的存储结构,简单来说,就是用 “数组” 来实现线性表,但相比普通数组,顺序表会额外记录 “当前元素个数” 和 “数组最大容量”,以便更好地管理数据(比如判断是否满了、是否为空)。
顺序表的核心特点:
- 物理存储连续:数据元素在内存中是紧挨着存放的,这是顺序表最核心的特征;
- 随机访问:由于地址连续,可通过公式 a[i] = 基地址 + i * 元素大小 直接访问第 i 个元素,时间复杂度为 O(1);
- 元素顺序存储:逻辑上的 “前后关系” 与物理上的 “存储顺序” 完全一致;
- 容量固定(静态)或动态扩容:顺序表分为 “静态顺序表”(数组大小固定)和 “动态顺序表”(数组大小可动态调整),实际开发中动态顺序表更常用。
二、顺序表的结构设计
在 C 语言中,我们用结构体来封装顺序表,因为顺序表需要包含三个核心信息:
- 存储数据的数组(动态顺序表用指针指向堆区内存);
- 顺序表当前的元素个数(len);
- 顺序表的最大容量(cap)。
1. 静态顺序表(了解即可)
静态顺序表的数组大小是固定的,缺点是容量不足时无法扩展,实际开发中很少使用。
#include <stdio.h>
// 定义静态顺序表的最大容量
#define MAX_SIZE 10
// 静态顺序表结构体
typedef struct
{
int data[MAX_SIZE]; // 存储数据的数组
int len; // 当前元素个数(初始为0)
} StaticSeqList;
2. 动态顺序表(重点掌握)
动态顺序表的数组是通过malloc在堆区申请的内存,当容量不足时,可以通过realloc扩容,灵活性更高,是实际开发中的首选。
#include <stdio.h>
#include <stdlib.h> // 包含malloc、realloc、free函数
// 动态顺序表结构体
typedef struct
{
int *data; // 指向堆区数组的指针
int len; // 当前元素个数(初始为0)
int cap; // 当前最大容量(初始可自定义,比如4)
} DynamicSeqList;
三、动态顺序表的核心操作实现
接下来我们逐一实现动态顺序表的 “初始化、插入、删除、查找、修改、销毁” 等操作,每个操作都附带详细注释,确保新手也能看懂。
1. 初始化顺序表
初始化的核心是:给data指针申请初始内存,设置初始容量cap,并将当前元素个数len设为 0。
// 初始化动态顺序表,参数为顺序表指针,init_cap为初始容量
void SeqListInit(DynamicSeqList *list, int init_cap)
{
// 校验初始容量(不能小于1)
if (init_cap < 1)
{
printf("初始容量不能小于1,已默认设置为4\n");
init_cap = 4;
}
// 给data申请堆区内存(大小 = 初始容量 * 元素类型大小)
list->data = (int *)malloc(init_cap * sizeof(int));
// 校验内存是否申请成功(malloc失败会返回NULL)
if (list->data == NULL)
{
printf("内存申请失败!\n");
exit(1); // 终止程序(实际开发可优化为返回错误码)
}
list->len = 0; // 初始元素个数为0
list->cap = init_cap; // 初始容量为传入的init_cap
printf("顺序表初始化成功!初始容量:%d\n", init_cap);
}
2. 扩容操作(内部辅助函数)
当插入元素时,如果len == cap(顺序表已满),就需要扩容。通常扩容策略是 “扩大为原来的 2 倍”(兼顾效率和内存利用率)。
// 顺序表扩容(内部使用,用户无需直接调用)
static void SeqListExpand(DynamicSeqList *list)
{
// 扩容为原来的2倍(若初始容量为0,直接扩为4)
int new_cap = list->cap == 0 ? 4 : list->cap * 2;
// 重新申请内存(realloc会保留原数据,并返回新内存地址)
int *new_data = (int *)realloc(list->data, new_cap * sizeof(int));
// 校验扩容是否成功
if (new_data == NULL)
{
printf("扩容失败!\n");
exit(1);
}
// 更新顺序表的指针、容量
list->data = new_data;
list->cap = new_cap;
printf("顺序表扩容成功!新容量:%d\n", new_cap);
}
3. 插入元素(按位置插入)
插入操作分为 “头部插入、中间插入、尾部插入”,核心逻辑是:
- 校验插入位置是否合法(不能小于 0,也不能大于当前元素个数len);
- 若顺序表已满,先扩容;
- 将插入位置及之后的元素 “向后移动 1 位”,腾出插入空间;
- 插入新元素,len加 1。
// 按位置插入元素:在index位置插入value(index从0开始)
void SeqListInsert(DynamicSeqList *list, int index, int value)
{
// 1. 校验插入位置合法性
if (index < 0 || index > list->len)
{
printf("插入位置非法!当前元素个数:%d,合法位置:0~%d\n", list->len, list->len);
return;
}
// 2. 若顺序表已满,先扩容
if (list->len == list->cap)
{
SeqListExpand(list);
}
// 3. 将index及之后的元素向后移动1位(从最后一个元素开始移)
for (int i = list->len; i > index; i--)
{
list->data[i] = list->data[i - 1];
}
// 4. 插入新元素,更新元素个数
list->data[index] = value;
list->len++;
printf("在位置%d插入元素%d成功!当前元素个数:%d\n", index, value, list->len);
}
4. 删除元素(按位置删除)
删除操作的核心逻辑是:
- 校验删除位置是否合法(不能小于 0,也不能大于等于len);
- 将删除位置之后的元素 “向前移动 1 位”,覆盖要删除的元素;
- len减 1(无需手动释放内存,因为后续插入会覆盖)。
// 按位置删除元素:删除index位置的元素,返回删除的元素值
int SeqListDelete(DynamicSeqList *list, int index)
{
// 1. 校验顺序表是否为空
if (list->len == 0)
{
printf("顺序表为空,无法删除!\n");
return -1; // 用-1表示删除失败(实际开发可优化为返回状态码)
}
// 2. 校验删除位置合法性
if (index < 0 || index >= list->len)
{
printf("删除位置非法!当前元素个数:%d,合法位置: 0~%d\n", list->len, list->len - 1);
return -1;
}
// 3. 记录要删除的元素值(用于返回)
int del_val = list->data[index];
// 4. 将index之后的元素向前移动1位(从index+1开始移)
for (int i = index; i < list->len - 1; i++)
{
list->data[i] = list->data[i + 1];
}
// 5. 更新元素个数
list->len--;
printf("删除位置%d的元素%d成功!当前元素个数:%d\n", index, del_val, list->len);
return del_val;
}
5. 查找元素(按值查找)
按值查找的核心是 “遍历数组”,找到第一个与目标值相等的元素,返回其位置;若未找到,返回 - 1。
// 按值查找元素:返回第一个值为value的元素位置,未找到返回-1
int SeqListFindByVal(DynamicSeqList *list, int value)
{
if (list->len == 0)
{
printf("顺序表为空,无法查找!\n");
return -1;
}
// 遍历数组查找
for (int i = 0; i < list->len; i++)
{
if (list->data[i] == value)
{
printf("找到元素%d,位置:%d\n", value, i);
return i;
}
}
printf("未找到元素%d\n", value);
return -1;
}
6. 修改元素(按位置修改)
修改操作很简单:校验位置合法性后,直接给目标位置赋值即可。
// 按位置修改元素:将index位置的元素改为new_val
void SeqListModify(DynamicSeqList *list, int index, int new_val)
{
if (list->len == 0)
{
printf("顺序表为空,无法修改!\n");
return;
}
if (index < 0 || index >= list->len)
{
printf("修改位置非法!\n");
return;
}
// 记录旧值(可选,用于打印提示)
int old_val = list->data[index];
list->data[index] = new_val;
printf("将位置%d的元素从%d修改为%d成功!\n", index, old_val, new_val);
}
7. 打印顺序表
遍历数组,打印所有元素,方便查看顺序表的当前状态。
// 打印顺序表的所有元素
void SeqListPrint(DynamicSeqList *list)
{
if (list->len == 0)
{
printf("顺序表为空!\n");
return;
}
printf("顺序表当前元素(共%d个):", list->len);
for (int i = 0; i < list->len; i++)
{
printf("%d ", list->data[i]);
}
printf("\n");
}
8. 销毁顺序表
由于顺序表的data指针指向堆区内存,若不手动释放,会造成内存泄漏,因此必须在使用完顺序表后销毁。
// 销毁顺序表:释放堆区内存,重置结构体成员
void SeqListDestroy(DynamicSeqList *list)
{
// 释放data指向的堆区内存(若已释放,再次free不会报错,但建议先判断)
if (list->data != NULL)
{
free(list->data);
list->data = NULL; // 避免野指针
}
list->len = 0; // 重置元素个数
list->cap = 0; // 重置容量
printf("顺序表销毁成功!\n");
}
四、完整测试代码
将上述操作组合起来,编写一个测试程序,验证顺序表的所有功能是否正常。
int main()
{
DynamicSeqList list; // 定义一个动态顺序表
// 1. 初始化顺序表(初始容量为3)
SeqListInit(&list, 3);
// 2. 插入元素(尾部插入2个,中间插入1个)
SeqListInsert(&list, 0, 10); // 位置0插入10(头部插入)
SeqListInsert(&list, 1, 20); // 位置1插入20(中间插入)
SeqListInsert(&list, 2, 30); // 位置2插入30(尾部插入)
SeqListInsert(&list, 3, 40); // 此时len=3,cap=3,会触发扩容(扩为6)
SeqListPrint(&list); // 预期输出:10 20 30 40
// 3. 查找元素
SeqListFindByVal(&list, 20); // 预期找到位置1
SeqListFindByVal(&list, 50); // 预期未找到
// 4. 修改元素
SeqListModify(&list, 1, 200); // 将位置1的20改为200
SeqListPrint(&list); // 预期输出:10 200 30 40
// 5. 删除元素
SeqListDelete(&list, 2); // 删除位置2的30
SeqListPrint(&list); // 预期输出:10 200 40
// 6. 销毁顺序表
SeqListDestroy(&list);
SeqListPrint(&list); // 销毁后为空
return 0;
}
测试结果输出:
顺序表初始化成功!初始容量:3
在位置0插入元素10成功!当前元素个数:1
在位置1插入元素20成功!当前元素个数:2
在位置2插入元素30成功!当前元素个数:3
顺序表扩容成功!新容量:6
在位置3插入元素40成功!当前元素个数:4
顺序表当前元素(共4个):10 20 30 40
找到元素20,位置:1
未找到元素50
将位置1的元素从20修改为200成功!
顺序表当前元素(共4个):10 200 30 40
删除位置2的元素30成功!当前元素个数:3
顺序表当前元素(共3个):10 200 40
顺序表销毁成功!
顺序表为空!
五、顺序表的优缺点与适用场景
优点:
- 随机访问效率高:通过下标直接访问元素,时间复杂度 O (1),适合频繁查询的场景;
- 存储密度高:无需额外存储指针(对比链表),内存利用率高;
- 实现简单:基于数组,逻辑直观,代码容易理解。
缺点:
- 插入 / 删除效率低:若在头部或中间插入 / 删除,需要移动大量元素,时间复杂度 O (n);
- 动态扩容有开销:realloc可能需要申请新内存并拷贝原数据,扩容过程有时间成本;
- 容量固定(静态)或可能浪费内存(动态):静态顺序表容量不足时无法扩展,动态顺序表扩容后可能有闲置内存。
适用场景:
- 频繁进行查询操作,插入 / 删除操作较少的场景;
- 已知数据规模,或数据规模增长较为稳定的场景;
- 对内存利用率要求较高,且不需要频繁调整数据位置的场景。
六、总结
本文详细讲解了 C 语言动态顺序表的原理、结构设计和核心操作实现,从初始化到销毁,每个步骤都附带了完整代码和注释。顺序表作为线性表的基础实现,是数据结构学习的重要起点,掌握它不仅能帮助你理解 “随机访问” 的优势,也能为后续学习链表、栈、队列等数据结构打下坚实基础。
如果你在实践中遇到问题(比如内存泄漏、扩容失败),可以通过printf打印中间变量(如len、cap、data地址)来排查,也可以在评论区留言讨论。下一篇文章,我们将对比顺序表和链表的差异,带你深入理解两种线性表的适用场景!
C语言动态顺序表详解

被折叠的 条评论
为什么被折叠?



