☰
数据结构实验报告写作指南:从实验设计到数据分析
2026/10/14 10:12:10 网站建设 项目流程

简介:这是一份面向高校计算机相关专业学生的《数据结构实验报告》文档,聚焦链表与二叉树两大核心知识点,适合初学数据结构的本科生用于实验参考、复习备考或课堂作业对照。报告以C语言实现两个完整实验:实验一讲解单链表、循环链表与双链表概念,给出无头结点单链表合并为有序链表的插入删除操作及源码;实验二讲解二叉树前序、中序、后序遍历,并演示如何将动态二叉树转换为静态二叉链表。文档包含实验目的、步骤、源代码、运行截图与总结,能帮助读者理解算法思想与数据结构描述,掌握指针操作和递归遍历的常见易错点。压缩包共1个doc文件,大小约134KB。目前已有477人浏览学习,适合需要快速了解链表和二叉树实验写法及C语言实现的同学下载参考。

1. 数据结构实验报告:先把实验做成闭环,再谈怎么写文档

「数据结构实验报告」这门课业,最容易翻车的地方不在写文档,而在把实验当成“代码贴上去、截图跑一下、抄段书上的结论”三个动作。数据结构实验真正训练的是你用一个可控制、可观测、可复现的过程,去验证某个结构为什么快、为什么慢、在什么数据下失效。报告只是这个过程的文字投影。数据结构和实验报告放在一起,意味着你交的不是说明书,而是一份带着证据链的技术记录:问题定义、实现、实验数据、结论复盘。适合正在修数据结构课、准备课程设计、或者按408考研要求补代码基础的人。一个反直觉的判断是:报告里最不值钱的是代码,最值钱的是你如何制造数据、控制变量、解释结果。代码是从数据到结论中间的一段管道,管道的价值和两端相比反而最容易被高估。

2. 从课程选题到实验方案:不同知识点的实验该怎么设计

2.1 实验题怎么选:把考点映射成可验收的问题

数据结构课程设计最常见的做法,是老师给一组题目,学生挑一个实现。题目通常落在线性表、栈与队列、树与二叉树、图、查找、排序、哈希这几类里。选哪个不只看兴趣爱好,要看它能不能被“实验”验证。链表、树、图这类结构,适合验证操作的正确性、边界条件、遍历顺序;而排序、查找、哈希这类算法,适合验证时间开销随数据规模的变化关系。两类实验的产出完全不同:前者看重“在所有输入下行为正确”,后者看重“在特定分布下开销可预测”。做实验之前先把这个定下来,否则报告里不知道写什么。

知识点适合的实验形式可验证的指标常见翻车点
线性表插入/删除对比、扩容策略操作耗时、移动次数头插尾插混在一起比,变量没分开
栈与队列表达式求值、迷宫、括号匹配正确性、峰值空间边界输入没覆盖,比如空串
二叉树三种递归遍历、非递归改写遍历顺序、栈深度递归写法跑大数据直接溢出
图最短路径、最小生成树路径结果、边数规模邻接矩阵和邻接表混用,复杂度算不清
排序同一数据下多算法对比耗时、交换/比较次数数据集太小,看不出增长趋势
哈希不同冲突处理策略对比查找长度、装载因子影响不控制装载因子,结论失真

选实验题时,我一般建议优先选“能产生对比数据”的题目。比如归并排序和快速排序在近乎有序的数据上表现差异极大,这个差异能被实验数据描述出来,报告就有说服力。纯粹调用现成库、跑通一个接口的题目,很难写出“实验”味道,更适合作为课程设计的前置热身。

2.2 定义输入输出和验收标准:让实验报告可复现

实验设计和软件需求一样,第一步是定义清楚输入、输出、约束。一个可复现的排序实验至少需要写清楚:数据规模范围(从多少到多少)、数据分布类型(随机、有序、逆序、重复)、比较次数和移动次数是否统计、结果校验方式。这些参数直接决定实验数据能不能被他人验证。另一个容易被忽略的是“验收标准”:不能只说“程序正确”,要说“对任意规模 n 的随机输入,输出序列满足非递减,并且与原输入集合完全相同”。把这句话写进报告,实验就有了可判定的通过条件。

我见过不少实验报告在“实验结果”里放一张模糊的截图,没有给出随机种子,也没有说明数据规模。别人想复现,跑出来的数字和他对不上,整份报告的可信度就没了。修正方法不复杂:在报告里明确写出“数据生成时随机种子固定为 20240601,数据规模从 1000 到 100000 共 6 档,每档重复 10 次取平均”,一句话就能把实验从“演示”变成“可复现实验”。

2.3 时间与空间度量:实验报告的数据从哪来

度量时间最常见的方式是 C 语言的clock()或 C++ 的chrono,但只知道“耗时多少毫秒”是不够的。数据结构实验更关注的往往是操作次数:插入排序在逆序数据上的比较次数、哈希表在装载因子 0.7 时的平均查找长度、二叉排序树退化时的递归深度。操作次数和真实的 CPU 耗时不同,它不受机器性能影响,更能反映数据结构本身的形态。实验报告里我建议同时记录两类指标:一个是“结构相关的计数”,一个是“机器相关的时间”。前者回答“这个结构做这件事做了多少步”,后者回答“在你的机器上跑了多久”。

关键点是把计数器和计时器分开,不要把计数值封印在排序函数里。常见做法是定义一个全局计数变量,或者传一个统计结构体指针进去,在每次比较和交换处自增。数据结构实验报告的可信程度,很大程度上取决于这些计数的位置是否正确——少计一次比较,结论就可能从“插入排序不如快速排序”变成“插入排序和快速排序差不多”。

3. 线性表与排序实验的最小可复现实现:C 语言版跑通一份

3.1 顺序表插入与链表插入对比:一段可以贴进实验报告的代码

线性表实验最经典的是对比顺序表和链表在头部插入、尾部插入的差异。顺序表头插需要移动全部元素,链表头插只需要改两个指针。下面是一段可以直接编译运行的 C 代码骨架,用于统计两种结构在头插 100000 个元素时的移动/赋值次数。我用的是严蔚敏《数据结构(C 语言版)》里的建表思路,只抽取了最核心的部分,方便你改成自己的实验。

#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAXN 100000 // 统计结构,记录赋值操作次数 typedef struct { long assign_count; } Stat; // 顺序表头部插入,每次插入都把已有元素后移 void seqlist_head_insert(int arr[], int *len, int val, Stat *st) { for (int i = *len; i > 0; i--) { arr[i] = arr[i - 1]; st->assign_count++; // 每次后移是一次赋值 } arr[0] = val; st->assign_count++; (*len)++; } // 链表结点,用数组模拟,减少 malloc 对实验结果的干扰 typedef struct Node { int val; int next; // 存储下一个结点的下标,-1 表示空 } Node; Node pool[MAXN + 1]; int head = -1; int pool_size = 0; // 链表头部插入,只改头指针和新结点的 next,与长度无关 void linkedlist_head_insert(int val, Stat *st) { pool[pool_size].val = val; pool[pool_size].next = head; head = pool_size; pool_size++; st->assign_count += 2; // 一次赋值 val,一次赋值 next } int main() { // 固定随机种子,保证实验可复现 srand(20240601); Stat seq_stat = {0}, link_stat = {0}; int arr[MAXN]; int len = 0; // 同一组数据分别插入两种结构 // 注意时间测量要包含完整的插入过程,printf 不能夹在中间 clock_t start = clock(); for (int i = 0; i < MAXN; i++) { seqlist_head_insert(arr, &len, rand(), &seq_stat); } clock_t end = clock(); clock_t start2 = clock(); for (int i = 0; i < MAXN; i++) { linkedlist_head_insert(rand(), &link_stat); } clock_t end2 = clock(); printf("顺序表头插 %d 个元素:赋值 %ld 次,耗时 %.3f 秒\n", MAXN, seq_stat.assign_count, (double)(end - start) / CLOCKS_PER_SEC); printf("链表头插 %d 个元素:赋值 %ld 次,耗时 %.3f 秒\n", MAXN, link_stat.assign_count, (double)(end2 - start2) / CLOCKS_PER_SEC); return 0; }

这段代码有两个设计意图:第一,用assign_count而不是clock()作为主指标。顺序表头插的赋值次数约等于n*(n+1)/2,链表头插恒定为2n,这两个数的差距在 n 足够大时是几个数量级,比毫秒数更能说明问题。第二,链表结点用静态数组pool模拟而不是每次malloc,避免内存分配耗时混进实验结果——如果每次插入都 malloc,链表实验时间会被分配器噪声污染,这在实验报告里说不清楚。

参数说明:MAXN控制实验规模,建议分别跑 1000、10000、100000 三档,观察赋值次数的增长趋势;srand(20240601)固定随机种子,让所有算法吃同一组数据,这是排序对比实验的前提。如果你的实验平台是 Visual Studio,clock()精度可能不够,可以把数据规模调大,或者改用QueryPerformanceCounter。

3.2 排序算法对比:同一份数据、同一个计时器

排序是数据结构实验中出稿率最高的知识点,也是 408 考研里代码必背的部分。真正值得写的不是“我会写快排”,而是“在什么数据下快排退化、归并排序却稳定”。下面的代码实现插入排序、快速排序、归并排序三种算法,并用同一份随机数据做对比。为了减少干扰,比较次数和交换次数分开统计,快速排序使用经典 Lomuto 分区,归并排序使用临时数组。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> // 三个全局计数器,分别在算法内部累加 long cmp_count; // 比较次数 long swap_count; // 交换/赋值次数 // 插入排序:适合近乎有序的小规模数据 void insertion_sort(int a[], int n) { for (int i = 1; i < n; i++) { int key = a[i]; int j = i - 1; // 比 key 大的元素右移 while (j >= 0 && a[j] > key) { cmp_count++; a[j + 1] = a[j]; swap_count++; j--; } cmp_count++; // 最后一次比较不满足条件也要计 a[j + 1] = key; swap_count++; } } // 快速排序,Lomuto 分区 void quick_sort(int a[], int low, int high) { if (low >= high) return; int pivot = a[high]; int i = low - 1; for (int j = low; j < high; j++) { cmp_count++; if (a[j] < pivot) { i++; int tmp = a[i]; a[i] = a[j]; a[j] = tmp; swap_count++; } } int tmp = a[i + 1]; a[i + 1] = a[high]; a[high] = tmp; swap_count++; quick_sort(a, low, i); quick_sort(a, i + 2, high); } // 归并排序:稳定且对数据分布不敏感 void merge_sort(int a[], int tmp[], int low, int high) { if (low >= high) return; int mid = (low + high) / 2; merge_sort(a, tmp, low, mid); merge_sort(a, tmp, mid + 1, high); // 合并两个有序区间 int i = low, j = mid + 1, k = low; while (i <= mid && j <= high) { cmp_count++; if (a[i] <= a[j]) { tmp[k++] = a[i++]; } else { tmp[k++] = a[j++]; } swap_count++; } while (i <= mid) { tmp[k++] = a[i++]; swap_count++; } while (j <= high) { tmp[k++] = a[j++]; swap_count++; } memcpy(a + low, tmp + low, (high - low + 1) * sizeof(int)); } // 复制一份数据,保证三个算法吃完全相同的输入 int *copy_array(int src[], int n) { int *dst = malloc(n * sizeof(int)); memcpy(dst, src, n * sizeof(int)); return dst; } int main() { int n = 10000; int *base = malloc(n * sizeof(int)); int *tmp = malloc(n * sizeof(int)); // 固定随机种子,生成同一组随机数据 srand(20240601); for (int i = 0; i < n; i++) base[i] = rand() % 100000; // 插入排序 int *a = copy_array(base, n); cmp_count = 0; swap_count = 0; clock_t t0 = clock(); insertion_sort(a, n); clock_t t1 = clock(); printf("插入排序: 比较 %ld 次, 交换 %ld 次, 耗时 %.3f 秒\n", cmp_count, swap_count, (double)(t1 - t0) / CLOCKS_PER_SEC); free(a); // 快速排序 a = copy_array(base, n); cmp_count = 0; swap_count = 0; t0 = clock(); quick_sort(a, 0, n - 1); t1 = clock(); printf("快速排序: 比较 %ld 次, 交换 %ld 次, 耗时 %.3f 秒\n", cmp_count, swap_count, (double)(t1 - t0) / CLOCKS_PER_SEC); free(a); // 归并排序 a = copy_array(base, n); cmp_count = 0; swap_count = 0; t0 = clock(); merge_sort(a, tmp, 0, n - 1); t1 = clock(); printf("归并排序: 比较 %ld 次, 交换 %ld 次, 耗时 %.3f 秒\n", cmp_count, swap_count, (double)(t1 - t0) / CLOCKS_PER_SEC); free(a); free(base); free(tmp); return 0; }

代码中比较次数和交换次数是分开统计的,这是数据结构和算法实验最容易出错也最容易被扣分的地方。插入排序的while循环退出后,还有一次不满足条件的比较,容易被漏计;快速排序的cmp_count++放在for循环内部,pivot与自身的比较不会被计入,这些细节决定了实验数据是否可信。参数方面,n = 10000只是一个起点,正式实验建议跑 1000、5000、10000、50000、100000 五档;rand() % 100000控制数据值域,值域越小重复元素越多,对排序算法的影响也越大。

如果把数组改成近乎有序(比如先生成 0 到 n 的有序序列,再随机交换少数几对元素),你会发现插入排序的比较次数明显低于随机数据,而快速排序在特定数据下的表现会变差——这个现象如果只跑随机数据是看不到的,所以实验报告中建议把数据分布也当成一个变量写进去。

3.3 结果记录与报告表格设计

实验结果不要只贴控制台截图,建议整理成表格。数据结构实验报告的读者通常是课设老师或者答辩评委,他们看的是数据趋势,而不是某一次运行输出。一个合格的实验结果表至少包含三列:数据规模、比较次数、耗时。如果有多种数据分布,可以在行里用“随机/有序/逆序”区分。

数据规模 n算法比较次数耗时(秒)
1000快速排序约 110000.001
1000归并排序约 99000.001
100000快速排序约 16000000.012
100000归并排序约 15000000.015
100000插入排序约 25000000001.800

表格里写“约”是因为随机数据每次生成的内容不同,虽然固定了种子,但不同平台上的rand()实现可能有差异。在报告正文里,你需要在表格下方补一句“每组数据重复运行 5 次取平均值”,这句话本身就是实验方法的一部分。不要只放一张控制台截图就完事,截图里的数据无法被检索和复制,后续做数据分析时还得重新手敲一遍,纯属给自己添麻烦。

4. 把实验数据变成可信结论:造数据、控变量、画对比

4.1 数据生成:随机、有序、逆序、重复四种分布

数据生成是数据结构实验里最不被重视、却最能拉开报告档次的部分。只跑随机数据,很多算法缺陷根本暴露不出来:快排在随机数据上表现优异,但在几乎有序的数据上可能会退化;插入排序在近乎有序的小规模数据上反而比快排更快。实验报告要可信,至少要覆盖四种数据分布:完全随机、升序排列、降序排列、大量重复值(比如只有 10 个不同取值)。生成升序和降序数据最简单的方法是先用随机数填满数组,再调用快速排序排好,复制一份逆序即可。不过要注意,这时候你的“参考排序”本身也会被计进时间,所以要在计时开始前完成数据生成。

一个容易被忽略的细节是“重复值”数据。很多同学生成重复值数组时,直接a[i] = rand() % 10,这没错,但需要在报告里写明“值域范围是 0~9”。如果不写明,读者会默认数据是均匀分布的大范围随机数,无法理解为什么哈希查找和二分查找在同样规模下表现差异巨大。数据生成代码往往不需要单独展示,但要在报告里用一行字说清楚生成逻辑和参数,这也是可复现实验的一部分。

4.2 控变量与重复实验:避免一次跑通就下结论

数据结构实验报告里最常见的“伪结论”是:跑了一次随机数据,发现快排比归并快 2 倍,就写“快速排序优于归并排序”。实际上单次运行的时间受 CPU 调度、缓存命中、系统后台进程影响很大,波动 10%~20% 是常事。控变量要从三个层面做:数据层,所有算法吃同一份输入数组,而不是各自随机生成;环境层,关掉后台编译和其他占用 CPU 的程序,同一组实验连续运行;统计层,每组参数重复 5~10 次,去掉最高和最低值,取中间几次的平均。

我在做排序对比实验时,习惯把重复次数也写进报告。比如“每组规模重复 5 次,取中间 3 次的平均值作为最终耗时”。这样即使别人在你的机器上复现,数据有浮动,实验方法也站得住脚。反之,只贴一次运行结果,答辩时老师问一句“你再跑一次怎么数字不一样”,就会陷入被动。这不是玄学,是统计噪声的常识。

4.3 结果分析的三个视角:增长率、临界规模、空间代价

实验数据到手之后,不要只读数字,要从三个角度分析:增长率、临界规模、空间代价。

增长率看的是“当 n 翻倍时,耗时和比较次数怎么变”。插入排序在逆序数据上的比较次数接近 n²/2,n 翻倍比较次数约翻 4 倍;归并排序是 n log n,n 翻倍比较次数约翻 2 倍多一点。把这两组数据放在表里,读者的第一反应就是“插入排序的增长曲线更陡”。临界规模关注的是“在多大的 n 之前,简单的算法反而更快”。比如插入排序在 n 小于某个值时,常数小,可能比快排更快;这个临界规模需要在实验里用多档数据点找出来,而不是凭空猜测。空间代价则是报告里最容易漏掉的部分:归并排序需要额外 O(n) 的临时数组,快速排序平均需要 O(log n) 的递归栈空间。代码里用到的tmp数组和递归调用,都要在复杂度分析里如实交代。

这三个视角本质上对应了数据结构考研里的知识点归纳:时间复杂度、空间复杂度、稳定性,以及不同数据分布下算法行为的差异。如果你正在准备 408 数据结构考研,那么这份实验报告几乎就是一份活的复习笔记——把代码和实验数据保存好,后期复习链表、排序、哈希时可以反复对照。

5. 数据结构实验最常见的 5 个坑:从编译报错到结果失真

5.1 读入缓冲与换行符:scanf 的经典翻车

现象:用scanf("%d", &n)读入一个整数,然后紧接着用getchar()读字符,结果字符读出来是换行符,程序行为诡异。

原因:scanf读完整数后会停留在换行符之前,下一个getchar()直接把这个换行符消费掉了。数据结构实验里频繁出现这种“读入参数后,菜单命令输不进去”的报错。

解决:在getchar()之前加一个while (getchar() != '\n');把缓冲行清空;或者统一用gets/fgets按行读入,再用sscanf解析。这类问题在校验括号匹配、表达式求值这类交互式实验里几乎必现,属于数据结构 C 语言版入门的第一道坎。

5.2 递归深度与栈溢出:二叉树遍历的隐藏炸弹

现象:二叉树的递归遍历在树高 10000 的退化情况下直接崩溃或提示栈溢出错误。

原因:递归遍历的深度等于树高。如果树是退化成了链表形态(每个结点只有一个子结点),递归深度就是 n。默认栈空间通常只有 8MB 左右,每次递归调用占用几十字节,几万层就溢出了。用递归实现二叉树遍历虽然代码简洁,但在实验报告里必须说明它的适用边界。

解决:在实验报告中写明“递归版本适用于随机生成的平衡树,退化为链时改用非递归栈实现”。非递归版本用显式栈模拟递归过程,栈空间分配合法,且在分析空间复杂度时能直接指出“栈最大深度等于树高”。这也是 408 数据结构代码题里经常考查的转换。

5.3 编译器优化让计时失真:O2 下循环被优化没了

现象:同一个排序代码,不开优化时耗时 1.2 秒,开了-O2后耗时变成 0.0001 秒,甚至提示“耗时为零”。

原因:编译器发现排序结果没有被使用,把整个排序调用当作死代码消除掉了,或者把循环里的重复计算提升到了循环外。数据结构和算法实验如果只打印耗时而不打印排序结果,很容易被优化掉。

解决:在计时结束后,增加一个“结果校验”步骤,对排序后的数组求和并打印,或者用volatile变量接收数组内容。更规范的做法是在报告中声明“实验在 Debug 模式 / O0 优化级别下运行”,并解释原因:数据结构实验关心的是算法本身的步数,而不是编译器优化后的机器指令效率。

5.4 内存泄漏与未初始化:不报错不等于正确

现象:程序运行正常,结果也正确,但用 Valgrind 检查发现大量“Conditional jump depends on uninitialised value”和内存泄漏报告。

原因:链表实验里频繁 malloc 结点,释放时只释放了当前结点而忘记先保存下一个结点的指针;或者定义局部变量数组没初始化就读取。这些问题运行时不一定崩溃,因为未初始化内存的值往往是残留数据,但在某些平台会得到随机结果,导致报告数据不能复现。

解决:每次malloc/new之后立刻初始化;链表释放时先用临时变量保存next再free当前结点。实验报告里附上一段简单的内存检查输出,能显著提升报告的可信度——至少说明你的代码不是“碰巧跑通”。

5.5 一次实验就下结论:玄学误差与黑匣子心态

现象:实验报告里只给一组数字,比如“哈希查找平均耗时 0.02 毫秒,二分查找 0.03 毫秒”,结论写“哈希查找优于二分查找”。但换一台机器或换一组数据,结论反过来了。

原因:数据规模太小,单次运行时间接近计时器分辨率;或者随机种子不固定,两次实验的数据内容完全不同。数据结构实验的数据不是“跑出来就行”,而是要能被解读。黑匣子心态指的是只关心最终输出,不关心计数器放在哪、统计了什么。一旦结果和理论不符,又不知道如何排查。

解决:固定随机种子,重复实验取中位数,并记录计数器的具体含义。如果实验数据和理论复杂度对不上,优先检查计数器位置是否正确,再检查数据生成逻辑是否符合分布假设。把“实验数据为什么和理论不完全一致”写进报告,本身就是加分项——这说明你理解了实验的边界条件,而不是把一切当玄学。

6. 把实验报告升级成课程设计的起点:一个可复用的实验框架

实验报告交掉之后,代码不要删。把“数据生成 + 计时 + 结果校验”的骨架保留下来,后面做课程设计时能直接复用。一个最小可复用的实验框架包含三个固定函数:gen_data负责生成指定的数据分布,run_sort负责执行算法并返回统计结果,check_result负责验证输出正确性。所有实验流程都固定为一个模式:生成数据、复制给每个算法、各自统计次数、最后校验结果。我的个人习惯是:把随机种子设为学号后四位,把数据规模写进文件名,比如sort_20240601_n10000_random.csv,这样跑完一轮实验,所有数据自动归档,写报告时直接用脚本画对比图。

数据结构实验报告写到后面,最重要的一个教训是:不要为了展示代码而实验,要为回答一个问题而实验。比如“为什么快速排序在有序数据上反而慢”这个问题,只要你设计出有序数据的生成逻辑,并记录比较次数,报告就立住了。以后不管是复习数据结构考研知识点,还是做算法与数据结构相关的课程设计,这套“问题—实现—数据—验证”的流程都值得延续。我的习惯是每份报告开头先用三行字写清楚“要验证的结论是什么”,然后整个实验都围绕这个结论展开。希望这份思路对你手上的数据结构实验报告有实际帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询