
简介这是一份面向C语言学习者和数据结构初学者的PDF资料聚焦线性表顺序存储结构的原理与C语言实现。内容从顺序存储的定义出发逐步讲解基于结构体的表结构设计、动态内存分配、初始化、按位获取元素、插入与删除等核心操作并配有可运行的代码片段和易错点说明能帮助读者理解数组存储背后的内存管理与边界判断逻辑。资源包仅含1个PDF文档大小41KB便于离线阅读或打印适合在备考、课程设计或日常复习时快速查阅。已有4257人浏览学习说明这份PDF对入门者具有实用参考价值。通过系统梳理顺序表操作的实现细节读者可掌握线性表顺序存储的完整代码框架为后续学习链表、栈和队列等数据结构打下基础。1. 先把话说透顺序存储到底解决什么问题很多人在初学 C 语言时都会卡在“线性表的顺序存储”这一节——书上的代码能看懂但出了书就不知道怎么用考试能答对但真让自己从零写一个能跑的学生信息管理系统还是会慌。这篇文章就顺着“如何用 C 语言把一张线性表存进一段连续内存”这条主线把结构体定义、初始化、插入、删除、查找、扩容、二分查找这几个核心动作全部拆开每个动作都给可复制的代码再讲清楚为什么要这么写、参数该怎么调、遇到异常看哪里。适合正在学数据结构、准备机试或想回头把基础补扎实的开发者。顺序存储听起来抽象但它就是“用数组存东西”。逻辑上相邻的元素在内存里也紧挨着。这个特性决定了三件事随机访问能到 O(1)、缓存命中率高、插入删除会牵一发动全身。你如果理解了这三条后面所有代码细节都只是围绕它们在打转。2. 定义存储结构和初始化顺序存储的地基顺序存储的物理形态是一块连续内存逻辑形态是一个“能告诉你当前存了多少个元素”的对象。所以第一步不是写数组而是设计结构体。2.1 结构体怎么设计才不留坑我见过不少初学者直接把数组定义在函数里靠一个全局变量记长度。这在 50 行代码的教学程序里能跑但放到真实项目里就是灾难。因为函数之间共享数据全靠外部全局变量一旦工程变大你根本不知道谁改了这个全局变量排查问题会非常痛苦。我一般情况下会用结构体把“数据区”和“当前长度”打包在一起再额外记一个“容量”。这三点缺一不可#define MAX_SIZE 100 typedef struct { int *data; // 指向连续内存首地址 int length; // 当前有效元素个数 int capacity; // 当前分配到的容量单位元素个数 } SeqList;这里的data是核心。它是int *类型但真正存储时它指向的是一块能放capacity个int的连续内存。length永远小于等于capacity这是判断“表满”的依据。capacity让结构体知道自己最多能存多少扩容时靠它判断是否需要更新。逻辑说明data用指针而不是固定数组这样扩容时只需重新分配内存并把原数据拷贝过去结构体定义本身不需要改动。这为后面实现动态扩容留了退路。参数设定大多数教材会把MAX_SIZE直接写死成 100。但在实际开发中我建议把初始容量设置成更小的值比如 8 或 16。原因是为了让扩容机制尽早被触发扩容代码能被真实执行到而不是一直“躺”在代码里没被验证。2.2 初始化决定生死最容易被忽略的两个边界值初始化函数是所有操作的前提但它恰恰是很多人写得最随意的地方。常见的翻车点是只分配内存不检查是否成功或者忘记把length和capacity设初值。#include stdio.h #include stdlib.h void initList(SeqList *list, int capacity) { if (capacity 0) { return; } list-data (int *)malloc(sizeof(int) * capacity); if (list-data NULL) { printf(内存分配失败\n); exit(1); } list-length 0; list-capacity capacity; }逻辑说明先用malloc申请一块能容纳capacity个int的内存然后把length清零、记录容量。malloc返回的指针如果不检查就直接用内存不足时程序会立刻崩溃而且崩得毫无征兆。这一步检查是写所有 C 程序的基本功不该省。参数设定capacity由调用方传入通常是 8、16 或根据业务峰值估算出来的值。示例中设成 100 也行但要注意如果每个元素是结构体比如学生信息100 个元素的字节数就很大了要在内存预算里算清楚。3. 扩容和插入顺序存储的核心命门插入是顺序存储里最有代表性的操作。头部插入要把所有元素往后挪尾部插入则几乎零成本。这一节讲清楚插入的三种位置及其时间复杂度再补上动态扩容的完整实现。3.1 插入的三条路头插、尾插、中间插插入算法的逻辑是先检查表是否已满、位置是否合法然后把从插入位置到末尾的所有元素逐个后移一位最后写入新元素并把长度加一。int insertList(SeqList *list, int position, int value) { if (list NULL || list-data NULL) { return -1; } if (position 1 || position list-length 1) { printf(插入位置不合法\n); return -1; } if (list-length list-capacity) { printf(表已满请先扩容\n); return -1; } for (int i list-length - 1; i position - 1; i--) { list-data[i 1] list-data[i]; } list-data[position - 1] value; list-length; return 0; }逻辑说明position从 1 开始计数这是数据结构教材里的标准约定。循环从最后一个有效元素开始依次把前一个元素往后挪最后腾出position - 1这个下标位置。如果从前往后挪就会把后面的元素覆盖掉这是新手最容易翻车的点。参数设定position的取值范围是 1 到length 1意思是可以在末尾追加一个新元素。调用示例为insertList(list, list.length 1, 100)时表示在末尾追加。3.2 动态扩容从“满”到“更大”的完整过程静态数组最大的毛病就是“满了就满了”没有任何后悔药。动态扩容是顺序存储从玩具变成工程工具的转折点。int expandList(SeqList *list) { int newCapacity list-capacity * 2; int *newData (int *)realloc(list-data, sizeof(int) * newCapacity); if (newData NULL) { printf(扩容失败原数据保留\n); return -1; } list-data newData; list-capacity newCapacity; return 0; }逻辑说明使用realloc而不是先malloc再free因为realloc会在原地扩容如果原位置后面有足够空间直接扩展如果不够它会另找一块更大区域并把原数据自动拷贝过去。失败时原数据仍然有效不会丢失这是realloc优于手写“新建 拷贝 删除”方案的原因。参数设定新容量取原容量的 2 倍这是最常见的扩容策略。为什么要乘 2 而不是加 1因为加 1 每次插入都触发迁移均摊成本是 O(n)乘 2 时均摊成本是 O(1)这是摊还分析的基本结论。使用方式在插入函数里把“直接报错”改成“先扩容再插入”if (list-length list-capacity) { if (expandList(list) -1) { return -1; } }4. 删除和查找把顺序存储的强项用到极致删除是插入的逆操作——从后往前覆盖。查找则是顺序存储的舞台它天然支持下标随机访问这是链式存储无法比拟的。4.1 删除操作覆盖式删除的两个边界条件删除的基本思路是从删除位置的下一个元素开始逐个往前覆盖最后把长度减一。被“删掉”的那个元素不需要真正清空因为length已经把访问范围隔开了。int deleteList(SeqList *list, int position) { if (list NULL || list-data NULL) { return -1; } if (position 1 || position list-length) { printf(删除位置不合法\n); return -1; } for (int i position - 1; i list-length - 1; i) { list-data[i] list-data[i 1]; } list-length--; return 0; }逻辑说明循环只跑到length - 1也就是说最后一个有效元素会被覆盖到前一个位置而它自己后来变成“无效数据”但内存里它还在。程序退出前这段内存在那放着后续插入时会自然被覆盖。边界检查删除时position最大等于length大于它属于越界。相比之下插入的position允许等于length 1这是两处最容易混淆的地方。4.2 顺序查找和二分查找谁更配顺序存储顺序查找没什么技术含量就是从头遍历。它适合无序数据查找成功或失败都是 O(n)。而顺序存储的真正优势是配合排序后的数组做二分查找把查找成本降到 O(log n)。int binarySearch(SeqList *list, int target) { int low 0; int high list-length - 1; while (low high) { int mid low (high - low) / 2; if (list-data[mid] target) { return mid; } else if (list-data[mid] target) { low mid 1; } else { high mid - 1; } } return -1; }逻辑说明前提是list-data已经升序排列。循环里先用中间元素做比较等于就直接返回下标小于目标则去右半边继续查大于目标则去左半边查。low (high - low) / 2这种写法可以避免(low high) / 2在极端情况下的整数溢出这个细节在刷算法题时经常被拿出来考。使用前提二分查找的代价是“数据必须有序”。如果数据频繁插入删除维护有序性的成本很高。此时要用一种混合策略批量插入后统一排序或使用平衡树、跳表等结构。顺序存储本身不限制你用哪种算法但它最适合的是“偶尔插入、频繁查找”的业务。4.3 查找时的性能对比一组能让决策清晰的数字业务类型顺序查找(平均比较次数)二分查找(平均比较次数)查找前是否需要排序100 个元素约 50 次约 7 次需要1000 个元素约 500 次约 10 次需要10000 个元素约 5000 次约 14 次需要数据越庞大二分查找的收益越明显。但要注意排序本身也有成本。如果业务是对一组静态数据做反复查找先排序再二分是标杆做法如果数据频繁增删每次增删后都排序的成本反而可能拖垮性能。5. 避坑指南5 个让我半夜爬起来改代码的教训5.1 越界写导致数据静默损坏现象程序运行时不报错但偶尔输出一个莫名的大数字或者别的变量被改成了奇怪的值。原因插入时没有检查position list-length 1导致写到了data[length]之外的位置。C 语言数组不检查边界越界写不会立即崩溃但会覆盖相邻内存里的其他数据。解决每次插入和删除都把位置合法性检查放在第一位。在malloc和realloc之后用memset把整块区域清零也能让非法位置快速暴露出 0 值而不是随机残留数据方便排查。5.2 realloc 失败后的指针悬空现象扩容后发现原数据丢了但程序没崩。原因很多人写成list-data (int *)realloc(list-data, ...)如果realloc失败它返回NULL并把原指针释放掉这时list-data变成了一个悬空指针。解决使用临时变量接住返回值检查为NULL后再赋值。这就是前面 3.2 节写的newData那一行的意义。永远不要让原指针直接被NULL覆盖。5.3 头插法的 i 递减写成递增现象插入后后面的元素重复出现前面的元素丢失。原因插入时从最后一个元素开始往后挪循环条件是i 0那种写错成递增方向会导致后半部分元素反复覆盖自己。解决用我做测试时的口决简化记忆插入是倒着挪删除是正着挪。代码注释里写上“从末尾往起始方向移动”再复制到项目里时不容易错。5.4 动态扩容后忘记更新 capacity现象扩容几次后明明malloc了很大空间但插入还是提示表满。原因capacity没有更新判断length capacity用的还是旧值。解决在expandList函数的最后一行统一更新data和capacity不要漏掉其中一个。养成习惯任何修改容量的函数必须在末尾同步字段。5.5 混淆下标和位序现象输入插入位置 3但实际插到了下标 3逻辑位序和数组下标傻傻分不清。原因人的计数习惯是 1、2、3C 数组下标是 0、1、2。如果函数内部直接用position当下标就会差一位。解决在局部变量中先定义int idx position - 1;再用idx访问数组。这样代码里只出现一次减法后续所有地方都基于idx操作大大减少混淆。6. 综合实例把理论打包成一个能跑的学生成绩管理程序前面各节讲的都是单个函数这一节把它们拼成一个完整的可复现程序。目标应用场景很简单最多存 100 个学生的成绩支持追加、按学号删除、按学号查找、打印全部。6.1 完整代码框架#include stdio.h #include stdlib.h #include string.h #define INIT_CAPACITY 8 typedef struct { int id; char name[20]; int score; } Student; typedef struct { Student *data; int length; int capacity; } SeqList; void initList(SeqList *list) { list-data (Student *)malloc(sizeof(Student) * INIT_CAPACITY); if (list-data NULL) { exit(1); } list-length 0; list-capacity INIT_CAPACITY; } void expandList(SeqList *list) { int newCap list-capacity * 2; Student *newData (Student *)realloc(list-data, sizeof(Student) * newCap); if (newData NULL) { printf(扩容失败\n); return; } list-data newData; list-capacity newCap; } int insertStudent(SeqList *list, Student stu) { if (list-length list-capacity) { expandList(list); } list-data[list-length] stu; list-length; printf(学号 %d 添加到末尾当前容量 %d已存 %d 人\n, stu.id, list-capacity, list-length); return 0; } int deleteById(SeqList *list, int id) { for (int i 0; i list-length; i) { if (list-data[i].id id) { for (int j i; j list-length - 1; j) { list-data[j] list-data[j 1]; } list-length--; printf(学号 %d 已删除剩余 %d 人\n, id, list-length); return 0; } } printf(未找到学号 %d\n, id); return -1; } Student *findById(SeqList *list, int id) { for (int i 0; i list-length; i) { if (list-data[i].id id) { return list-data[i]; } } return NULL; } void printList(SeqList *list) { for (int i 0; i list-length; i) { printf(学号 %d姓名 %s成绩 %d\n, list-data[i].id, list-data[i].name, list-data[i].score); } }6.2 主函数调用和验证点int main() { SeqList list; initList(list); Student s1 {1, 张三, 85}; Student s2 {2, 李四, 92}; Student s3 {3, 王五, 77}; insertStudent(list, s1); insertStudent(list, s2); insertStudent(list, s3); printList(list); // 查找学号为 2 的记录 Student *found findById(list, 2); if (found) { printf(找到了%s 的成绩是 %d\n, found-name, found-score); } else { printf(未找到此人\n); } // 删除学号为 1 的记录 deleteById(list, 1); printList(list); free(list.data); return 0; }用它来验证上一节提到的所有细节。把INIT_CAPACITY改成 3再连插 4 个学生观察扩容是否正常触发。再去故意插入一个position越界的元素看看错误提示是否有效。这套程序是我自己每次讲顺序存储时都会跑一遍的基准测试确认所有边界行为都在控制之内。在结束之前分享一个我自己踩过的坑最初写deleteById时忘了在删除后释放“被删空”的元素。后来发现这个坑不会立刻暴露——如果后面继续插入新元素它会覆盖旧数据一切正常但如果你在删除后立刻遍历列表读数据那个位置上的残留数据会被打印出来。解决方式不是去手动清理而是永远信任length这个边界。顺序存储的世界里长度就是法律任何操作都别绕过它。希望这些代码和参数能帮你真正把顺序存储这节吃透少走我当初走过的弯路。本文还有配套的精品资源点击获取