简介:这份「实验-数据结构程序」压缩包面向计算机专业学生与算法初学者,围绕数据结构核心知识点提供成套编程练习,帮助读者通过动手实现排序、查找与链式结构来巩固课堂理论。包内共61个文件,以32个cpp源文件、13个头文件与16张参考书封面图为主,源码覆盖交换排序、选择排序、插入排序、折半查找、顺序查找、散列查找,以及单链表、链队列、顺序栈、二叉树、邻接矩阵与邻接表、串操作、对称矩阵压缩存储等典型实验,头文件与主程序分离,便于按模块编译调试。压缩包约4MB,已有315人学习。读者可据此获得可直接运行的实验代码、清晰的类与模板设计思路,以及对照教材章节逐项验证的练习素材,适合作为课程实验、期末复习与算法入门的参考。
1. 拆开“实验-数据结构程序.7z”:一个压缩包里到底该有什么
如果你从课程平台下载到一个叫“实验-数据结构程序.7z”的压缩包,第一反应大概率是解压看看里面有什么。但真正做过数据结构实验的人都知道,这个包的价值不在于“有多少个 .c 文件”,而在于它能不能让你在本地把线性表、栈、队列、树、图、排序、查找这些结构一个个跑通,并且对照实验报告把关键代码和运行结果串起来。我带过几届课程设计,也帮人排查过无数个“编译报错、运行崩溃、结果对不上”的实验包,最常见的翻车不是代码写不出来,而是环境没配好、输入数据格式不对、或者把参考代码直接当成能交的作业。这篇笔记就围绕这个压缩包,讲清楚它通常包含什么、怎么在本地复现、参数怎么调、哪些坑一踩一个准,以及怎么把它变成你自己的实验报告素材。适合正在做数据结构实验、准备期末复习、或者想用 C/C++ 把基础结构重新过一遍的人。
2. 先看清压缩包里的目录结构:实验-数据结构程序.7z 的典型组成
2.1 解压后先别急着编译,按这四类文件分堆
一个典型的数据结构实验包,解压后通常不会只有一个文件夹。我一般会先按用途分成四类:源码文件(.c / .cpp / .h)、工程文件(.dev、.cbp、.vcxproj、Makefile)、数据文件(.txt、.in、.dat)、文档文件(.doc、.pdf、.md)。分堆的目的是避免把“参考代码”和“你要提交的代码”混在一起。很多包里的源码是按实验序号命名的,比如exp1_seqlist.c、exp2_stack.c、exp3_bintree.c,但也有的包用中文名或者拼音缩写,这时候先看文档里的实验指导书,确认每个文件对应哪个实验。
分堆之后,优先打开文档文件。实验报告模板通常会写明每个实验的输入输出要求、测试用例格式、以及需要截图的运行结果。这一步花十分钟,能省掉后面两小时的瞎试。如果包里没有文档,那就看源码开头的注释块,一般会写“实验名称、作者、日期、功能描述”。
2.2 用 tree 命令或资源管理器导出文件清单
在 Windows 上可以直接用资源管理器看,但在 Linux 或 macOS 上,我习惯用tree把结构打出来,方便对照。如果没有 tree,用find也行。
# 在解压后的目录下执行,列出两层目录和文件 find . -maxdepth 2 -type f | sort # 如果有 tree 命令,直接看结构更直观 tree -L 2 -a逻辑说明:find . -maxdepth 2 -type f只列文件不列目录,避免输出太乱;sort让同名实验的文件排在一起。参数-maxdepth 2表示只看两层,因为数据结构实验包一般不会嵌套太深。如果输出里出现.o、.exe、.out这类编译产物,说明这个包被人编译过,最好先清理掉再重新编译,避免旧目标文件干扰。
2.3 确认编译器和标准:C 还是 C++,C89 还是 C11
数据结构实验包最常见的是 C 语言,但也有用 C++ 的(比如用iostream和template)。看文件后缀:.c用 gcc,.cpp用 g++。如果源码里用了//注释和变量随时声明,那至少是 C99;如果用了for (int i = 0; ...),也是 C99 起。老教材配套的代码可能是 C89,变量必须声明在块首,这时候用新编译器反而会报错。
我一般会先试编译一个最小的文件,看报错信息判断标准。比如:
# 试编译线性表实验,输出到 /tmp 避免污染目录 gcc -std=c99 -Wall -Wextra -o /tmp/exp1 exp1_seqlist.c # 如果是 C++ 文件 g++ -std=c++11 -Wall -Wextra -o /tmp/exp1 exp1_seqlist.cpp参数说明:-std=c99指定标准,-Wall -Wextra打开常用警告,能提前发现未初始化变量、类型不匹配等问题。如果报错说for循环里声明变量有问题,就换成-std=c89或-std=gnu89。如果报错说找不到bool类型,就加#include <stdbool.h>或者换 C99。
提示:不要一上来就改源码去迎合编译器,先确认教材对应的标准。很多实验报告要求“代码能在 Dev-C++ 里编译”,而 Dev-C++ 默认用的是较老的 gcc,标准不一致会导致你本地能跑、老师那边跑不了。
3. 把线性表、栈、队列跑通:最小可复现的编译与测试流程
3.1 线性表实验:顺序表与链表的编译差异
顺序表实验通常只有一个.c文件,里面定义了SqList结构体和ListInsert、ListDelete、LocateElem等函数。编译时直接 gcc 即可。但链表实验经常拆成linklist.c和main.c,或者把结构体定义放在linklist.h里。这时候编译要一起编:
# 顺序表:单文件编译 gcc -std=c99 -Wall -o /tmp/seqlist seqlist.c /tmp/seqlist # 链表:多文件编译 gcc -std=c99 -Wall -o /tmp/linklist linklist.c main.c /tmp/linklist逻辑说明:多文件编译时,如果头文件在当前目录,gcc 默认会找,不用加-I.。但如果头文件在include/子目录,就要加-Iinclude。运行的时候,如果程序需要从键盘输入数据,直接运行后按提示输入即可;如果程序从文件读数据,就要确认数据文件路径。
参数说明:-o /tmp/seqlist把可执行文件放到 /tmp,避免在实验目录里生成一堆.exe。如果你在 Windows 上,可以改成-o seqlist.exe。-Wall一定要加,链表实验里最常见的警告是“赋值从指针目标类型中丢弃限定符”,这通常意味着你试图修改const指针指向的内容。
3.2 栈和队列:注意循环队列的队满判断
栈的实验一般比较直接,Push、Pop、GetTop写对就行。队列实验的坑集中在循环队列:教材里通常用“少用一个元素空间”来区分队空和队满,即front == rear为队空,(rear + 1) % MAXSIZE == front为队满。如果你按“增加一个 size 变量”或“设置 tag 标志”来写,也能跑,但实验报告里要说明你的判断方式,否则老师可能按教材标准扣分。
// 循环队列的队满判断(教材常见写法) #define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int front; int rear; } SqQueue; int QueueFull(SqQueue *q) { return (q->rear + 1) % MAXSIZE == q->front; } int QueueEmpty(SqQueue *q) { return q->front == q->rear; }逻辑说明:(q->rear + 1) % MAXSIZE == q->front表示 rear 再走一步就追上 front,此时认为队满。这样队列最多存MAXSIZE - 1个元素。如果你改成存MAXSIZE个,就需要额外的标志位,否则无法区分空和满。参数MAXSIZE一般设为 100 或 50,按实验要求改。
3.3 用重定向和管道做批量测试
手动输入测试数据很累,而且容易输错。我一般会把测试用例写进文本文件,然后用重定向运行:
# 把测试输入写进 input.txt,每行一个操作或一个数据 # 例如:先输入元素个数,再输入元素,再输入插入位置和值 cat > input.txt << 'EOF' 5 10 20 30 40 50 3 99 EOF # 运行程序并从文件读输入 /tmp/seqlist < input.txt # 如果程序输出很多,可以重定向到文件再对比 /tmp/seqlist < input.txt > output.txt diff output.txt expected.txt逻辑说明:<是输入重定向,>是输出重定向。diff用来对比你的输出和预期输出,如果完全一致就说明逻辑正确。参数方面,input.txt的格式必须和程序里的scanf顺序完全对应,否则会读错。常见错误是程序里先读n再读n个元素,但测试文件里先写了元素再写n,导致数据错位。
注意:有些实验程序在输出时会打印菜单,比如“1. 插入 2. 删除 3. 查找”,这时候输入文件里要包含菜单选项。我一般会先手动跑一遍,把完整的输入序列记下来,再写成文件。
4. 树、图、排序、查找:实验包里最容易翻车的四个模块
4.1 二叉树实验:递归建树和遍历的输入格式
二叉树实验通常要求“按先序序列建立二叉树”,输入是一个扩展先序序列,用#表示空节点。比如AB#D##C##表示根 A,左子 B,B 的右子 D,右子 C。很多人在这一步翻车,是因为输入字符串里带了空格或换行,导致scanf("%c", &ch)读到了空白字符。
// 扩展先序建树 typedef struct BiTNode { char data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; void CreateBiTree(BiTree *T) { char ch; scanf("%c", &ch); if (ch == '#') { *T = NULL; } else { *T = (BiTree)malloc(sizeof(BiTNode)); (*T)->data = ch; CreateBiTree(&(*T)->lchild); CreateBiTree(&(*T)->rchild); } }逻辑说明:递归建树时,scanf会依次读取每个字符,包括换行符。如果输入是AB#D##C##后面跟一个回车,那么最后一个#读完后,下一次递归会读到回车符,导致建树错误。解决办法是在scanf前加while ((ch = getchar()) == '\n');或者在输入格式里明确不换行。参数方面,#是空节点标志,可以改成其他不冲突的字符,但实验报告里要说明。
4.2 图实验:邻接矩阵和邻接表的转换
图实验一般要求实现邻接矩阵和邻接表两种存储结构,并实现 DFS 和 BFS。邻接矩阵的输入通常是顶点数和边数,然后每行一条边。邻接表的输入格式类似,但需要动态分配边节点。最常见的坑是顶点编号从 0 开始还是从 1 开始,以及无向图每条边要存两次。
// 邻接矩阵建图(无向图) #define MAXV 100 typedef struct { int edges[MAXV][MAXV]; int vexnum, edgenum; } MGraph; void CreateMGraph(MGraph *G) { int i, j, k; scanf("%d %d", &G->vexnum, &G->edgenum); for (i = 0; i < G->vexnum; i++) for (j = 0; j < G->vexnum; j++) G->edges[i][j] = 0; for (k = 0; k < G->edgenum; k++) { scanf("%d %d", &i, &j); G->edges[i][j] = 1; G->edges[j][i] = 1; // 无向图对称 } }逻辑说明:无向图的邻接矩阵是对称的,所以edges[i][j]和edges[j][i]都要置 1。如果是有向图,只置edges[i][j]。参数MAXV是最大顶点数,一般设 100。如果顶点编号从 1 开始,数组要开到MAXV + 1,并且循环从 1 开始。DFS 和 BFS 的代码要配合visited数组,每次遍历前清零。
4.3 排序实验:比较次数和移动次数的统计
排序实验通常要求实现直接插入排序、冒泡排序、快速排序、简单选择排序、堆排序等,并统计比较次数和移动次数。坑在于:不同教材对“比较”和“移动”的定义不同。比如快速排序的枢轴比较算不算一次比较?堆排序的筛选过程中,父子比较算几次?我一般会按教材的伪代码逐行对应,在关键操作前后加计数器。
// 冒泡排序统计比较和移动 void BubbleSort(int a[], int n, int *compare, int *move) { int i, j, temp; *compare = 0; *move = 0; for (i = 0; i < n - 1; i++) { for (j = 0; j < n - 1 - i; j++) { (*compare)++; if (a[j] > a[j + 1]) { temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; (*move) += 3; // 一次交换算 3 次移动 } } } }逻辑说明:(*compare)++在每次比较前加一,(*move) += 3表示一次交换涉及三次赋值。有些教材把交换算作一次移动,那就改成(*move)++。参数n是元素个数,compare和move是输出参数。测试时用同一组随机数据跑不同排序,对比统计结果,能直观看出算法差异。
4.4 查找实验:哈希表的冲突处理
查找实验通常要求实现顺序查找、折半查找和哈希查找。哈希查找的坑在于冲突处理方法:线性探测、二次探测、链地址法。教材里常用除留余数法加线性探测。注意装填因子和表长的选择,表长一般取质数,装填因子控制在 0.75 以下。
// 哈希表线性探测 #define HASHSIZE 13 typedef struct { int key; int flag; // 0 空,1 占用 } HashTable; int Hash(int key) { return key % HASHSIZE; } int SearchHash(HashTable ht[], int key) { int addr = Hash(key); while (ht[addr].flag == 1 && ht[addr].key != key) { addr = (addr + 1) % HASHSIZE; } if (ht[addr].flag == 1 && ht[addr].key == key) return addr; return -1; }逻辑说明:Hash函数用除留余数法,HASHSIZE取质数 13 能减少冲突。线性探测的步长为 1,每次addr = (addr + 1) % HASHSIZE。查找时如果遇到空位就说明不存在,但要注意删除操作不能简单置空,否则会截断探测链,通常用标记flag = -1表示已删除。参数HASHSIZE按实验要求改,一般取 11、13、17 等质数。
5. 避坑与排查:实验-数据结构程序.7z 最常见的五个翻车现场
5.1 现象:编译报错“undefined reference tomalloc”或“implicit declaration of function”
原因:没有包含<stdlib.h>或<stdio.h>。老教材的代码有时省略头文件,但新编译器默认不允许隐式声明。解决:在文件开头补上#include <stdlib.h>和#include <stdio.h>。如果报错说bool未定义,补#include <stdbool.h>。
5.2 现象:程序运行后闪退,或者输出“段错误 (核心已转储)”
原因:指针未初始化、数组越界、或malloc后没检查返回值。链表实验里最常见的是p = p->next时p已经是 NULL。解决:用 gdb 或加打印语句定位。编译时加-g,然后gdb /tmp/linklist,运行后bt看调用栈。另一个办法是在每个指针解引用前加assert(p != NULL)。
5.3 现象:输入数据后程序卡住,或者读入的数据和预期不符
原因:scanf的格式字符串和输入不匹配,或者输入缓冲区里有残留的换行符。比如先scanf("%d", &n)再scanf("%c", &ch),ch会读到换行符。解决:在%c前加空格,写成scanf(" %c", &ch),空格会跳过空白字符。或者用getchar()吃掉换行符。
5.4 现象:排序结果正确,但比较次数和教材答案对不上
原因:统计口径不同。有的教材把“比较”定义为关键字比较,有的把循环条件判断也算进去。解决:对照教材伪代码,逐行数。如果实在对不上,就在实验报告里说明你的统计方式,并给出你的统计结果。老师一般更看重你理解算法,而不是死磕数字。
5.5 现象:在 Dev-C++ 里能跑,在 VS Code 或命令行里报错
原因:Dev-C++ 默认用较老的 gcc 和 C89 标准,而 VS Code 可能用 gcc 最新版,默认 C17。解决:统一用-std=c99或-std=c89编译。如果代码里用了gets,新编译器会报错,改成fgets。如果用了strcpy不检查长度,加-D_CRT_SECURE_NO_WARNINGS只在 MSVC 下有用,gcc 下要手动改。
提示:实验包里的代码如果是从网上抄的,很可能带有作者的个人习惯,比如用
void main()而不是int main()。新编译器会警告甚至报错,改成int main()并在末尾return 0;。
6. 把实验包变成自己的东西:改造、验证与提交前的检查清单
6.1 用随机数据做对拍验证
不要只跑教材上的那组测试数据。我一般会写一个简单的随机数据生成器,然后拿你的实现和教材参考实现(或者标准库的qsort)做对拍。比如排序实验,生成 1000 个随机整数,分别用你的冒泡和qsort排序,对比结果是否一致。
// 生成随机数据并写入文件 #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand(time(NULL)); int n = 1000; FILE *fp = fopen("random.txt", "w"); fprintf(fp, "%d\n", n); for (int i = 0; i < n; i++) { fprintf(fp, "%d ", rand() % 10000); } fclose(fp); return 0; }逻辑说明:srand(time(NULL))用当前时间做种子,保证每次生成不同数据。rand() % 10000生成 0 到 9999 的整数。把数据写入文件后,用你的排序程序读入并输出,再用sort命令或另一个程序排序,对比结果。参数n可以改成 100、10000,测试不同规模。
6.2 提交前检查:文件命名、注释、运行截图
实验报告通常要求提交源码和报告。源码文件名要按老师要求,比如学号_姓名_实验1.c。注释至少包含:实验名称、作者、日期、功能描述、输入输出说明。运行截图要清晰,包含输入数据和输出结果。如果报告里要求贴关键代码,不要整段复制,只贴核心函数,并加上自己的理解。
6.3 一个我常犯的错:忘了释放内存
链表、树、图实验里大量使用malloc,但很多人写完就交,忘了free。虽然实验程序运行完就退出,操作系统会回收内存,但实验报告里如果被问到“内存管理”,这就是扣分点。我一般会写一个DestroyList或DestroyTree函数,递归释放所有节点。这不仅是好习惯,也能帮你理解指针和递归。
// 递归释放二叉树 void DestroyTree(BiTree T) { if (T != NULL) { DestroyTree(T->lchild); DestroyTree(T->rchild); free(T); } }逻辑说明:后序遍历的顺序释放,先释放左右子树,再释放根节点。参数T是树根指针。如果树很大,递归可能导致栈溢出,可以改成非递归用栈模拟,但实验规模一般不大,递归足够。
6.4 把实验包当复习提纲:按知识点重新组织
如果你是为了期末复习或考研,不要只跑一遍代码。我习惯把实验包里的每个结构对应到王道数据结构或严蔚敏教材的章节,然后自己默写核心操作。比如线性表的插入删除、二叉树的三种遍历、图的 DFS/BFS、快速排序的 partition、哈希表的查找。默写不出来就回去看代码,直到能独立写出。这个压缩包最大的价值不是代码本身,而是它提供了一个可运行的验证环境,让你能立刻看到自己写的逻辑对不对。
最后说一个我的习惯:每次做完实验,我会把源码里的关键函数复制到一个单独的review.c文件里,去掉菜单和输入输出,只保留数据结构和算法,然后自己写测试用例。这样复习的时候不用在菜单里点来点去,直接跑测试就能验证。希望帮到你。
本文还有配套的精品资源,点击获取