简介华中科技大学数据结构实验C语言实现包含顺序表、单链表、二叉树与邻接表无向图四个核心主题适合高校计算机专业学生及自学数据结构的开发者对照练习。资源包共4个.c源文件压缩后仅17KB代码简洁集中便于直接阅读与二次修改。每个实验均覆盖核心操作顺序表的创建、插入、删除与查找单链表的头插、遍历及节点管理二叉树的递归构造与前中后序遍历邻接表实现无向图的建立与深度/广度优先搜索。通过这组实验代码读者可深入理解线性表、树形结构和图的基本存储方式掌握C语言中数组、指针和递归的实际应用同时提升算法实现与调试能力。目前已有574人学习下载适合作为课程实验参考或期末复习的配套资料。1. 数据结构实验不只是做题:这份资源解决的是从思路到能跑的完整链路很多人拿到数据结构实验的第一反应是去翻课本、找算法描述,结果卡在「思路懂了,代码却编译不过」或者「本地跑通一两个用例,一交上去就崩」。这份资源的核心价值不在题目本身,而在于给了一套可以直接落地的代码骨架、工程结构和调试节奏——它把「抽象数据结构」和「C 语言实现」之间的空隙填上了,让新手能照着写,熟手能省掉重复搭框架的时间。它适合正在刷实验报告的人,也适合想快速回忆起某类数据结构怎么用 C 表达的老手。目录里不会有玄学,全是踩过坑之后留下来的东西。2. 实验前准备:先把工程框架立起来,而不是急着写算法数据结构实验最常见的翻车方式,是打开 IDE 写一个几百行的单文件,然后在一个函数里堆完所有逻辑。这种写法不是不能跑,但排查问题、验收样例、二次修改的时候,基本等于在黑匣子里猜。这一章先把地基打好。2.1 工程骨架:五个文件的职责与依赖我一般会按「声明、实现、测试、工具」四个维度拆文件,这对后续每个实验都通用。资源包里给的工程骨架大概是这样的:exp1_sequence/ ├── include/ │ └── seqlist.h # 头文件:结构体定义、函数声明、宏定义 ├── src/ │ ├── seqlist.c # 实现文件:顺序表的插入、删除、合并 │ └── main.c # 主文件:读入数据、调用接口、输出结果 ├── tests/ │ └── sample1.txt # 测试用例:多组输入,按格式给出 ├── build/ │ └── Makefile # 编译脚本,统一处理头文件路径和输出位置 └── README.md # 说明每个实验的编译命令、输入格式、验收点这个结构不是摆设。include 放声明、src 放实现,能让编译器帮你检查「函数签名是否一致」,而不是靠肉眼比对;tests 目录单独存在,是因为数据结构实验的验收几乎都是「给输入、比对输出」,没有测试用例就没法判断代码是否真的符合要求;build 下的 Makefile 则是为了把编译命令固化成一行make。常见做法是先写头文件,再写实现,最后写 main,顺序反了会导致你边写边补声明,头文件越补越乱。参数说明里值得注意的一点:头文件里必须有#ifndef或#pragma once之类的防重复包含宏,否则两个实现文件互相 include 的时候,结构体定义会重复报错。资源里的模板直接把这段写好了,照抄即可。2.2 输入输出规范:多组样例与文件验收数据结构实验的输入输出坑得很隐蔽。题目描述里写着「输入包含多组数据,每组占一行」,但多数人只在 main 里写了一个scanf(%d, n),跑一组样例就结束了,验收脚本一跑多组就直接卡死或输出错误。正确写法是先理解验收系统的工作方式——它会把一个样例文件整体喂给你的程序,然后逐行比对输出。样例文件长这样:5 1 3 5 7 9 3 2 4 6这表示第一行是第一个序列的长度,第二行是元素,第三行是第二个序列的长度,依此类推。你的 main 需要用while (scanf(...) ! EOF)的结构去循环读,而不是假设只有一组。int n1, n2; while (scanf(%d, n1) 1) { // 读入第一组的 n1 个元素 for (int i 0; i n1; i) scanf(%d, seq1[i]); // 读入第二组的 n2 个元素 scanf(%d, n2); for (int i 0; i n2; i) scanf(%d, seq2[i]); // 调用合并或处理函数 merge(seq1, n1, seq2, n2, result, result_len); // 按要求的格式输出 for (int i 0; i result_len; i) printf(%d , result[i]); printf(\n); }这段代码的逻辑是:把scanf的返回值当成循环条件——返回 1 表示成功读到一个整数,如果是 EOF 就退出。注意为什么不用! EOF直接判断:当输入里出现非数字字符时,scanf会返回 0,但文件并没有结束,可能陷入死循环。取 1能保证只处理「确实读到了数字」的情况。还有一个细节:每组输出之间到底是换行还是空格,必须严格按照题目要求,验收时多一个空格都不行,这是血泪经验。提示:本地调试时,用./program tests/sample1.txt这种方式把样例文件重定向进程序,比手动一个个敲输入靠谱得多。3. 顺序表与二叉树:两个经典实验的代码骨架这一章直接给代码,但重点不是抄,而是告诉你每段代码对应的数据结构思维在哪里。3.1 顺序表的多项式合并:循环别写成 while多项式合并是顺序表实验里最常见的题目。核心思路是用数组模拟多项式,数组下标是次数,元素值是对应项的系数。合并的过程就是两个数组相同下标相加。typedef struct { int coef; // 系数 int exp; // 指数 } PolyTerm; void add_poly(PolyTerm a[], int len_a, PolyTerm b[], int len_b, PolyTerm result[], int *len_result) { int i 0, j 0, k 0; while (i len_a j len_b) { if (a[i].exp b[j].exp) { result[k].coef a[i].coef b[j].coef; result[k].exp a[i].exp; i; j; k; } else if (a[i].exp b[j].exp) { result[k] a[i]; i; k; } else { result[k] b[j]; j; k; } } // 把剩余项直接拷到结果末尾 while (i len_a) { result[k] a[i]; i; k; } while (j len_b) { result[k] b[j]; j; k; } *len_result k; }这段代码最关键的逻辑在a[i].exp b[j].exp这个分支上——它假设两个多项式都已经按指数降序排好了。很多人会在没排序的情况下去合并,结果输出乱了。因此调用前必须先做一次排序或要求输入本身就是有序的。代码里没有用 for 循环是因为两个数组长度不同,用 while 才能处理「其中一个先走完」的情况。最后两个 while 负责把剩下的项接上,这是很容易漏的一个点。另外一个坑在系数相加后为 0 的情况,比如3x^2 (-3x^2)。如果直接把结果写进数组,后续输出会出现一个0x^2的项。常规做法是加一个判断,coef 0时不写入、也不递增k。这里留给读者自行改,改动成本很低。3.2 二叉树的递归遍历与哈夫曼编码:先画图再写码二叉树实验的题目通常是「给定前序和中序,重建二叉树,输出后序」或「构造哈夫曼树并输出编码」。前者考递归理解,后者考建树顺序。重建二叉树的核心逻辑是:前序的第一个节点一定是根,在中序里找到根的位置,左边是左子树、右边是右子树,然后递归处理左右两个区间。TreeNode* build_tree(int preorder[], int pre_start, int pre_end, int inorder[], int in_start, int in_end) { if (pre_start pre_end) return NULL; int root_val preorder[pre_start]; int root_idx -1; // 在中序里找根的位置 for (int i in_start; i in_end; i) { if (inorder[i] root_val) { root_idx i; break; } } TreeNode *root (TreeNode *)malloc(sizeof(TreeNode)); root-val root_val; int left_len root_idx - in_start; root-left build_tree(preorder, pre_start 1, pre_start left_len, inorder, in_start, root_idx - 1); root-right build_tree(preorder, pre_start left_len 1, pre_end, inorder, root_idx 1, in_end); return root; }参数说明:left_len是左子树的节点数量,由中序里根的位置推出来。前序里根的后面left_len个元素都属于左子树,再后面才是右子树。很多人会把pre_start 1直接写成左子树的起点,但如果左子树长度不为 1,这个起点后的区间边界就会错。这里的malloc每递归一层就申请一次内存,最后不用了要记得在外部统一free,否则内存泄漏在数据结构实验里是会被扣分的。哈夫曼编码的坑在另外一处:题目会让你输出「带权路径长度」,这个值可以在建树过程中累加,不需要真的遍历树。每次从优先队列里取出两个最小权值节点合并,合并后的权值累加到 WPL 上,直到只剩一个根节点。如果用 C 写优先队列嫌麻烦,可以直接用数组加循环找最小值,数据量小的时候性能没什么差别,但代码量会少很多。4. 图与排序:把最难啃的骨头拆成接口图论和数据排序这两块,是数据结构实验里最容易让人写一半就放弃的。原因是代码量大、错误隐蔽。但其实只要把接口想清楚,写起来并不吓人。4.1 图的邻接表实现:三个结构体搞定邻接表是图实验首选的存储方式,三个结构体就能撑起一张有向图:typedef struct EdgeNode { int adj_vertex; // 邻居顶点编号 int weight; // 边的权值,无权图可以置 1 struct EdgeNode *next; // 下一条边 } EdgeNode; typedef struct VertexNode { int data; // 顶点信息,一般是编号或名称 EdgeNode *first_edge; // 第一条出边 } VertexNode; typedef struct { VertexNode vertices[100]; // 顶点数组 int vertex_num, edge_num; // 顶点数和边数 } Graph;代码的含义一眼就能看懂:每个顶点挂一个链表,链表里的每个节点是一条边。为什么不用邻接矩阵?因为矩阵的空间复杂度是 O(V²),顶点一多就浪费;而且很多实验题问的是「从某点出发能走到哪」,邻接表天然支持这种遍历。常数100是题目里顶点的上限,一般够用,如果题目说有 10 万顶点,这个数组就得改成动态分配的。图的遍历(DFS/BFS)代码这里不贴全,因为骨架都一样:DFS 用栈(或递归)挨个访问邻居,BFS 用队列。唯一值得注意的坑是「访问标记」——很多人写完忘了建visited数组,导致死循环。我在自己的代码里习惯把它定义为int visited[100],每次测试前用memset清零,而不是写在函数内部;写在函数内部的话,递归层数一深,局部变量反复声明,很容易出逻辑错。4.2 排序接口统一:一个函数指针数组切换排序实验通常要求实现冒泡、插入、归并、快排等,然后比较时间复杂度。如果你每个排序都写一个独立的测试 main,那会累死。更清晰的做法是定义统一的排序函数类型:typedef void (*SortFunc)(int arr[], int n); void bubble_sort(int arr[], int n) { /* ... */ } void quick_sort(int arr[], int n) { /* ... */ } void merge_sort(int arr[], int n) { /* ... */ } void test_sort(SortFunc func, int data[], int n) { int *copy (int *)malloc(n * sizeof(int)); memcpy(copy, data, n * sizeof(int)); func(copy, n); if (is_sorted(copy, n)) { printf(通过\n); } else { printf(排序结果错误\n); } free(copy); }这段代码的核心是SortFunc这个函数指针类型。它把「排序算法」抽象成一个参数,test_sort接收一个函数进去,然后自动跑一遍验证结果。好处是你只写一份测试逻辑,就能测所有排序算法;换算法时只需要改一行调用。需要提醒的是memcpy的第三个参数:不是n,而是n * sizeof(int)。这个错几乎是新手必犯的,memcpy按字节复制,你只给它n的话,它复制了 n 个字节,对 int 数组来说只复制了四分之一。这就是典型的「知道函数概念,但参数没查清楚」的坑。输出语句里的「通过」建议改成具体的耗时或比较次数,这样实验报告里能写清楚。5. 避坑与排查:四个常见事故与调试三步法这一章是资源里最值钱的部分,因为大部分人的问题不是不懂原理,而是栽在那些「看起来没错」的细节上。5.1 四个最容易翻车的运行时错误现象 1:程序输出「段错误」,一运行就崩。原因是访问了数组越界位置。场景通常是读入数据时,for (int i 0; i n; i)把写成了多一次循环。或者图实验里,顶点编号从 1 开始,而数组下标从 0 开始,你直接用顶点编号访问vertices[vertex],当编号等于 n 时就越界。解决:所有数组访问先检查下标范围。我在每次scanf后加一条if (v 1 || v n) continue;的守卫,虽然不优雅,但能立刻止损。现象 2:输出比样例多一个换行,验收时判错误。原因是 printf 里多打了\n,或者printf(\n)放在了循环内部而不是循环外。解决:比对样例文本,尤其看最后一行的换行是否与题目一致。本地可以用diff命令做精确比对,方法见 5.2。现象 3:递归程序崩溃,但偶尔能跑。原因是递归基写错,导致无限递归,以及结构体里的指针没有被正确初始化。二叉树的build_tree里,如果pre_start pre_end时返回 NULL,但左子树区间计算有问题,递归就会无休止地塌陷。解决:先在纸上写出前序和中序的区间边界,标出 start/end 的含意,再对照代码检查。这个建议听起来笨,但确实是最有效的。现象 4:排序结果在数据少时正确,多组时错误。原因是多组样例共用了同一块内存,上一组的数据残留到了下一组。最常见的是没重置result_len或没对数组清零。解决:在每次循环开头重置result_len 0,如果用了局部数组做缓存,直接memset(tmp, 0, sizeof(tmp));。5.2 调试三步法:打印、断言、diff不要用 debugger 逐行单步,在数据结构实验这个规模下,打印大法效率最高。具体三步走。第一步,在关键函数入口和出口各打印一遍状态。比如合并函数,进函数时打印进入 add_poly, len_a3 len_b2,出函数时打印result_len4。这样你能快速判断是调用前数据错了,还是函数内部逻辑错了。printf(进入 add_poly: n1%d n2%d\n, len_a, len_b); // 函数主体... printf(退出 add_poly: result_len%d\n, *len_result);第二步,用 assert 做前置条件检查。C 标准库的assert宏会在条件不成立时直接终止并告诉你在哪一行。比如合并前检查两个多项式是否有序:assert(is_sorted_by_exp(a, len_a)); assert(is_sorted_by_exp(b, len_b));如果宏挂了,说明错误发生在调用方——你在没排序的情况下就把数据送进了函数。这比在合并内部慢慢找快得多。第三步,用 diff 做输出比对。把样例文件重定向进程序,输出到一个临时文件,再与标准答案比对:./exp1 tests/sample1.txt out.txt diff -y out.txt ans.txtdiff -y会并排显示左右两列,有差异的行会标出来;如果没有任何输出,说明全过。注意 ans.txt 必须是严格按题目格式手写的答案,不要从样例说明里复制粘贴——格式对不上时会误导你。提示:本地跑得过不代表验收能过,因为验收用的测试数据通常更隐蔽。跑完样例后,自己补一组边界数据:空输入、单元素、最大长度、重复元素。6. 进阶验证:写一个自动检查脚本,把实验收尾变轻松最后一个技巧,是把上面三步走「自动化」——写一个小脚本,对每个实验都做「输入重定向 内存检查 输出比对」,跑一遍就知道自己代码是否有问题。这个脚本我觉得是这套资源里最实用的一部分,省掉无数手动重复劳动。#!/bin/bash # check.sh —— 自动编译、运行、比对,顺带查内存泄漏 make clean /dev/null make /dev/null for testfile in tests/*.txt; do echo 运行 $testfile ./program $testfile /tmp/output.txt 2/tmp/error.txt if [ -s /tmp/error.txt ]; then echo 程序有运行时错误信息: cat /tmp/error.txt exit 1 fi # 和标准答案比对 ansfile${testfile%.txt}_ans.txt if [ -f $ansfile ]; then diff -y /tmp/output.txt $ansfile || exit 1 else echo 没有找到标准答案文件 $ansfile,跳过比对 fi done # valgrind 查内存泄漏,有输出说明有泄漏 valgrind --leak-checkfull ./program tests/sample1.txt /dev/null 2/tmp/valgrind.txt if grep -q definitely lost /tmp/valgrind.txt; then echo 存在内存泄漏,详见 /tmp/valgrind.txt exit 1 fi echo 所有测试通过脚本的逻辑不复杂:先把代码重新编译,确保用的是最新版本;然后遍历 tests 目录下所有测试文件,逐一运行并比对;最后单独用 valgrind 跑一次,检查是否有内存泄漏。有个细节值得学——答案文件的命名约定,sample1.txt对sample1_ans.txt。这保证你用ansfile${testfile%.txt}_ans.txt这个表达式就能自动找到对应答案文件,不用每次手写文件名。对于不用 valgrind 的环境,可以退而求其次,在 main 的最末尾自己打印一句「程序正常结束」,如果能看到这句,说明没有提前exit或崩溃。但 valgrind 能抓到的「内存泄漏」是多出来的:一旦你在循环里每轮 malloc 一次而忘记了释放,最后 valgrind 会报告definitely lost。实验报告里加上这个检查项,审阅时会放心很多。我自己的习惯是拿到任何实验资源,先把 check.sh 改成每台机器都能跑的样子,再开始写代码。从那以后,写一个实验就顺手跑一次bash check.sh,改代码再也不怕「改完这处坏了那处」。希望帮到你。本文还有配套的精品资源点击获取