摘要数据结构是计算机科学的核心基础是连接 C 语言基础与实际项目开发、算法设计的关键桥梁。本文从数据结构学习总览入手明确学习范围、目标、要求与收获再深度拆解线性结构的开篇重点 —— 顺序表涵盖ADT 抽象数据类型定义、核心原理、完整功能接口封装初始化、增删改查、遍历、销毁、内存操作函数详解、实战测试案例、常见错误排查、面试高频考点全程结合底层原理、代码实现与注释兼顾专业性与易懂性适合 C 语言基础学习者入门数据结构、期末复习、笔试面试备考。0. 数据结构学习总览体系化梳理避免盲目学习数据结构的核心是 “组织数据的方式”—— 如何高效地存储、访问、修改数据是后续算法设计、项目开发如操作系统、数据库、嵌入式开发的基础。以下明确学习框架、目标与要求帮你建立清晰的学习认知。1. 我们到底学什么数据结构核心知识体系数据结构按 “元素之间的关系” 可分为三大类本次先聚焦线性结构后续逐步延伸至树型、图型结构具体体系如下a. 线性结构一对一关系重点开篇线性结构的核心特征数据元素之间前后相连、仅有一对一的逻辑关系是最基础、最常用的数据结构也是后续复杂结构的基础。1顺序存储结构连续内存核心实现顺序表基于数组底层原理数据元素在物理内存中连续存放通过 “起始地址 元素下标” 实现随机访问核心特点查找快O (1) 时间复杂度、遍历快插入、删除慢需移动大量元素O (n) 时间复杂度适用场景数据查找频繁、插入删除较少的场景如学生成绩查询、静态数据存储2链式存储结构非连续内存核心实现单链表、双链表、循环链表、内核链表Linux 内核通用链表底层原理数据元素节点分散存储在内存中通过 “指针” 连接前后元素形成逻辑上的线性关系核心特点插入、删除快无需移动元素仅修改指针O (1) 时间复杂度查找慢需从头遍历O (n) 时间复杂度适用场景数据插入删除频繁、查找较少的场景如消息队列、动态链表管理3特殊线性结构受限线性结构栈Stack遵循 “先进后出FILO” 原则仅允许在一端栈顶进行插入、删除操作应用函数调用栈、表达式求值、括号匹配队列Queue遵循 “先进先出FIFO” 原则允许在一端队尾插入、另一端队头删除应用消息队列、任务调度、键盘输入缓冲区b. 树型结构一对多关系进阶重点树型结构的核心特征数据元素之间是层次化的一对多关系适用于描述具有层级结构的数据。基础树型结构二叉树每个节点最多有两个子节点核心操作前序遍历、中序遍历、后序遍历、层序遍历面试必考进阶树型结构二叉搜索树BST用于快速查找、插入、删除左子树值 根节点 右子树值平衡二叉树AVL解决二叉搜索树失衡问题保证查找效率稳定左右子树高度差≤1红黑树近似平衡插入删除效率更高Linux 内核、STL 容器底层常用B 树多路平衡树适合大量数据存储数据库索引底层核心结构c. 图型结构多对多关系高阶内容图型结构的核心特征数据元素之间是任意多对多的关系是最复杂的数据结构。核心概念顶点、边、有向图、无向图、权重应用路径规划、社交网络、网络拓扑、推荐系统2. 学习目标学完能达到什么水平学习数据结构不是单纯记代码而是提升 “数据组织与逻辑设计” 能力具体目标如下a.编程逻辑能力显著提升从 “写简单语法” 升级为 “处理复杂逻辑、抽象问题”能将实际需求转化为数据结构设计培养 “模块化、结构化” 思维不再写杂乱无章的代码b.C 语言语法能力彻底巩固与深化吃透指针、结构体、函数指针、动态内存管理malloc/calloc/realloc/free等核心难点掌握通用代码封装技巧能写可复用、可扩展的 C 语言代码c.库封装能力实战掌握能独立封装通用数据结构库如顺序表、链表、栈、队列支持任意数据类型int、结构体等理解 “抽象数据类型ADT” 的思想能隐藏底层实现提供统一接口d.具备面试核心竞争力掌握校招 / 笔试 / 面试中数据结构的高频考点如顺序表内存对齐、链表操作、二叉树遍历能独立手写核心代码如顺序表增删改查、链表反转、二叉树遍历应对面试手写题3. 学习要求门槛不高循序渐进即可无需具备高深的 C 语言基础满足以下条件即可轻松入门掌握 C 语言基础语法变量、循环、分支、函数理解结构体的定义与使用能定义结构体、访问成员掌握指针的基本用法指针变量定义、解引用、取地址了解动态内存分配知道 malloc/calloc/free 的作用能简单使用愿意动手敲代码、调试代码数据结构是 “练” 出来的不是 “看” 出来的4. 学完能得到什么明确学习价值a.编程能力质的飞跃从 “会写代码” 到 “会写高质量代码”能处理更复杂的项目需求b.底层逻辑认知提升理解计算机存储数据、处理数据的底层逻辑为后续学习操作系统、数据库、嵌入式开发打下基础c.面试与就业优势数据结构是校招、社招尤其是后端、嵌入式、算法岗的必考内容掌握后能大幅提升面试通过率d.算法设计基础数据结构是算法的载体掌握数据结构后能轻松入门算法设计如排序、查找算法5. 今日核心内容顺序表线性结构开篇面试高频顺序表是最基础、最易理解的数据结构也是后续学习链表、栈、队列的基础。它的本质是 “用数组实现的线性表”但通过封装实现了通用、可扩展的操作接口解决了普通数组固定大小、无法灵活增删的问题。5.1 什么是顺序表底层原理详解5.1.1 定义顺序表Sequential List是线性表的顺序存储结构它将线性表中的所有元素按逻辑顺序依次存储在一片连续的物理内存空间中元素的逻辑顺序与物理存储顺序完全一致。5.1.2 核心特征必记面试常考逻辑关系元素之间是一对一的线性关系物理存储地址连续元素在内存中紧密排列类似数组但比数组更灵活访问方式支持随机访问通过下标计算元素地址O (1) 时间复杂度存储限制需要提前分配固定大小的内存空间容量固定若要扩容需重新分配内存5.1.3 顺序表与普通数组的区别面试易错点很多人会把顺序表和数组混淆二者核心区别如下表格对比维度普通数组顺序表容量固定不变定义时确定可扩容通过重新分配内存操作接口仅支持下标访问无统一接口封装了增删改查、遍历、销毁等统一接口通用性仅支持单一数据类型支持任意数据类型通过 void * 指针内存管理手动管理易出现内存泄漏封装销毁接口自动释放内存更安全5.1.4 顺序表的优缺点面试必背优点随机访问效率高通过起始地址 下标可直接访问任意元素时间复杂度 O (1)存储密度高元素之间无额外空间除了必要的填充字节内存利用率高遍历效率高连续内存可通过循环快速遍历所有元素无需跳转指针缺点插入、删除效率低插入 / 删除元素时需移动后续所有元素时间复杂度 O (n)容量固定初始若初始容量分配过小会频繁扩容分配过大会浪费内存扩容成本高扩容时需重新分配内存、复制原有数据消耗系统资源5.1.5 适用场景数据量固定或变化不大且以查找、遍历操作为主的场景对访问效率要求高对插入删除效率要求不高的场景如学生成绩管理、静态数据查询5.2 顺序表 ADT 抽象数据类型定义核心封装思想ADTAbstract Data Type抽象数据类型是指 “只关注数据的逻辑结构和操作接口不关注底层实现” 的思想。我们封装顺序表时采用 ADT 思想隐藏底层数组实现对外提供统一的操作接口同时支持任意数据类型存储。5.2.1 结构体定义核心代码带详细注释#include stdio.h #include stdlib.h #include string.h // 顺序表抽象数据类型ADT定义 struct seqlist_st { void *arr; // 顺序表存储空间的起始地址void* 指针支持任意数据类型存储 int size; // 每个元素的大小单位字节用于计算元素地址偏移 int nmemb; // 当前顺序表中已存储的元素个数有效元素数 int capacity; // 顺序表的最大容量最多能存储的元素个数 };5.2.2 结构体成员详细解释必懂避免踩坑void *arr通用指针无类型指针可以指向任意类型的数据int、char、float、结构体等底层本质是一片连续的内存空间通过地址偏移访问不同元素为什么不用具体类型如 int*为了实现通用性让顺序表能存储任意类型数据int size每个元素的字节大小如 int 是 4 字节char 是 1 字节结构体按内存对齐后的大小计算核心作用计算元素的地址偏移第 i 个元素地址 arr i * sizeint nmemb记录当前已存储的有效元素个数初始值为 0用于判断顺序表是否已满nmemb capacity、是否为空nmemb 0int capacity顺序表的最大容量即最多能存储的元素个数初始化时确定后续可通过扩容接口修改本文后续补充扩容功能5.3 顺序表核心功能接口封装完整代码 注释 原理顺序表的核心操作的是 “增、删、改、查、遍历、初始化、销毁”以下接口全部封装为独立函数支持任意数据类型兼顾安全性与通用性每一步都带底层原理解释。5.3.1 1. 顺序表初始化seqlist_init功能初始化顺序表分配结构体空间和数据存储空间初始化所有成员变量。函数原型// 参数 // s二级指针要修改指针本身因为要给指针分配内存 // size每个元素的大小字节 // capacity顺序表的最大容量 // 返回值0 成功-1 失败内存分配失败 int seqlist_init(struct seqlist_st **s, int size, int capacity);实现代码带详细注释int seqlist_init(struct seqlist_st **s, int size, int capacity) { // 1. 参数合法性校验避免无效参数 if (s NULL || size 0 || capacity 0) { printf(初始化失败参数非法\n); return -1; } // 2. 分配顺序表结构体本身的内存空间 // *s是指针需要给指针指向的空间分配内存结构体大小 *s (struct seqlist_st *)malloc(sizeof(struct seqlist_st)); if (*s NULL) // 内存分配失败判断必须做避免野指针 { perror(malloc seqlist_st failed); // 打印错误原因 return -1; } // 3. 分配顺序表数据存储空间arr指向的空间 // calloc分配capacity个size大小的空间并初始化为0比malloc更安全 (*s)-arr calloc(capacity, size); if ((*s)-arr NULL) // 数据空间分配失败需释放已分配的结构体空间避免内存泄漏 { perror(calloc arr failed); free(*s); // 释放结构体空间 *s NULL; // 置空指针避免野指针 return -1; } // 4. 初始化结构体成员变量 (*s)-size size; // 赋值每个元素的大小 (*s)-capacity capacity;// 赋值最大容量 (*s)-nmemb 0; // 初始有效元素个数为0 printf(顺序表初始化成功容量%d元素大小%d字节\n, capacity, size); return 0; }原理补充面试考点为什么用二级指针因为要修改指针s本身给s分配内存一级指针只能修改指针指向的内容无法修改指针本身。为什么用 calloc 而不用 malloccalloc 会自动将分配的内存初始化为 0避免数据脏读malloc 分配的内存是随机值需手动初始化。内存分配失败的处理必须释放已分配的空间避免内存泄漏这是高质量代码的基本要求面试常考内存泄漏问题。5.3.2 2. 顺序表扩容seqlist_expand补充解决容量不足问题功能当顺序表已满nmemb capacity时扩容为原来的 2 倍行业通用扩容策略避免频繁扩容。函数原型// 参数s顺序表指针new_capacity新容量建议为原容量的2倍 // 返回值0 成功-1 失败 int seqlist_expand(struct seqlist_st *s, int new_capacity);实现代码int seqlist_expand(struct seqlist_st *s, int new_capacity) { // 1. 参数校验 if (s NULL || new_capacity s-capacity) { printf(扩容失败参数非法或新容量不大于原容量\n); return -1; } // 2. 重新分配数据存储空间realloc扩容原有内存保留原有数据 void *new_arr realloc(s-arr, new_capacity * s-size); if (new_arr NULL) { perror(realloc new_arr failed); return -1; } // 3. 更新顺序表成员 s-arr new_arr; // 指向新的内存空间 s-capacity new_capacity;// 更新最大容量 printf(顺序表扩容成功原容量%d新容量%d\n, s-capacity/2, s-capacity); return 0; }面试考点扩容策略为什么扩容为 2 倍避免频繁扩容每次扩容成本高同时平衡内存利用率扩容过大浪费内存过小频繁扩容。realloc 的用法realloc 会尝试在原有内存后面扩展空间若空间不足会分配新空间并复制原有数据最后释放原空间。5.3.3 3. 增加元素插入seqlist_insert功能向顺序表的指定位置 pos 插入一个元素插入后元素顺序保持线性。函数原型// 参数 // s顺序表指针 // data要插入的数据const void* 避免修改原数据 // pos插入位置0 ≤ pos ≤ nmembpos0插在头部posnmemb插在尾部 // 返回值0 成功-1 失败参数非法、顺序表已满 int seqlist_insert(struct seqlist_st *s, const void *data, int pos);实现代码带详细注释int seqlist_insert(struct seqlist_st *s, const void *data, int pos) { // 1. 参数合法性校验 if (s NULL || data NULL) { printf(插入失败参数非法空指针\n); return -1; } // 插入位置校验pos不能小于0也不能大于当前有效元素个数否则会出现空隙 if (pos 0 || pos s-nmemb) { printf(插入失败位置非法\n); return -1; } // 顺序表已满先扩容扩容为原容量的2倍 if (s-nmemb s-capacity) { if (seqlist_expand(s, s-capacity * 2) ! 0) { printf(插入失败扩容失败\n); return -1; } } // 2. 计算数据起始地址void*不能直接运算强制转换为char*按字节偏移 char *base (char *)s-arr; // 3. 移动元素从pos位置开始后续所有元素向后移动一个位置避免覆盖 // 从最后一个元素开始移动直到pos位置若从pos开始移动会覆盖后面的元素 for (int i s-nmemb; i pos; i--) { // 复制将第i-1个元素复制到第i个位置 // 源地址base (i-1)*s-size // 目的地址base i*s-size // 复制长度s-size每个元素的大小 memcpy(base i * s-size, base (i - 1) * s-size, s-size); } // 4. 插入新元素将data复制到pos位置 memcpy(base pos * s-size, data, s-size); // 5. 有效元素个数1 s-nmemb; printf(插入成功当前有效元素个数%d\n, s-nmemb); return 0; }原理补充面试考点为什么要将 void转换为 char因为 void指针不能直接进行算术运算不知道每个元素的大小char是 1 字节可按字节偏移计算元素地址。元素移动方向必须从后向前移动若从前向后移动pos 位置后面的元素会被覆盖导致数据丢失面试常考错误点。插入位置的范围pos 可以等于 nmemb插在尾部但不能大于 nmemb否则会出现内存空隙导致访问越界。5.3.4 4. 删除元素seqlist_delete功能删除顺序表指定位置 pos 的元素删除后后续元素向前移动保持线性关系。函数原型// 参数s顺序表指针pos删除位置0 ≤ pos nmemb // 返回值0 成功-1 失败参数非法、顺序表为空 int seqlist_delete(struct seqlist_st *s, int pos);实现代码带详细注释int seqlist_delete(struct seqlist_st *s, int pos) { // 1. 参数合法性校验 if (s NULL) { printf(删除失败顺序表为空指针\n); return -1; } // 顺序表为空无法删除 if (s-nmemb 0) { printf(删除失败顺序表为空\n); return -1; } // 删除位置校验pos不能小于0也不能大于等于有效元素个数否则越界 if (pos 0 || pos s-nmemb) { printf(删除失败位置非法\n); return -1; } // 2. 计算数据起始地址 char *base (char *)s-arr; // 3. 移动元素从pos1位置开始后续所有元素向前移动一个位置覆盖pos位置的元素 for (int i pos; i s-nmemb - 1; i) { memcpy(base i * s-size, base (i 1) * s-size, s-size); } // 4. 有效元素个数-1 s-nmemb--; printf(删除成功当前有效元素个数%d\n, s-nmemb); return 0; }面试考点删除后的元素处理删除后无需手动清空 pos 位置的元素后续元素会覆盖该位置且 nmemb 减少该位置不再被视为有效元素。边界条件顺序表为空时不能删除pos 不能等于 nmemb否则访问越界。5.3.5 5. 修改元素seqlist_modify功能修改顺序表指定位置 pos 的元素用新数据覆盖原有数据。函数原型// 参数 // s顺序表指针 // pos修改位置0 ≤ pos nmemb // data新数据const void* 避免修改原数据 // 返回值0 成功-1 失败参数非法、顺序表为空 int seqlist_modify(struct seqlist_st *s, int pos, const void *data);实现代码int seqlist_modify(struct seqlist_st *s, int pos, const void *data) { // 1. 参数合法性校验 if (s NULL || data NULL) { printf(修改失败参数非法空指针\n); return -1; } if (s-nmemb 0) { printf(修改失败顺序表为空\n); return -1; } if (pos 0 || pos s-nmemb) { printf(修改失败位置非法\n); return -1; } // 2. 计算pos位置的元素地址用新数据覆盖 char *base (char *)s-arr; memcpy(base pos * s-size, data, s-size); printf(修改成功\n); return 0; }5.3.6 6. 查询元素seqlist_find功能根据自定义的比较规则查找顺序表中与目标数据匹配的元素返回其下标。函数原型// 参数 // s顺序表指针 // data目标数据要查找的数据 // cmp比较函数指针自定义比较规则返回0表示匹配非0表示不匹配 // 返回值匹配元素的下标≥0-1 失败未找到、参数非法 int seqlist_find(struct seqlist_st *s, const void *data, int (*cmp)(const void*, const void*));实现代码带详细注释int seqlist_find(struct seqlist_st *s, const void *data, int (*cmp)(const void*, const void*)) { // 1. 参数合法性校验 if (s NULL || data NULL || cmp NULL) { printf(查找失败参数非法\n); return -1; } if (s-nmemb 0) { printf(查找失败顺序表为空\n); return -1; } // 2. 遍历顺序表逐个比较元素 char *base (char *)s-arr; for (int i 0; i s-nmemb; i) { // 调用自定义比较函数比较第i个元素和目标数据 // 第i个元素地址base i*s-size if (cmp(base i * s-size, data) 0) { printf(查找成功元素下标%d\n, i); return i; // 返回匹配元素的下标 } } // 3. 未找到匹配元素 printf(查找失败未找到目标元素\n); return -1; }原理补充面试考点为什么用函数指针因为顺序表支持任意数据类型不同类型的比较规则不同如 int 比较、结构体比较用函数指针让用户自定义比较规则实现通用性。比较函数的设计返回 0 表示匹配非 0 表示不匹配符合 C 语言库函数的设计习惯如 strcmp 函数。5.3.7 7. 遍历顺序表seqlist_traverse功能遍历顺序表中的所有有效元素调用自定义的打印函数实现不同类型元素的打印通用性。函数原型// 参数 // s顺序表指针const修饰避免修改顺序表内容 // pri打印函数指针自定义打印规则适配不同数据类型 // 返回值无 void seqlist_traverse(const struct seqlist_st *s, void (*pri)(const void *data));实现代码void seqlist_traverse(const struct seqlist_st *s, void (*pri)(const void *data)) { // 1. 参数合法性校验 if (s NULL || pri NULL) { printf(遍历失败参数非法\n); return; } if (s-nmemb 0) { printf(遍历失败顺序表为空\n); return; } // 2. 遍历所有有效元素调用打印函数 char *base (char *)s-arr; printf(顺序表元素); for (int i 0; i s-nmemb; i) { pri(base i * s-size); // 回调自定义打印函数 } printf(\n); }面试考点const 修饰的作用const 修饰顺序表指针防止在遍历过程中修改顺序表的内容提升代码安全性面试常考 const 的用法。函数指针的回调机制遍历函数不关心元素的具体类型和打印方式将打印逻辑交给用户实现 “解耦”这是模块化编程的核心思想。5.3.8 8. 销毁顺序表seqlist_destroy功能释放顺序表占用的所有内存数据空间 结构体空间避免内存泄漏。函数原型// 参数s二级指针要修改指针本身置空为NULL // 返回值无 void seqlist_destroy(struct seqlist_st **s);实现代码带详细注释void seqlist_destroy(struct seqlist_st **s) { // 1. 参数合法性校验 if (s NULL || *s NULL) { printf(销毁失败顺序表已为空\n); return; } // 2. 先释放数据存储空间arr指向的空间 free((*s)-arr); (*s)-arr NULL; // 置空指针避免野指针 // 3. 再释放顺序表结构体空间 free(*s); *s NULL; // 置空指针避免野指针 printf(顺序表销毁成功\n); }面试考点高频销毁顺序的顺序必须先释放数据空间arr再释放结构体空间否则会导致 arr 指针成为野指针无法释放数据空间内存泄漏。为什么用二级指针因为要将指针s置空*s NULL一级指针无法修改指针本身只能修改指针指向的内容。内存泄漏的危害长期运行的程序如服务器、嵌入式设备内存泄漏会导致内存耗尽程序崩溃。5.4 内存操作三函数详解顺序表核心依赖必掌握顺序表的增、删、改操作核心依赖 C 语言标准库中的三个内存操作函数memcpy、memmove、memset这三个函数是 C 语言面试高频考点必须吃透其用法、区别与底层原理。5.4.1 1. memcpy内存复制函数函数原型#include string.h void *memcpy(void *dest, const void *src, size_t n);功能从源地址src开始复制n个字节的数据到目标地址dest返回目标地址dest。核心特点不检查源地址和目标地址是否重叠复制速度快适合地址不重叠的内存复制若地址重叠复制结果未定义可能出现数据覆盖、复制错误示例顺序表插入 / 修改中使用// 将src指向的数据复制n个字节到dest memcpy(dest, src, n);面试考点memcpy 与 strcpy 的区别高频strcpy 只能复制字符串会自动在末尾添加 \0 终止符memcpy 可以复制任意类型数据int、结构体等按字节复制。strcpy 不需要指定复制长度以 \0 为终止标志memcpy 需要指定复制长度n。strcpy 若源字符串没有 \0会导致越界访问memcpy 只要n指定正确不会越界。5.4.2 2. memmove内存移动函数函数原型#include string.h void *memmove(void *dest, const void *src, size_t n);功能与 memcpy 完全一致从源地址src复制n个字节到目标地址dest返回dest。核心特点支持源地址和目标地址重叠复制结果定义明确不会出现数据错误实现逻辑比 memcpy 复杂复制速度略慢推荐场景当源地址和目标地址可能重叠时如顺序表插入 / 删除时的元素移动示例顺序表元素移动推荐使用// 即使dest和src地址重叠也能正确复制 memmove(dest, src, n);面试考点高频memcpy 与 memmove 的区别地址重叠处理memcpy 不支持重叠memmove 支持重叠。速度memcpy memmovememmove 需要判断地址重叠多一步逻辑。安全性memmove memcpy避免地址重叠导致的错误。5.4.3 3. memset内存初始化函数函数原型#include string.h void *memset(void *s, int c, size_t n);功能将地址s开始的n个字节全部设置为字符c注意c是 int 类型但实际只使用低 8 位即 ASCII 码返回s。核心特点用于初始化内存如将内存清空为 0、设置为指定字符按字节初始化适合初始化连续内存空间不能用于初始化非字符类型的复杂数据如 int 数组设置为 1会出错示例// 将arr指向的100个字节全部设置为0常用初始化方式 memset(arr, 0, 100); // 将arr指向的10个字节全部设置为aASCII码97 memset(arr, a, 10);面试易错点错误用法memset(arr, 1, 10*sizeof(int))想将 int 数组初始化为 1。原因memset 按字节初始化int 是 4 字节每个字节都会被设置为 1最终每个 int 值为 0x00000001即 1看似正确不若要设置为其他值如 2会变成 0x00000002正确但如果是负数如 - 1会变成 0xffffffff正确。但注意memset 只能初始化 “每个字节相同” 的值不能初始化任意 int 值如 5。5.5 顺序表完整实战测试案例可直接运行以下案例测试顺序表的所有功能分别测试 int 类型和结构体类型验证通用性带详细注释可直接复制编译运行。5.5.1 测试 1int 类型顺序表// 1. 自定义打印函数int类型 void print_int(const void *data) { printf(%d , *(int *)data); // 强制转换为int*解引用获取值 } // 2. 自定义比较函数int类型 int cmp_int(const void *a, const void *b) { return *(int *)a - *(int *)b; // 相等返回0不相等返回差值 } // 3. 测试主函数 int main() { struct seqlist_st *s NULL; int ret; // 1. 初始化顺序表int类型每个元素4字节初始容量5 ret seqlist_init(s, sizeof(int), 5); if (ret ! 0) { printf(初始化失败程序退出\n); return -1; } // 2. 插入元素插在尾部 int a 10, b 20, c 30, d 40, e 50, f 60; seqlist_insert(s, a, 0); // 插在头部pos0 seqlist_insert(s, b, 1); // 插在pos1 seqlist_insert(s, c, 2); // 插在pos2 seqlist_insert(s, d, 3); // 插在pos3 seqlist_insert(s, e, 4); // 插在尾部pos4 seqlist_insert(s, f, 5); // 容量已满触发扩容扩容为10 // 3. 遍历顺序表 seqlist_traverse(s, print_int); // 输出10 20 30 40 50 60 // 4. 查找元素查找30 int find_data 30; int pos seqlist_find(s, find_data, cmp_int); // 5. 修改元素将pos位置的元素改为300 int modify_data 300; seqlist_modify(s, pos, modify_data); seqlist_traverse(s, print_int); // 输出10 20 300 40 50 60 // 6. 删除元素删除pos2的元素 seqlist_delete(s, 2); seqlist_traverse(s, print_int); // 输出10 20 40 50 60 // 7. 销毁顺序表 seqlist_destroy(s); return 0; }5.5.2 测试 2结构体类型顺序表验证通用性// 定义一个学生结构体 struct student { int id; // 学号 char name[20]; // 姓名 float score; // 成绩 }; // 1. 自定义打印函数结构体类型 void print_stu(const void *data) { struct student *stu (struct student *)data; printf(学号%d姓名%s成绩%.2f , stu-id, stu-name, stu-score); } // 2. 自定义比较函数按学号比较 int cmp_stu(const void *a, const void *b) { struct student *stu1 (struct student *)a; struct student *stu2 (struct student *)b; return stu1-id - stu2-id; // 按学号相等返回0 } // 3. 测试主函数 int main() { struct seqlist_st *s NULL; int ret; // 1. 初始化顺序表结构体类型每个元素大小为struct student的大小初始容量3 ret seqlist_init(s, sizeof(struct student), 3); if (ret ! 0) { printf(初始化失败程序退出\n); return -1; } // 2. 定义学生数据 struct student stu1 {101, 张三, 88.5}; struct student stu2 {102, 李四, 76.0}; struct student stu3 {103, 王五, 92.5}; struct student stu4 {104, 赵六, 59.0}; // 用于测试扩容 // 3. 插入学生数据 seqlist_insert(s, stu1, 0); seqlist_insert(s, stu2, 1); seqlist_insert(s, stu3, 2); seqlist_insert(s, stu4, 3); // 扩容为6 // 4. 遍历学生信息 printf(\n学生信息\n); seqlist_traverse(s, print_stu); // 5. 查找学生查找学号102的学生 struct student find_stu {102, , 0.0}; int pos seqlist_find(s, find_stu, cmp_stu); // 6. 修改学生成绩将学号102的学生成绩改为80.0 struct student modify_stu {102, 李四, 80.0}; seqlist_modify(s, pos, modify_stu); printf(\n修改后学生信息\n); seqlist_traverse(s, print_stu); // 7. 删除学生删除学号103的学生 struct student delete_stu {103, , 0.0}; pos seqlist_find(s, delete_stu, cmp_stu); seqlist_delete(s, pos); printf(\n删除后学生信息\n); seqlist_traverse(s, print_stu); // 8. 销毁顺序表 seqlist_destroy(s); return 0; }5.6 常见错误排查新手必看面试常考错误 1使用一级指针初始化顺序表导致初始化失败// 错误写法 struct seqlist_st *s; seqlist_init(s, sizeof(int), 5); // 传递一级指针 // 正确写法 struct seqlist_st *s NULL; seqlist_init(s, sizeof(int), 5); // 传递二级指针原因初始化函数需要修改指针s本身给s分配内存一级指针无法修改指针本身只能修改指针指向的内容。错误 2插入 / 删除时元素移动方向错误从前向后移动// 错误写法插入时从前向后移动 for (int i pos; i s-nmemb; i) { memcpy(base i * s-size, base (i - 1) * s-size, s-size); }原因从前向后移动pos 位置后面的元素会被覆盖导致数据丢失正确方向是从后向前移动。错误 3忘记释放内存导致内存泄漏// 错误写法只初始化不销毁 struct seqlist_st *s; seqlist_init(s, sizeof(int), 5); // 不调用seqlist_destroy(s);原因顺序表的结构体和数据空间都是动态分配的不销毁会导致内存泄漏长期运行会耗尽内存。错误 4使用 memcpy 处理地址重叠的内存// 错误写法地址重叠时用memcpy char arr[10] 123456789; memcpy(arr2, arr, 5); // 源地址arr和目标地址arr2重叠原因memcpy 不支持地址重叠会导致复制结果错误应使用 memmove。错误 5顺序表扩容时新容量小于等于原容量// 错误写法 seqlist_expand(s, s-capacity); // 新容量等于原容量原因扩容的目的是增加容量新容量必须大于原容量否则扩容无意义。5.7 面试高频考点总结必背顺序表的定义、特点、优缺点必考。顺序表的内存对齐结合结构体大小计算如顺序表结构体的大小。顺序表插入、删除的时间复杂度O (n)查找的时间复杂度O (1)。顺序表扩容策略为什么扩容为 2 倍。memcpy 与 memmove 的区别地址重叠处理、速度、安全性。顺序表初始化、销毁时为什么用二级指针如何实现通用顺序表支持任意数据类型void * 指针 函数指针。顺序表与链表的区别面试高频对比题。顺序表的内存泄漏问题如何避免。手写顺序表的核心接口插入、删除、遍历面试手写题高频。本章整体总结数据结构的核心是 “组织数据的方式”线性结构是基础顺序表是线性结构的开篇重点。顺序表本质是 “连续内存的线性表”支持随机访问插入删除慢适合查找频繁的场景。通用顺序表的实现核心void * 指针支持任意类型 函数指针自定义比较、打印规则。核心操作接口初始化、扩容、增、删、改、查、遍历、销毁必须掌握每个接口的实现原理和代码。内存操作三函数memcpy、memmove、memset是顺序表的核心依赖必须吃透其用法和区别。学习顺序表的关键理解 ADT 抽象数据类型思想掌握模块化、通用化的代码封装技巧多动手敲代码、调试错误。