简介:这份资源是南京邮电大学数据结构课程四次实验的完整源码合集,面向正在学习数据结构的高校学生与自学者,帮助读者通过可运行代码理解线性表、栈与队列、树形结构以及图算法等核心内容。压缩包共64个文件,以h头文件与cpp源文件为主体,另含pdb、obj、ilk等编译调试中间文件及dsw、dsp工程文件,整体约1.58MB,可直接在开发环境中打开编译运行。内容覆盖多项式运算、线性表操作、二叉树与哈夫曼树、图的基本操作及飞机换乘次数等典型实验场景,并附有实验报告文档,便于对照代码梳理实现思路与算法细节。目前已有2637人学习下载,适合作为课程实验的参考范例与调试排错辅助材料,建议在理解基础上独立完成编码,以真正掌握数据结构的应用方法。
1. 数据结构实验源码到底该怎么读:从“能跑”到“能改”的分水岭
很多人拿到一份数据结构实验源码合集,第一反应是编译跑通、看到控制台输出就关掉。但真正拉开差距的,是你能不能把里面的链表、栈、队列、树、图逐个拆开,改一个参数、换一种输入,观察行为变化。这份“南邮数据结构试验全部源码”本质上是一套覆盖线性表、栈与队列、串、树与二叉树、图、查找、排序的完整实验代码集合,通常以 C/C++ 为主,配合少量头文件和测试数据。它解决的不是“有没有代码抄”,而是“每个数据结构在内存里到底怎么长、指针怎么跳、边界怎么崩”。适合正在上这门课、准备实验验收、或者想用一套可编译代码反向补基础的人。如果你只复制粘贴交差,这份源码的价值几乎为零;如果你把它当成可调试的黑匣子,逐行打断点看指针变化,它比大多数教材都管用。
2. 先分清实验源码的目录结构与编译入口:别一上来就全量编译
2.1 典型目录长什么样,哪些文件是核心
一套完整的数据结构实验源码,目录通常不会只有一个main.c。常见做法是按实验编号分文件夹,每个文件夹里放源码、头文件、测试数据,偶尔带一个 Makefile 或 CMakeLists。下面是一个我见过的典型结构,你可以对照自己手里的版本:
ds-lab/ ├── lab01-linear-list/ │ ├── seqlist.c │ ├── seqlist.h │ ├── linklist.c │ ├── linklist.h │ └── main.c ├── lab02-stack-queue/ │ ├── seqstack.c │ ├── linkqueue.c │ └── main.c ├── lab03-string/ │ ├── string.c │ └── main.c ├── lab04-tree/ │ ├── bitree.c │ ├── bitree.h │ └── main.c ├── lab05-graph/ │ ├── graph.c │ └── main.c ├── lab06-search-sort/ │ ├── search.c │ ├── sort.c │ └── main.c └── Makefile核心文件是每个实验的.c实现和对应.h声明,main.c只是驱动。很多人翻车在只编译main.c,结果报一堆未定义引用。正确做法是先看#include了哪些本地头文件,再把同目录的.c一起编。
2.2 用一条命令编译单个实验,避免全量报错干扰
不要一上来就make整个项目。先挑一个实验单独编译,把错误范围缩小。以 lab01 为例:
# 进入单个实验目录,只编译该实验相关源文件 cd ds-lab/lab01-linear-list # -g 保留调试符号,-Wall 打开常见警告,-o 指定输出名 gcc -g -Wall -o lab01 main.c seqlist.c linklist.c # 运行,观察默认测试输出 ./lab01逻辑说明:-g是为了后面用 gdb 或 IDE 打断点,没有它你只能看汇编。-Wall会暴露“未使用变量”“隐式声明”这类问题,数据结构代码里指针类型不匹配很常见,开着警告能提前发现。-o lab01把可执行文件固定命名,避免默认a.out覆盖。
参数说明:如果你的源码用了 C99 以上的特性(比如for(int i=0;...)),加-std=c99或-std=c11。如果用了数学库math.h,链接时补-lm,且-lm必须放在源文件后面,否则某些链接器会报未定义。
提示:编译顺序不影响结果,但
-lm的位置会影响。放在-o前面通常最稳。
2.3 用 Makefile 统一管理时,先确认变量指向
如果目录里有 Makefile,先别急着make。打开看三件事:CC是什么编译器、CFLAGS有没有-g、SRCS是不是用通配符抓了所有.c。常见写法:
CC = gcc CFLAGS = -g -Wall -std=c11 SRCS = $(wildcard lab*/*.c) OBJS = $(SRCS:.c=.o) TARGET = ds_all $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) -lm clean: rm -f $(OBJS) $(TARGET)逻辑说明:wildcard会把所有实验的.c都抓进来,如果不同实验里有同名函数(比如两个main),链接必然冲突。所以全量 Makefile 通常只适合“每个实验独立可执行”的写法,或者用条件编译隔离。遇到multiple definition of main,就回到 2.2 的单实验编译。
参数说明:$(SRCS:.c=.o)是后缀替换,把源文件列表变成目标文件列表。clean目标里的rm -f加-f是为了文件不存在时不报错。如果你在 Windows 上用 MinGW,rm可能不可用,改成del或直接用mingw32-make配合clean的cmd版本。
3. 线性表与链表实验:指针操作最容易翻车的地方
3.1 顺序表插入:为什么你的length总是多 1
顺序表实验里最经典的 bug 是插入位置判断和长度更新顺序。看一段常见实现:
// 在顺序表 L 的第 pos 个位置插入元素 e,pos 从 1 开始 int ListInsert(SqList *L, int pos, int e) { // 合法性检查:pos 范围是 1 到 length+1 if (pos < 1 || pos > L->length + 1) { return 0; // 插入失败 } // 如果表已满,直接返回失败 if (L->length >= MAXSIZE) { return 0; } // 从最后一个元素开始后移,直到 pos-1 下标位置腾出空位 for (int i = L->length; i >= pos; i--) { L->data[i] = L->data[i - 1]; } L->data[pos - 1] = e; // 写入新元素 L->length++; // 长度加一 return 1; }逻辑说明:循环从i = L->length开始,把data[i-1]搬到data[i],相当于把pos-1到length-1的元素整体后移一位。注意循环条件是i >= pos,不是i > pos,否则pos位置的原元素不会被移走。length++必须放在最后,如果提前加,循环上界就错了。
参数说明:pos是逻辑位置,从 1 开始;数组下标从 0 开始,所以写入用pos - 1。MAXSIZE通常是宏定义,比如 100 或 1000,改大改小会影响栈上数组大小,太大可能爆栈,建议超过 10000 时改用动态分配。
3.2 单链表头插与尾插:两种建表方式的输出顺序差异
单链表实验一般要求实现头插法和尾插法,两者输出顺序相反。头插法代码短,但很多人搞不清指针顺序:
// 头插法建立链表,输入 n 个元素,返回头指针 LinkList CreateListHead(int n) { LinkList head = (LinkList)malloc(sizeof(LNode)); head->next = NULL; // 初始化头结点 for (int i = 0; i < n; i++) { int x; scanf("%d", &x); LNode *p = (LNode *)malloc(sizeof(LNode)); p->data = x; p->next = head->next; // 新结点指向原首结点 head->next = p; // 头结点指向新结点 } return head; }逻辑说明:头插法每次把新结点插在头结点之后,所以输入顺序和链表顺序相反。p->next = head->next必须在head->next = p之前,否则原首结点会丢失,造成内存泄漏。尾插法则需要额外维护一个tail指针,每次tail->next = p; tail = p;,最后tail->next = NULL。
参数说明:n是输入元素个数,scanf的返回值最好检查,但实验代码常省略。malloc后必须判断是否为空,虽然实验环境很少失败,但养成习惯。头结点不存数据,可以简化插入删除的边界处理。
3.3 用 gdb 看指针变化:一条命令定位断链
链表断链是血泪经验里最常见的问题。编译时加-g,然后:
gdb ./lab01 (gdb) break CreateListHead (gdb) run (gdb) next (gdb) print head->next (gdb) print p (gdb) continue逻辑说明:break在函数入口下断点,next单步执行不进入函数,print查看指针值。如果head->next在某个next之后变成0x0或乱码,说明赋值顺序错了。continue放行到下一个断点。
参数说明:print可以跟表达式,比如print *head看结构体内容。如果指针是野指针,print可能报Cannot access memory,这时往回看哪一步没初始化。
4. 栈、队列与表达式求值:边界条件比算法本身更致命
4.1 顺序栈的 top 指针:指向栈顶元素还是栈顶下一个
顺序栈实验里,top的定义有两种:指向栈顶元素,或指向栈顶下一个空位。两种写法都能跑,但混用必崩。常见的是指向栈顶元素,初始top = -1:
// 入栈:先判断是否满,再移动 top,最后写入 int Push(SqStack *S, int e) { if (S->top == MAXSIZE - 1) { return 0; // 栈满 } S->top++; // top 先加 S->data[S->top] = e; // 再存值 return 1; } // 出栈:先判断是否空,再取值,最后减 top int Pop(SqStack *S, int *e) { if (S->top == -1) { return 0; // 栈空 } *e = S->data[S->top]; // 先取值 S->top--; // 再减 top return 1; }逻辑说明:入栈时top先加再存,出栈时先取再减,这样top始终指向栈顶元素。如果反过来,入栈先存再加,出栈先减再取,top就指向栈顶下一个空位,初始值应为 0。两种风格不能混,否则第一个元素就会错位。
参数说明:MAXSIZE是栈容量,top == MAXSIZE - 1表示满。出栈用int *e带回数据,调用时传&x。如果元素类型不是int,把int换成ElemType并同步改printf格式符。
4.2 循环队列的队空队满判断:少用一个元素空间
循环队列实验最容易被问“怎么区分空和满”。常见做法是少用一个存储单元:front == rear为空,(rear + 1) % MAXSIZE == front为满。
// 入队 int EnQueue(SqQueue *Q, int e) { if ((Q->rear + 1) % MAXSIZE == Q->front) { return 0; // 队满 } Q->data[Q->rear] = e; Q->rear = (Q->rear + 1) % MAXSIZE; return 1; } // 出队 int DeQueue(SqQueue *Q, int *e) { if (Q->front == Q->rear) { return 0; // 队空 } *e = Q->data[Q->front]; Q->front = (Q->front + 1) % MAXSIZE; return 1; }逻辑说明:rear指向下一个空位,front指向当前队头。入队先判满再写入,出队先判空再取值。取模运算保证下标循环。队列实际最多存MAXSIZE - 1个元素,这是“少用一个空间”的代价。
参数说明:MAXSIZE建议用宏或const int,不要硬编码在取模里。如果要求存满MAXSIZE个,需要额外加一个tag或size变量,实验里较少用。
4.3 表达式求值:栈混用的经典翻车点
表达式求值通常用两个栈:操作数栈和运算符栈。翻车点集中在优先级比较和括号处理。常见规则:遇到运算符,如果栈顶优先级不低于当前运算符,就先弹出栈顶运算符做一次运算,直到栈顶优先级更低或遇到左括号,再把当前运算符入栈。遇到右括号,一直弹到左括号。数字可能多位,要用循环拼。
// 简化版优先级:+ - 为 1,* / 为 2,( 为 0 int Priority(char op) { if (op == '+' || op == '-') return 1; if (op == '*' || op == '/') return 2; return 0; // 左括号或非法字符 }逻辑说明:优先级函数只负责返回数值,不处理括号匹配。主循环里遇到(直接入运算符栈,遇到)弹出直到(并丢弃。数字解析用while (isdigit(ch))累加。
参数说明:isdigit需要#include <ctype.h>。如果表达式含空格,先跳过。除法要判断除数为 0,实验数据一般不会给,但自己测试时要加。
5. 树与图实验:递归与非递归的取舍,以及遍历验证
5.1 二叉树递归遍历:三行代码背后的调用栈
二叉树先序、中序、后序递归遍历代码极短,但理解调用栈才能改非递归。先序:
// 先序遍历:根 -> 左 -> 右 void PreOrder(BiTree T) { if (T != NULL) { printf("%c ", T->data); // 访问根 PreOrder(T->lchild); // 递归左子树 PreOrder(T->rchild); // 递归右子树 } }逻辑说明:if (T != NULL)是递归出口。先序访问根在最前,中序在中间,后序在最后。三行顺序换一下就是另一种遍历。递归深度等于树高,极端情况下(单支树)可能栈溢出,实验数据一般规模小,问题不大。
参数说明:T是根结点指针,data类型按实验要求可能是char或int。printf格式符要对应,%c和%d别搞混。
5.2 非递归中序遍历:用栈模拟递归
非递归中序是实验常考。核心思路:一路向左入栈,直到空,弹栈访问,再转向右子树。
// 非递归中序遍历 void InOrderNonRec(BiTree T) { BiTree stack[100]; // 简单数组栈,存结点指针 int top = -1; BiTree p = T; while (p != NULL || top != -1) { while (p != NULL) { stack[++top] = p; // 当前结点入栈 p = p->lchild; // 一路向左 } if (top != -1) { p = stack[top--]; // 弹出栈顶 printf("%c ", p->data); // 访问 p = p->rchild; // 转向右子树 } } }逻辑说明:外层while条件是p非空或栈非空。内层while把左孩子全部压栈。弹出后访问,再让p指向右孩子,重复。栈用数组模拟,容量 100 对实验树足够,如果树结点超过 100,改大或改用链栈。
参数说明:stack存的是结点指针,top初始 -1。++top和top--是前置操作,保证先加后用。如果树为空,外层条件直接不成立,函数什么都不做。
5.3 图的邻接矩阵与邻接表:选哪个看边密度
图实验一般要求建图并遍历。邻接矩阵适合稠密图,邻接表适合稀疏图。实验里两种都可能要求实现。邻接矩阵建图:
// 邻接矩阵建无向图,n 个顶点,e 条边 void CreateMGraph(MGraph *G) { scanf("%d %d", &G->numVertexes, &G->numEdges); for (int i = 0; i < G->numVertexes; i++) { scanf("%c", &G->vexs[i]); // 顶点信息 } // 初始化矩阵为 0 for (int i = 0; i < G->numVertexes; i++) { for (int j = 0; j < G->numVertexes; j++) { G->arc[i][j] = 0; } } // 读入边,无向图对称赋值 for (int k = 0; k < G->numEdges; k++) { int i, j; scanf("%d %d", &i, &j); G->arc[i][j] = 1; G->arc[j][i] = 1; } }逻辑说明:先读顶点数和边数,再读顶点信息,初始化矩阵,最后读边。无向图arc[i][j]和arc[j][i]都要置 1。如果带权,把 1 换成权值,0 换成无穷大。
参数说明:scanf("%c")读字符时可能读到换行,前面加空格" %c"跳过空白。顶点下标从 0 开始,输入边时要注意。邻接表建图类似,只是用链表存邻接点。
5.4 DFS 与 BFS:访问标记数组别忘了初始化
图的遍历必须用visited数组防止重复访问。DFS 递归写法:
int visited[MAXVEX]; // 全局或传入 void DFS(MGraph G, int i) { visited[i] = 1; printf("%c ", G.vexs[i]); for (int j = 0; j < G.numVertexes; j++) { if (G.arc[i][j] == 1 && !visited[j]) { DFS(G, j); } } }逻辑说明:进入结点先标记已访问,再打印,然后遍历所有邻接点,未访问的递归。BFS 用队列,出队时把未访问邻接点入队并标记。visited必须在遍历前全部清零,否则结果缺失。
参数说明:MAXVEX是最大顶点数,visited大小要匹配。如果图不连通,外层还要循环所有顶点,对未访问的调用 DFS。
6. 查找与排序实验:数据规模一变,结论就变
6.1 折半查找:前提是有序,边界 mid 怎么算
折半查找实验通常先排序再查找。核心循环:
// 在有序数组 a 中查找 key,返回下标,找不到返回 -1 int BinarySearch(int a[], int n, int key) { int low = 0, high = n - 1; while (low <= high) { int mid = (low + high) / 2; // 可能溢出,但实验规模小 if (a[mid] == key) { return mid; } else if (a[mid] < key) { low = mid + 1; } else { high = mid - 1; } } return -1; }逻辑说明:low <= high是循环条件,等号不能少,否则最后一个元素找不到。mid用(low + high) / 2,如果low和high都很大可能溢出,更稳的写法是low + (high - low) / 2。low = mid + 1和high = mid - 1保证区间缩小。
参数说明:n是元素个数,a必须已升序。如果降序,比较方向反过来。查找失败返回 -1,调用方要判断。
6.2 快速排序:pivot 选不好,递归就爆栈
快速排序实验里,pivot 常选第一个元素。分区函数:
// 分区:返回 pivot 最终位置 int Partition(int a[], int low, int high) { int pivot = a[low]; // 选第一个元素为枢轴 while (low < high) { while (low < high && a[high] >= pivot) high--; a[low] = a[high]; // 比 pivot 小的移到左边 while (low < high && a[low] <= pivot) low++; a[high] = a[low]; // 比 pivot 大的移到右边 } a[low] = pivot; // 枢轴归位 return low; }逻辑说明:while (low < high)外层控制扫描。内层两个while分别从右往左找小于 pivot 的,从左往右找大于 pivot 的。赋值而不是交换,减少写操作。最后a[low] = pivot把枢轴放到正确位置。
参数说明:如果数组已经有序,选第一个元素为 pivot 会导致每次分区极不平衡,递归深度 O(n),可能爆栈。实验数据一般随机,但自己测试时要注意。改进方法是随机选 pivot 或三数取中。
6.3 排序算法对比:实验报告里该放什么数据
实验报告常要求对比不同排序算法的时间。下面这张表是我一般会填的,你可以按实际运行结果改:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 直接插入 | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔 | O(n^1.3) | O(n²) | O(1) | 不稳定 |
| 冒泡 | O(n²) | O(n²) | O(1) | 稳定 |
| 快速 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 简单选择 | O(n²) | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 归并 | O(n log n) | O(n log n) | O(n) | 稳定 |
逻辑说明:这张表用于实验报告对比,不是让你背。实际跑的时候,用clock()或gettimeofday测时间,数据规模取 1000、5000、10000,观察增长趋势。小规模下 O(n²) 和 O(n log n) 差别不明显,规模上去才拉开。
参数说明:clock()返回 CPU 时间,除以CLOCKS_PER_SEC得秒。如果数据量太小,时间接近 0,测不准。建议每个规模重复跑多次取平均。
7. 避坑与排查:数据结构实验里最常见的 5 个翻车现场
7.1 编译通过但运行崩溃:先查空指针和数组越界
现象:程序编译无警告,一运行就Segmentation fault。原因:链表操作中p->next时p为 NULL,或者顺序表插入时pos超出范围导致数组越界写。解决:在每次解引用指针前加if (p != NULL),在数组访问前检查下标。用 gdb 的backtrace看崩溃位置,print相关指针。
7.2 输出结果对但顺序不对:栈和队列的指针定义混了
现象:栈的入栈出栈结果看起来对,但和预期顺序相反。原因:top初始值和移动顺序不匹配,比如初始top = 0却按指向栈顶元素的方式写。解决:统一一种约定,要么top指向栈顶元素(初始 -1,先加后存),要么指向栈顶下一个空位(初始 0,先存后加)。改完检查所有入栈出栈函数。
7.3 递归遍历树时栈溢出:树退化成链表
现象:递归遍历较大二叉树时程序崩溃。原因:树是单支树,递归深度等于结点数,系统栈不够。解决:实验数据一般不会太大,如果自己构造了极端数据,改用非递归遍历,或者增大栈空间(不推荐)。更实际的做法是控制测试数据规模。
7.4 图遍历漏掉结点:visited 数组没清零
现象:DFS 或 BFS 输出结点数少于实际。原因:visited数组是全局或静态,上次遍历后没清零。解决:每次遍历前用memset(visited, 0, sizeof(visited))或循环清零。如果图不连通,外层循环所有顶点,对未访问的调用遍历。
7.5 排序结果不稳定:误用不稳定算法还要求稳定
现象:按成绩排序后,同分学生的原始顺序变了。原因:用了快速排序或堆排序,它们不稳定。解决:如果实验要求稳定,改用归并排序或插入排序。或者在比较时加入原始下标作为第二关键字,把不稳定算法“手动稳定化”。
8. 把源码变成自己的:改一个参数、加一个测试、写一份对照笔记
拿到这套源码,最值钱的用法不是跑通,而是改。我一般会挑三个地方动手:第一,把顺序表的MAXSIZE从 100 改成 10,然后插入 11 个元素,看它怎么报满,这样能记住边界判断写在哪。第二,把链表的头插法改成尾插法,只改指针操作,观察输出顺序变化,这样能分清两种建表的本质区别。第三,给排序算法加一个计数器,统计比较次数和移动次数,用同一组随机数据跑所有排序,把数字填进 6.3 那张表。做完这三步,实验报告里的“心得体会”就不用编了,你有一堆真实数据可写。
进阶一点,可以给每个实验写一个test.c,用assert验证边界。比如顺序表插入后检查length是否等于预期,链表删除后检查目标值是否真的不存在。这样改代码时不怕改坏。再进一步,把递归遍历改成非递归,把邻接矩阵改成邻接表,把冒泡改成快速,每改一个就对比输出是否一致。一致说明你理解对了,不一致就回去看指针和下标。
我自己的习惯是:每改一处,就在源码旁边用注释写一行“为什么改这里”,比如// 原版 top 指向栈顶下一个,改成指向栈顶元素,初始值 -1。过一周再回来看,这行注释比任何笔记都管用。数据结构实验的源码不是用来交差的,是用来拆的。拆得越碎,记得越牢。希望帮到你。
本文还有配套的精品资源,点击获取