数据结构 - 线性表第二篇:动态顺序表进阶接口实现
前言接上一篇动态顺序表基础实现上次完成了初始化、销毁、自动扩容、头尾插入、头尾删除、通用打印核心基础操作算是搭好了动态顺序表的骨架。本篇继续完善顺序表进阶功能手写实现四个高频核心接口指定位置插入、指定位置删除、按值查找元素、按位置修改元素初学数据结构一定要慢下来每一个函数理解底层下标移动逻辑每一处边界条件自己琢磨为什么要这么限制比赶进度抄代码收获大得多。一、上篇简单回顾上一篇我们实现了顺序表最基础操作函数功能SLInit初始化空动态顺序表Destroy释放内存、销毁顺序表SLExp检测容量满了自动二倍扩容PushBack尾部插入数据PushFront头部插入数据SLDeleteBack头部删除数据SLDeleteBehind尾部删除数据Print通用格式化打印函数本篇新增 4 个进阶接口补齐顺序表所有基础能力新增函数功能时间复杂度SLInsert在指定下标位置插入元素O(n)SLErase删除指定下标位置的元素O(n)SLFind按数值查找返回对应下标O(n)SLModify修改指定下标位置的元素值O(1)二、进阶接口核心原理2.1 指定位置插入 SLInsert先校验下标合法性pos 0 pos size检测容量不够则自动扩容从顺序表末尾开始把pos及往后的元素整体后移一位空出pos下标放入新元素有效数据个数size2.2 指定位置删除 SLErase校验下标合法性pos 0 pos size把pos后面的元素整体向前覆盖一位有效数据个数size--只做逻辑删除不手动清空内存顺序表特性使然2.3 按值查找 SLFind遍历顺序表数组匹配到目标值直接返回下标遍历结束没找到返回-1交给调用者判断处理。2.4 按位置修改 SLModify校验下标合法后直接赋值覆盖对应位置元素即可随机访问特性时间复杂度 O (1)。三、手写代码常见 5 个致命 Bug新手必看Bug1指定插入 pos 断言范围写错错误写法assert(pos 0 pos ps-size);错误原因插入位置可以是末尾下标 size相当于尾插写成 size会导致无法在最后位置插入。正确写法assert(pos 0 pos ps-size);Bug2指定删除 pos 断言越界错误写法assert(pos 0 pos ps-size);错误原因删除必须操作已有有效元素最大下标只能是size-1等于 size 就是非法越界。正确写法assert(pos 0 pos ps-size);Bug3元素移动循环方向写反插入时如果从前往后移元素会覆盖还没移动的数据造成数据丢失。必须从后往前倒着移动元素。Bug4查找函数内部强行打印新手容易把printf写死在查找函数里导致只想获取下标、不想打印时无法复用。规范写法查找只返回下标打印交给测试层。Bug5修改函数缺少指针和下标校验不写assert防护传入空指针、非法下标直接野指针崩溃健壮性极差。四、完整多文件最终代码4.1 SeqList.h 头文件新增接口声明#pragma once #includestdio.h #includestdlib.h #includeassert.h // 通用打印格式宏切换类型只改这一处 #define SLTYPE_PRINT_FMT %d // 顺序表存储类型 typedef int SLType; // 动态顺序表结构体 typedef struct SeqList { SLType* a; int size; // 有效数据个数 int capacity; // 储存空间总容量 }SL; // 初始化和销毁 void SLInit(SL* ps); void Destroy(SL* ps); void Print(SL* ps); // 扩容检测 void SLExp(SL* ps); // 头部操作 void PushFront(SL* ps, SLType x); // 头部插入 void SLPopFront(SL* ps); // 头部删除 // 尾部操作 void PushBack(SL* ps, SLType x); // 尾部插入 void SLPopBack(SL* ps); // 尾部删除 // 进阶接口新增 int SLFind(SL* ps, SLType x); // 按值查找 void SLInsert(SL* ps, int pos, SLType x); // 指定位置插入 void SLErase(SL* ps, int pos); // 指定位置删除 void SLModify(SL* ps, int pos, SLType x); // 按位置修改4.2 SeqList.c 功能实现文件补全进阶函数#includeSeqList.h // 初始化顺序表 void SLInit(SL* ps) { assert(ps); ps-a NULL; ps-capacity 0; ps-size 0; } // 销毁顺序表释放动态内存 void Destroy(SL* ps) { assert(ps); if (ps-a) { free(ps-a); ps-a NULL; ps-capacity 0; ps-size 0; } } // 检测并自动扩容 void SLExp(SL* ps) { assert(ps); if (ps-capacity ps-size) { int Newspace ps-capacity 0 ? 4 : 2 * ps-capacity; SLType* Newps (SLType*)realloc(ps-a, Newspace * sizeof(SLType)); if (Newps NULL) { perror(Failed to apply for space); exit(1); } ps-a Newps; ps-capacity Newspace; } } // 尾部插入 void PushBack(SL* ps, SLType x) { assert(ps); SLExp(ps); ps-a[ps-size] x; } // 头部插入 void PushFront(SL* ps, SLType x) { assert(ps); SLExp(ps); // 元素后移从最后一个元素开始往前移 for (int i ps-size; i 0; i--) { ps-a[i] ps-a[i - 1]; } ps-a[0] x; ps-size; } // 头部删除 void SLPopFront(SL* ps) { assert(ps); assert(ps-size); // 元素前移覆盖第一个元素 for (int i 0; i ps-size - 1; i) { ps-a[i] ps-a[i 1]; } ps-size--; } // 尾部删除 void SLPopBack(SL* ps) { assert(ps); assert(ps-size); // 逻辑删除有效数据个数减1 ps-size--; } // 通用打印函数 void Print(SL* ps) { assert(ps); for (int i 0; i ps-size; i) { printf(SLTYPE_PRINT_FMT , ps-a[i]); } printf(\n); } // 按值查找返回下标没找到返回-1 int SLFind(SL* ps, SLType x) { assert(ps); for (int i 0; i ps-size; i) { if (ps-a[i] x) { return i; } } return -1; } // 指定位置插入元素 void SLInsert(SL* ps, int pos, SLType x) { assert(ps); assert(pos 0 pos ps-size); SLExp(ps); // 元素从后往前后移 for (int i ps-size; i pos; i--) { ps-a[i] ps-a[i - 1]; } ps-a[pos] x; ps-size; } // 指定位置删除元素 void SLErase(SL* ps, int pos) { assert(ps); assert(pos 0 pos ps-size); assert(ps-size); // 元素从前往后前移覆盖 for (int i pos; i ps-size - 1; i) { ps-a[i] ps-a[i 1]; } ps-size--; } // 修改指定位置元素 void SLModify(SL* ps, int pos, SLType x) { assert(ps); assert(pos 0 pos ps-size); ps-a[pos] x; }4.3 test.c 测试文件全覆盖测试所有接口#include SeqList.h int main() { // 定义顺序表变量 SL sl; // 初始化 SLInit(sl); printf( 顺序表初始化完成 \n\n); // 测试尾插 printf(----- 测试尾插 -----\n); PushBack(sl, 10); PushBack(sl, 20); PushBack(sl, 30); PushBack(sl, 40); printf(尾插 10 20 30 40 后); Print(sl); printf(\n); // 测试头插 printf(----- 测试头插 -----\n); PushFront(sl, 5); PushFront(sl, 1); printf(头插 5 1 后); Print(sl); printf(\n); // 测试尾删 printf(----- 测试尾删 -----\n); SLPopBack(sl); SLPopBack(sl); printf(尾删2次后); Print(sl); printf(\n); // 测试头删 printf(----- 测试头删 -----\n); SLPopFront(sl); printf(头删1次后); Print(sl); printf(\n); // 测试指定位置插入 printf(----- 测试指定位置插入 -----\n); SLInsert(sl, 1, 99); printf(在下标1插入99后); Print(sl); printf(\n); // 测试按值查找 printf(----- 测试按值查找 -----\n); int ret SLFind(sl, 99); if (ret ! -1) printf(找到元素99下标为%d\n, ret); else printf(未找到该元素\n); printf(\n); // 测试按位置修改 printf(----- 测试按位置修改 -----\n); SLModify(sl, 1, 88); printf(修改下标1为88后); Print(sl); printf(\n); // 测试指定位置删除 printf(----- 测试指定位置删除 -----\n); SLErase(sl, 1); printf(删除下标1元素后); Print(sl); printf(\n); // 释放内存 printf( 顺序表销毁完成 \n); Destroy(sl); return 0; }五、编译运行与输出结果编译命令gcc test.c SeqList.c -o SeqList运行输出 顺序表初始化完成 ----- 测试尾插 ----- 尾插 10 20 30 40 后10 20 30 40 ----- 测试头插 ----- 头插 5 1 后1 5 10 20 30 40 ----- 测试尾删 ----- 尾删2次后1 5 10 20 ----- 测试头删 ----- 头删1次后5 10 20 ----- 测试指定位置插入 ----- 在下标1插入99后5 99 10 20 ----- 测试按值查找 ----- 找到元素99下标为1 ----- 测试按位置修改 ----- 修改下标1为88后5 88 10 20 ----- 测试指定位置删除 ----- 删除下标1元素后5 10 20 顺序表销毁完成 六、学习总结顺序表所有增删改查基础接口至此全部完成从初始化、扩容、头尾操作到指定位置操作形成完整闭环assert断言是新手必须养成的习惯防护空指针、非法下标程序崩溃直接定位问题插入删除核心就是元素下标移动牢记插入从后往前移删除从前往后移坚持多文件拆分头文件声明、源文件实现、单独测试文件养成工程化编码思维手写踩坑比看视频记知识点更牢固每一个 Bug 都是帮自己补齐 C 语言指针、数组、下标短板。七、后续更新预告下一篇正式进入实战项目基于完整版动态顺序表手写实现简易通讯录支持联系人增删改查、查找、修改、保存等功能把顺序表知识真正落地应用。初学数据结构的小伙伴可以跟着一起手写有问题评论区交流一起稳扎稳打打好基础