为了能够实现顺序表这一个数据结构,小编是分别分为三个文件编写完成的。分别是一个头文件
(.h),一个实现文件(.c),一个测试文件(.c) 。
以下对这三个模块(头文件、顺序表实现文件、测试文件 )的代码,按功能模块、函数逻辑等进
行详细解释,帮助理解动态顺序表的完整实现:
头文件( SeqList.h )—— 接口定义与类型声明:
#include
#include
#include
// 定义动态顺序表存储的数据类型,这里用 int 举例,可按需修改
typedef int SLDataType;
// 定义顺序表结构体
typedef struct SeqList
{
SLDataType* arr; // 指向动态分配内存的指针,用于存储数据元素
int size; // 当前顺序表中有效数据元素的个数
int capacity; // 顺序表当前已分配的空间容量(能存储的最大元素个数)
}SL;
// 函数声明,对外提供操作顺序表的接口
// 打印顺序表中元素
void SLPrint(SL* ps);
// 初始化顺序表
void SLInit(SL* ps);
// 销毁顺序表(头文件中原本注释了,实际建议保留正确声明 )
void SLDestroy(SL* ps);
// 尾插:在顺序表末尾添加元素
void SLPushBack(SL* ps, SLDataType x);
// 头插:在顺序表开头添加元素
void SLPushFront(SL* ps, SLDataType x);
// 尾删:删除顺序表末尾元素
void SLPopBack(SL* ps);
// 头删:删除顺序表开头元素
void SLPopFront(SL* ps);
// 在指定位置 pos 之前插入数据(pos 从 0 开始计数 )
void SLInsert(SL* ps, int pos, SLDataType x);
// 删除指定位置 pos 的数据(pos 从 0 开始计数 )
void SLErase(SL* ps, int pos);
// 查找指定数据 x 在顺序表中的位置,找到返回下标,没找到返回 -1
int SLFind(SL* ps, SLDataType x);
作用与意义:
- 类型与结构定义:通过 typedef 定义了存储数据的类型 SLDataType 和顺序表结构体 SL ,让
代码后续修改数据类型更方便,也清晰描述了顺序表的组成(数据存储指针、有效元素个数、容量
)。
- 接口声明:把操作顺序表的函数以声明形式列出,相当于给其他文件(如测试文件)提供“使用手
册”,其他文件只要包含这个头文件,就能调用这些函数操作顺序表,实现了接口封装,隐藏了内
部实现细节。
顺序表实现文件( SeqList.c )—— 接口函数具体实现:
1. 初始化函数 SLInit:
1. 初始化函数 SLInit void SLInit(SL* ps) { ps->arr = NULL; // 初始化时,动态数组指针置空,还未分配内存 ps->size = 0; // 有效元素个数初始为 0 ps->capacity = 0; // 容量初始为 0 }
逻辑:
将顺序表的动态数组指针、有效元素个数、容量都初始化为初始状态,准备好后续使用。调
用这个函数后,顺序表就可以开始被操作(插入元素时会触发扩容分配内存 )。
2. 检查容量函数 SLCheckCapacity
void SLCheckCapacity(SL* ps) { // 当有效元素个数等于容量时,说明空间已满,需要扩容 if (ps->size == ps->capacity) { // 确定新容量:如果原来容量是 0 (刚初始化还没分配过空间), 就设为 4;否则容量翻倍 int NewCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; // 调用realloc扩容,realloc 会尝试在原内存地址后扩展空间,如果不行则重新找一块新内存并拷贝原数据 // 这里第二个参数是新容量乘以每个元素的大小,单位是字节 SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapacity * sizeof(SLDataType)); if (tmp == NULL) // 如果内存分配失败(如内存不足) { perror("realloc fail!"); // 打印错误信息 exit(1); // 直接终止程序,实际项目中可考虑更优雅的错误处理 } ps->arr = tmp; // 让顺序表的数组指针指向新分配的内存 ps->capacity = NewCapacity; // 更新容量为新容量 } }
逻辑:
作为顺序表插入操作的“幕后支撑”,在每次插入元素前(或需要时)检查空间是否足够。空
间不足时,通过 realloc 动态扩容,保证顺序表能继续存储新元素,是实现动态顺序表的关键
逻辑。
3. 打印函数 SLPrint
void SLPrint(SL* ps) { for (int i = 0;i < ps->size;i++) { printf("%d ", ps->arr[i]); // 逐个打印顺序表中的有效元素 } printf("\n"); // 打印完换行,让输出更整洁 }
逻辑:
遍历顺序表的有效元素(依据 size 确定遍历范围 ),逐个打印,方便查看顺序表当前存储
的数据内容。
4. 尾插函数 SLPushBack
void SLPushBack(SL* ps, SLDataType x) { assert(ps != NULL); // 断言检查,确保传入的顺序表指针不为空,否则程序崩溃提示 SLCheckCapacity(ps); // 先检查空间,不够就扩容 ps->arr[ps->size] = x; // 把新元素放到当前有效元素的末尾位置(下标为 size 处 ) ps->size++; // 有效元素个数加 1 }
逻辑:
在顺序表末尾添加元素,先通过 SLCheckCapacity 确保有空间,然后把元素放到合适位置,
更新有效元素个数,是最基础的插入操作之一。
5. 头插函数 SLPushFront
void SLPushFront(SL* ps, SLDataType x) { assert(ps != NULL); // 检查顺序表指针有效性 SLCheckCapacity(ps); // 检查空间 // 从后往前,把原有元素依次向后挪动一位,为新元素腾出开头位置 for (int i = ps->size;i > 0;i--) { ps->arr[i] = ps->arr[i - 1]; } ps->arr[0] = x; // 把新元素放到开头位置 ps->size++; // 有效元素个数加 1 }
逻辑:
要在顺序表开头插入元素,需要先把原有元素整体向后“挪位”,空出第一个位置放新元素,
操作过程中要注意循环的方向(从后往前挪,避免元素被覆盖 ),这也导致头插的时间复杂
度是 O(n) ( n 是当前有效元素个数 )。
6. 尾删函数 SLPopBack
void SLPopBack(SL* ps) { // 断言检查:顺序表指针不为空,且有效元素个数大于 0(否则没法删 ) assert(ps && ps->size); ps->size--; // 直接把有效元素个数减 1,逻辑上“删除”最后一个元素(后续插入会覆盖 ) }
逻辑:
实现简单的尾删,通过减少 size , 让后续操作不再访问最后一个元素,达到逻辑删除的效
果,时间复杂度是 O(1),因为不需要移动其他元素。
7. 头删函数 SLPopFront
void SLPopFront(SL* ps) { assert(ps && ps->size); // 检查有效性 // 把从第二个元素开始的所有元素,依次向前挪动一位,覆盖第一个元素,达到删除开头的效果 for (int i = 0;i < ps->size - 1;i++) { ps->arr[i] = ps->arr[i + 1]; } ps->size--; // 有效元素个数减 1 }
逻辑:
删除开头元素,需要把后面的元素依次向前移动,填补被删除的位置,时间复杂度是 O(n)
( n 是有效元素个数 ),因为要移动多个元素。
8. 指定位置pos插入函数 SLInsert
void SLInsert(SL* ps, int pos, SLDataType x) { assert(ps); // 检查顺序表指针 // 检查插入位置合法性:pos 要在 0 到 size 之间(可以插入到最后一个元素之后,相当于尾插 ) assert(pos >= 0 && pos <= ps->size); SLCheckCapacity(ps); // 检查空间 // 从 pos 位置开始,把后面的元素依次向后挪动一位,为新元素腾出位置 for(int i = ps->size;i > pos;i--) { ps->arr[i] = ps->arr[i - 1]; } ps->arr[pos] = x; // 在 pos 位置放入新元素 ps->size++; // 有效元素个数加 1 }
逻辑:
是比较通用的插入操作,可在任意合法位置(包括开头、中间、末尾 )插入元素。核心是通过循环移动元素腾出位置,时间复杂度取决于移动元素的数量,平均为 O(n) 。
9. 指定位置删除函数 SLErase
void SLErase(SL* ps, int pos) { assert(ps); // 检查顺序表指针 // 检查删除位置合法性:pos 要在 0 到 size - 1 之间(只能删除已有元素位置 ) assert(pos >= 0 && pos < ps->size); // 从 pos 位置开始,把后面的元素依次向前挪动一位,覆盖 pos 位置的元素,达到删除效果 for (int i = pos;i < ps->size - 1;i++) { ps->arr[i] = ps->arr[i + 1]; } ps->size--; // 有效元素个数减 1 }
逻辑:
删除指定位置的元素,通过移动后面的元素覆盖要删除的位置,时间复杂度同样取决于移动元素的数量,平均为 O(n) 。
10. 查找函数 SLFind
int SLFind(SL* ps, SLDataType x) { for (int i = 0;i < ps->size;i++) { if (ps->arr[i] == x) { return i; // 找到目标元素,返回其下标 } } return -1; // 遍历完没找到,返回 -1 标识 }
逻辑:
遍历顺序表的有效元素,逐个比较,找到目标元素就返回下标,没找到返回 -1 ,是基础的查找逻辑,时间复杂度为 O(n) ( n 是有效元素个数 )。
11. 销毁函数 SLDestroy
void SLDestroy(SL* ps) { assert(ps); // 检查顺序表指针 if (ps->arr != NULL) // 如果动态数组指针不为空(说明分配过内存 ) { free(ps->arr); // 释放动态分配的内存,避免内存泄漏 } ps->arr = NULL; // 指针置空,避免野指针 ps->size = ps->capacity = 0; // 重置有效元素个数和容量 }
逻辑:
使用完顺序表后,释放动态分配的内存,把顺序表相关成员重置,防止内存泄漏和野指针问题,是动态内存管理必不可少的操作。
测试文件( test.c ,代码里包含了测试逻辑 )—— 验证接口功能:
#include"SeqList.h"
// 注意:实际项目中不要这样包含 .c 文件!这里只是示例演示,正常应该通过头文件 + 编译链接的方式使用
#include"SeqList.c"
void test01()
{
SL sl; // 定义一个顺序表变量
SLInit(&sl); // 初始化顺序表
// 尾插测试
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPrint(&sl); // 打印:1 2 3 4
// 以下是被注释的其他测试代码,可按需取消注释测试对应功能
// SLPushBack(&sl, 5);
// SLPushFront(&sl, 1);
// SLPushFront(&sl, 2);
// SLPushFront(&sl, 3);
// SLPushFront(&sl, 4);//4 3 2 1
// SLPushFront(NULL, 1); // 触发断言失败,因为传了 NULL
// SLPopBack(&sl);
// SLPrint(&sl);
// SLPopBack(&sl);
// SLPrint(&sl);
// SLPopBack(&sl);
// SLPrint(&sl);
// SLPopBack(&sl);
// SLPrint(&sl);
// SLPopBack(&sl);
// SLPopFront(&sl);
// SLPrint(&sl);
// SLPopFront(&sl);
// SLPrint(&sl);
// SLPopFront(&sl);
// SLPrint(&sl);
// SLPopFront(&sl);
// SLPrint(&sl);
// SLPopFront(&sl);
// SLPrint(&sl);
// SLInsert(&sl, 4, 100);
// SLPrint(&sl);
// SLErase(&sl, 3);
// SLPrint(&sl);
// int find = SLFind(&sl, 22222);
// if (find != -1)
// {
// printf(" \n");
// }
// else {
// printf(" \n");
// }
SLDestroy(&sl); // 销毁顺序表,释放内存
}
int main()
{
test01(); // 调用测试函数
return 0;
}
- test01 函数:定义了一系列对顺序表的操作,包括初始化、尾插、打印、其他注释掉的操作(头
插、尾删、头删、指定位置插入删除、查找等 ),最后销毁顺序表。通过这些操作,可以验证顺
序表各个接口函数是否能正常工作,是测试代码常用的“用例驱动”写法。
总结来说,这三个模块分工明确:
- 头文件 SeqList.h 负责定义类型和接口,是“规范”;
- 实现文件 SeqList.c 负责实现接口逻辑,是“具体干活的”;
- 测试文件负责验证接口功能,是“质检员” 。
通过这样的分层设计,代码的可读性、可维护性、可扩展性都会更好,也符合软件工程中“高内
聚、低耦合”的思想。
这个测试文件部分一定要注意。在上面的两个文件中都是分别按照顺序所写的函数。但在这个测试
文件中,这些函数的顺序可能是混用的。我可能在尾插函数使用后再使用打印函数,也可能在头插
函数之后使用打印函数,也可能在使用删除某个位置的函数后使用打印函数。根据不同的场景有不
同的用法,所以一定要理解在头文件和实现文件中的代码内容。只有将这些函数熟练掌握,才能够
在测试文件中灵活运用。
以上三个文件就是关于小编在实现顺序表时所写的全部代码。小编所用的是VS2022。还有小编想
要强调的是小编写的代码也只是小编对于这个顺序表的理解,当然每个人有每个人的理解和代码写
法。小编也只是将小编所写的分享出来,能够给大家提供参考,希望对大家能有帮助。再次感谢大
家的观看。接下来小编还会分享更多的数据结构内容,喜欢的可以给小编留个关注,谢谢大家!