简介:面向南京邮电大学数据结构课程学生的完整实验源码包,贯穿线性表、栈与队列、二叉树与哈夫曼树、图与最短路径等核心章节,适合课程同步练习、期末复习或考研复试前快速回顾。资源共64个文件,主要为C/C++头文件和源文件,头文件用于接口声明,源文件用于算法实现;另附可执行程序、调试辅助文件以及doc格式实验报告,压缩包仅1.58MB,轻量便于下载。目前已有2635人学习下载,说明该套代码在校内外有较好的参考口碑。通过源码可重点理解多项式运算、飞机换乘次数、哈夫曼树构造、图的遍历与最短路径等经典问题的完整编码流程,结合实验报告更能把握题设与设计思路;使用时务必遵循学术诚信,以参考和二次改进为主,真正内化数据结构的基本原理。
1. 南邮数据结构试验全部源码:这门课的实战通关底稿
数据结构是南邮计算机类专业的必修硬课,实验从顺序表一路排到图、排序和查找,教材翻得再熟,真到动手写完整源码时,很多人还是挂在“代码跑不通”上——不是算法不懂,是下标越界、指针悬空、递归没出口这些细节反复踩坑。这份源码要解决的问题很直接:把每个实验的完整可运行代码、配套测试数据和报告思路整理成一套能抄、能改、能讲清原理的材料。适合正在赶实验报告的本校生,也适合准备考研数据结构、想看实现细节的跨考生,期末复习拿来过一遍同样顺手。
2. 搭建实验环境与工程骨架:Dev-C++ 配置、头文件组织和批量测试
2.1 为什么用 C 语言 + Dev-C++:先定编译环境,少走一半弯路
南邮数据结构实验目前的常见要求是 C 语言实现,课程教材多参考严蔚敏《数据结构(C语言版)》,实验题目基本从配套习题里改出来。所以环境选型第一优先是能完整编译 C99 的工具。我一般建议用 Dev-C++,这个工具内置 MinGW 编译器,单文件编译快,调试时的断点和变量监视对课程实验这种规模完全够用。如果老师指定老版本 VC++ 6.0,有两个坑:默认的 C 标准偏老,for(int i=0;...)这种写法直接编译报错;调试器在 64 位 Windows 上支持差,断点乱跳是常态。我的血泪经验是,实验到二叉树部分还踩在环境坑上,太亏了,先把编译器搞定再谈算法。
提示:交源码时文件名最好带学号和实验编号,比如
20241234_exp1.c,助教汇总时不用反复确认,这也是工程习惯的一部分。
2.2 工程目录怎么摆:头文件与源文件分离,一个实验一个文件夹
数据结构的实验源码不是单文件就能写明白的,尤其线性表和图这种多模块题目。我习惯按实验建文件夹,每个文件夹里把头文件、实现、入口、测试数据分开,目录结构大概是这样的:
exp2_stack_queue/ ├── main.c # 实验入口,含菜单和多组测试逻辑 ├── sq_stack.c # 顺序栈实现 ├── sq_stack.h # 顺序栈接口 ├── sq_queue.c # 循环队列实现 ├── sq_queue.h # 循环队列接口 └── testdata/ ├── push.txt # 入栈测试数据 └── merge.txt # 队列合并测试数据这样做的理由是:提交作业时老师一般只看.c源文件,但自己调试时,把接口声明放在头文件里,能把“改算法”和“改菜单”两件事分开,编译错误也更易定位。每个实验文件夹放一份README.txt,写清编译命令和测试数据说明,期末复习翻回来时不用重新猜文件用途。这个习惯我在工作后依然沿用,源码工程的第一价值永远是“能快速上手”。
2.3 最小可编译模板:先跑通再填逻辑的 main 骨架
第一次做实验最怕“代码写了一屏,编译一个错,不知道从哪调”。我建议先搭一个能编译、能运行的骨架,再往里填算法。下面这个模板覆盖状态码、结构体定义、初始化和主函数四件套,任何实验都能套用:
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define MAX_SIZE 100 typedef int Status; /* 函数返回值状态:OK 或 ERROR */ typedef int ElemType; /* 元素类型,后面可换成 char、结构体等 */ typedef struct { /* 顺序表 */ ElemType data[MAX_SIZE]; int length; } SqList; /* 初始化顺序表:长度置 0,数据区不必频繁清空 */ Status InitList(SqList *L) { if (!L) return ERROR; L->length = 0; return OK; } int main(void) { SqList L; if (InitList(&L) != OK) { printf("init failed\n"); return 1; } printf("init ok, length=%d\n", L.length); return 0; }逻辑说明:这个骨架的关键是把SqList定义成结构体而不是裸数组,这样L.length能直接记录当前长度,插入删除时不用额外传“实际长度”参数。Status统一成int,所有操作函数返回OK/ERROR,主函数里用返回值判断是否继续,比裸用void函数好查得多。参数说明:MAX_SIZE是顺序表容量,南邮实验数据量一般不超过 100,但后面查找排序实验如果要求跑大量数据,记得把它放大或改用动态数组realloc,否则会被容量卡死。
2.4 用 freopen 批量跑测试数据:实验报告截图不用手敲
实验报告要贴运行结果,但每次手动敲测试数据又慢又容易错。常见做法是在 main 里加一段可选的freopen重定向,测试数据放进文件,跑完直接看输出文件:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { /* 如果带了 -t 参数,就从文件读入、输出到 out.txt */ if (argc > 1 && strcmp(argv[1], "-t") == 0) { freopen("testdata/push.txt", "r", stdin); freopen("out.txt", "w", stdout); } SqStack S; InitStack(&S); /* 原来的测试逻辑原样保留 */ Push(&S, 10); Push(&S, 20); printf("top=%d\n", GetTop(&S)); /* 期望输出 20 */ return 0; }逻辑说明:freopen把stdin和stdout重定向到文件,算法部分一行不用改,就能用同一份代码跑三组输入。参数说明:-t是个命令行开关,不加它时照常从键盘输入、在屏幕输出,方便上课演示;加它时走文件。每组测试数据都保留在testdata目录里,写报告时把out.txt内容贴进去,比你每次手工敲一份省事得多。注意freopen之后不要写太多printf提示语,否则这些提示会全部写进输出文件,报告里的结果图会带一堆无关文字。
3. 线性表、栈与队列:顺序链式存储的边界处理与测试用例
3.1 顺序表插入删除:下标从 0 还是从 1,决定你能不能跑通
顺序表的插入删除是所有实验里第一个完整函数,也是第一个让人翻车的地方。教材约定“第 i 个位置”从 1 开始计数,但 C 数组从 0 开始,两个计数体系一混,代码就乱。我习惯在注释开头就写清i 从 1 开始,然后统一用i-1访问数组:
/* 将 e 插入到顺序表 L 的第 i 个位置,i 从 1 开始 */ Status ListInsert(SqList *L, int i, ElemType e) { int j; if (!L) return ERROR; if (L->length >= MAX_SIZE) return ERROR; /* 表满 */ if (i < 1 || i > L->length + 1) return ERROR; /* 位置非法 */ for (j = L->length; j >= i; j--) { /* 从尾部开始后移 */ L->data[j] = L->data[j - 1]; /* 把 data[j-1] 挪到 data[j] */ } L->data[i - 1] = e; L->length++; return OK; }逻辑说明:插入位置最大是length + 1(插到末尾),最小是 1(插到头部)。循环从最后一个元素开始往后挪,j从L->length递减到i,不会出现数据覆盖。删除操作正好相反,循环从i到length-1往前搬。如果你把i改成从 0 开始,合法性判断变成i < 0 || i > length,循环边界也要同步改成j > i,前后两处必须一致,这是最典型的低级错误。参数说明:ListInsert的参数顺序固定用(L, i, e),和教材保持一致,实验报告里伪码和源码能一一对应,老师查代码不用来回翻函数签名。
3.2 单链表创建:二级指针到底该不该用,头插尾插怎么选
单链表实验的经典翻车点是:在函数里创建链表,返回后主函数里L还是 NULL。原因很简单,传进来的链表头指针是值传递,函数内部L = malloc(...)改的是形参副本。解决方案有两个:要么让函数返回新的头指针,要么用二级指针。第二种更贴近教材写法:
typedef struct Node { ElemType data; struct Node *next; } Node, *LinkList; /* 尾插法根据数组 arr 创建带头结点的单链表 */ void CreateListTail(LinkList *L, ElemType arr[], int n) { LinkList s, rear; int i; *L = (LinkList)malloc(sizeof(Node)); /* 带头结点 */ (*L)->next = NULL; rear = *L; for (i = 0; i < n; i++) { s = (LinkList)malloc(sizeof(Node)); s->data = arr[i]; s->next = NULL; /* 新结点 next 必须初始化 */ rear->next = s; /* 新结点挂到表尾 */ rear = s; /* 表尾指针后移 */ } }逻辑说明:LinkList *L是指向头指针的指针,只有这样才能把malloc出来的头结点地址带出函数。rear始终指向当前最后一个结点,每次分配新结点s后挂到rear->next,再rear = s,保证尾插法的输入顺序和数组顺序一致。如果你用头插法,输入数组的第一个元素会变成链表最后一个结点,遍历顺序是反的——有些题目要求“逆序输出”,直接头插法创建再遍历就是答案。参数说明:n不要从外部猜测,由调用方传入数组元素个数;数组传参会退化成指针,所以必须配n才能确定边界。
3.3 循环队列判空判满:牺牲一个存储单元的经典解法
循环队列代码不长,但“队空和队满怎么区分”是实验报告必问题。常见做法是牺牲一个存储单元,让front == rear表示队空,(rear + 1) % MAX_Q_SIZE == front表示队满:
#define MAX_Q_SIZE 6 typedef struct { ElemType data[MAX_Q_SIZE]; int front, rear; /* front 指向队头元素,rear 指向队尾的下一个位置 */ } SqQueue; /* 入队:队满返回 ERROR,否则写入并移动 rear */ int EnQueue(SqQueue *Q, ElemType e) { if ((Q->rear + 1) % MAX_Q_SIZE == Q->front) { return ERROR; } Q->data[Q->rear] = e; Q->rear = (Q->rear + 1) % MAX_Q_SIZE; return OK; } /* 出队:队空返回 ERROR,出队元素由 *e 带回 */ int DeQueue(SqQueue *Q, ElemType *e) { if (Q->front == Q->rear) { return ERROR; } *e = Q->data[Q->front]; Q->front = (Q->front + 1) % MAX_Q_SIZE; return OK; }逻辑说明:为什么front == rear不能判满?因为队列环起来后,入队出队交替,满的时候front和rear也可能相等,必须预留一个空位打破歧义。这里MAX_Q_SIZE取 6,实际最多存 5 个元素,这个“容量减一”要在报告里写清楚。参数说明:rear指向的不是最后一个元素,而是下一个要写入的位置;出队时*e带出的是data[front],两个指针更新都必须取模,忘了取模数组越界是迟早的事。
3.4 顺序查找与折半查找:low <= high 还是 low < high,边界决定生死
查找部分一般会让写顺序查找和折半查找,折半查找的边界写错会导致死循环或漏查最后一个元素。我常用low <= high:
/* 在有序数组 a 的前 n 个元素中查找 key,返回下标,找不到返回 -1 */ int BinarySearch(ElemType a[], int n, ElemType key) { int low = 0, high = n - 1, mid; while (low <= high) { mid = low + (high - low) / 2; /* 防溢出的中点写法 */ if (a[mid] == key) { return mid; } else if (a[mid] < key) { low = mid + 1; /* 目标在右半区 */ } else { high = mid - 1; /* 目标在左半区 */ } } return -1; }逻辑说明:low <= high在low == high时还会再判断一次中间元素;如果改成low < high,区间缩到只剩一个元素时会直接退出循环,可能返回 -1 但元素就在那里。mid = low + (high - low) / 2和(low + high) / 2结果一样,但在大数组上防溢出,408 真题里出现过这个细节。参数说明:数组必须有序,如果实验给的是无序数据,得先排序再折半,这个前提很多人报告里忘记写。
4. 树、图与排序:递归遍历、邻接表和三种排序的实现要点
4.1 二叉树递归遍历:终止条件不写,栈溢出就来找你
二叉树实验最早的坑是“空树递归没出口”。结构体定义和递归遍历框架如下:
typedef struct BiTNode { ElemType data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; /* 先序遍历:根、左、右 */ void PreOrder(BiTree T, void (*visit)(ElemType *e)) { if (T == NULL) return; /* 递归终止条件 */ visit(&T->data); PreOrder(T->lchild, visit); PreOrder(T->rchild, visit); } /* 中序遍历:左、根、右 */ void InOrder(BiTree T, void (*visit)(ElemType *e)) { if (T == NULL) return; InOrder(T->lchild, visit); visit(&T->data); InOrder(T->rchild, visit); }逻辑说明:所有递归遍历共同点是“进入子节点前先判断是否为空”,空子树直接 return。把visit做成函数指针,是为了实验报告里分别演示“输出结点值”和“统计叶子数”两种操作,不用复制遍历代码。要求叶子数时,只要在visit回调里判断e->lchild == NULL && e->rchild == NULL时计数即可。参数说明:visit接收ElemType *而不是值,是为了回调里能修改结点或做指针比较,这是教材源码的常见封装方式。另一个高频变形是求树高,用后序遍历return max(height(left), height(right)) + 1,它依赖递归返回值,和这里的回调风格不同,建议两个版本都写一遍。
4.2 哈夫曼树构建:Select 函数里最容易被忽略的 parent 判断
哈夫曼树实验的代码核心是Select:从当前森林里选两个权值最小的根结点。这个函数写错多半是初始值或 parent 判断没写:
#include <limits.h> #define MAX_NODES 100 typedef struct { int weight; int parent, lchild, rchild; } HTNode, HuffmanTree[MAX_NODES]; /* 在 HT[1..n] 中选两个双亲为 0 的最小权值结点,下标带回 s1, s2 */ void Select(HuffmanTree HT, int n, int *s1, int *s2) { int i; long min1 = LONG_MAX, min2 = LONG_MAX; *s1 = 0; *s2 = 0; for (i = 1; i <= n; i++) { if (HT[i].parent != 0) continue; /* 已在树内,跳过 */ if (HT[i].weight < min1) { /* 新的最小值 */ min2 = min1; *s2 = *s1; min1 = HT[i].weight; *s1 = i; } else if (HT[i].weight < min2) { /* 新的次小值 */ min2 = HT[i].weight; *s2 = i; } } }逻辑说明:parent != 0表示该结点已被合并到别的树里,不能再选,这是哈夫曼构建里最容易漏的一行。min1、min2用LONG_MAX初始化,避免结点权值恰好等于固定初始值导致二次比较失效。当第一个可用结点出现时,它同时成为最小值和次小值——注意代码里min2 = min1; *s2 = *s1;的处理顺序,先保存旧值再更新新值。参数说明:HT数组下标从 1 开始是教材惯例,下标 0 做占位不存数据;Select的n是当前森林规模,主循环从i = n+1到2n-1逐个创建新结点,每次调Select传入已使用结点数。
4.3 图的邻接表创建:头插法和尾插法的遍历顺序差异
图实验经典题目是给顶点和边,建邻接表再 DFS/BFS 输出遍历序列。邻接表创建有个隐藏差异:头插法和尾插法得到的遍历结果不同。
#define MAXV 100 typedef struct ArcNode { int adjvex; struct ArcNode *nextarc; } ArcNode; typedef struct { int data; ArcNode *firstarc; } VNode; typedef struct { VNode adjlist[MAXV]; int n, e; /* 顶点数、边数 */ } ALGraph; /* 创建无向图邻接表,边输入格式:u v */ void CreateALGraph(ALGraph *G) { int i, u, v; ArcNode *p; scanf("%d%d", &G->n, &G->e); for (i = 0; i < G->n; i++) { G->adjlist[i].firstarc = NULL; G->adjlist[i].data = i; } for (i = 0; i < G->e; i++) { scanf("%d%d", &u, &v); /* 无向图:u 的邻接表加 v,v 的邻接表加 u */ p = (ArcNode *)malloc(sizeof(ArcNode)); p->adjvex = v; p->nextarc = G->adjlist[u].firstarc; /* 头插法 */ G->adjlist[u].firstarc = p; p = (ArcNode *)malloc(sizeof(ArcNode)); p->adjvex = u; p->nextarc = G->adjlist[v].firstarc; G->adjlist[v].firstarc = p; } }逻辑说明:头插法把最新输入的边插到链表头部,插入 O(1),但邻接顺序和输入顺序相反。如果想保持输入顺序输出邻接点,要改尾插法并维护 tail 指针。实验如果只要求遍历序列,头插法更快,但报告里必须注明“结果与输入顺序相反”,否则老师核对样例时可能对不上。参数说明:这里默认顶点编号 0 到 n-1;如果输入从 1 开始,遍历时下标要减 1,很多人在这步直接数组越界。DFS 递归版本很短:
int visited[MAXV] = {0}; void DFS(ALGraph *G, int v) { ArcNode *p; visited[v] = 1; printf("%d ", v); for (p = G->adjlist[v].firstarc; p != NULL; p = p->nextarc) { if (!visited[p->adjvex]) { DFS(G, p->adjvex); } } }逻辑说明:visited是全局数组,跑多组测试前要memset(visited, 0, sizeof(visited))。DFS 递归深度和路径长度相关,实验数据小没事,但大图慎用递归,这就是 4.1 说的栈溢出风险同款问题。
4.4 BFS 的 visited 标记:为什么必须先标记再入队
BFS 的常见错误写法是“出队时才标记 visited”,这会导致同一结点被重复入队多次:
void BFS(ALGraph *G, int v) { int que[MAXV], front = 0, rear = 0; int visited[MAXV] = {0}; int u; ArcNode *p; visited[v] = 1; que[rear++] = v; while (front < rear) { u = que[front++]; printf("%d ", u); for (p = G->adjlist[u].firstarc; p; p = p->nextarc) { if (!visited[p->adjvex]) { visited[p->adjvex] = 1; /* 入队前立刻标记 */ que[rear++] = p->adjvex; } } } }逻辑说明:BFS 的队列长度最坏等于顶点数,用定长数组安全。入队前标记 visited,这样两个邻居指向同一个未访问结点时,第二次遇到会因 visited 为 1 跳过;如果出队时才标记,环状图里同一结点会被重复入队,队列可能溢出。参数说明:front和rear用普通数组模拟队列,不会出现出队后又入队超过 n 个的情况,这种简化在课程实验里完全够用。如果想算最短路径步数,维护一个level[]数组,level[neighbor] = level[u] + 1,这是图实验加分延展,也是 408 图和数组结合出题的点。
4.5 快排、堆排、归并排序:核心函数的三组对比
排序实验三个算法代码量大,建议至少把快排的Partition和堆排的SiftDown背熟:
/* 快速排序的一趟划分,返回枢轴最终位置 */ int Partition(ElemType a[], int low, int high) { ElemType pivot = a[low]; /* 取第一个元素为枢轴 */ while (low < high) { while (low < high && a[high] >= pivot) high--; a[low] = a[high]; while (low < high && a[low] <= pivot) low++; a[high] = a[low]; } a[low] = pivot; return low; } /* 堆排序的下沉调整,k 是待调整结点下标(从 0 开始),n 是堆规模 */ void SiftDown(ElemType a[], int k, int n) { int i = k, j = 2 * i + 1; /* 左孩子 */ ElemType tmp = a[i]; while (j < n) { if (j + 1 < n && a[j] < a[j + 1]) j++; /* 挑较大的孩子 */ if (tmp >= a[j]) break; a[i] = a[j]; i = j; j = 2 * i + 1; } a[i] = tmp; }逻辑说明:快排Partition是“挖坑填数”法,先从 high 往 low 找比枢轴小的,再从 low 往 high 找比枢轴大的,最后把枢轴放到low == high的位置。两个内层 while 必须带low < high,否则枢轴不是最小值时数组下标越界。堆排的SiftDown里tmp >= a[j]的等号处理很关键,相等时 break 可以避免无意义交换,但堆排整体仍是不稳定排序,实验报告要写清楚。参数说明:数组下标从 0 开始,第 k 个结点的左右孩子分别是2k+1和2k+2;建堆时从n/2 - 1往前调,排序时每次把堆顶换到末尾再对前n-1个元素下沉。三种排序的适用场景:快排平均最快但近乎有序数据退化 O(n²),归并稳定适合链表,堆排序原地但常数大,报告里附一张数据规模时间对比表最好。
5. 南邮数据结构实验的高频翻车现场:现象、原因与排查方法
实验作业最常见的不是算法想不出来,而是“明明照着教材抄的,为什么跑不出来”。下面这几条是源码调试里最常踩的坑,综合了多届实验报告里反复出现的问题,每一条按现象、原因、解决三步写,排查时可以按图索骥。
5.1 顺序表插入后输出乱码:先检查移动方向和下标范围
现象:调用ListInsert后打印顺序表,中间某段数据变成乱码数字,或者插入后原来的最后一个元素丢失。
原因:最常见的是移动方向写反。插入时应该从最后一个元素开始往后挪,有人写成从插入位置开始往前挪,导致后面的数据被未初始化区域覆盖;第二个常见原因是i的合法范围判断错误,允许i == length + 2,数组越界写入非法位置。
解决:先把ListInsert的合法性判断改成i < 1 || i > L->length + 1,然后在循环前打印L->length,确认插入前长度。如果还是乱码,检查循环起点——length是元素个数,最后一个元素下标是length - 1,移动时从j = L->length开始,是把data[length-1]搬到data[length],这个“长度和下标差 1”的错位是最隐蔽的坑。
5.2 链表一运行就段错误:八成是 next 没初始化
现象:创建完链表后调用遍历函数,程序直接段错误,或者输出一串地址后卡死。
原因:链表的next没有初始化。最常见的是头插法或尾插法里先malloc新结点,忘了写s->next = NULL,导致新结点的 next 是野指针,遍历时读到随机地址就崩。另一种是尾插法里rear->next = s之后没有rear = s,尾指针还指着老结点,链表形成环。
解决:每次malloc新结点后马上写两行:s->data = ...; s->next = NULL;。怀疑有环时,遍历函数加一个计数器,超过 1000 次直接退出,再回头查是哪个结点的 next 没接对。反过来,如果是头插法,新结点 next 必须先指向当前头结点的 next,再把头结点的 next 指向新结点,顺序反了也会丢链表。
5.3 二叉树递归遍历栈溢出:退化链树让递归深度爆表
现象:二叉树实验用先序序列创建树,输入一个退化成链的树(比如只有右孩子),递归遍历时程序直接崩溃或报 stack overflow。
原因:递归深度等于树高,退化成单链的树树高等于结点数,几千个结点就能把默认栈空间用光。这不是算法错误,而是输入数据把递归结构推到了极限。
解决:实验报告里说明“退化为链时递归深度为 O(n)”,这是考点。如果老师要求必须处理大输入,把PreOrder改成非递归版本,用显式栈模拟递归,每一层的结点入栈而不是函数调用入栈。非递归先序遍历大概 20 行,面试手撕也是加分项;SiftDown这类非递归写法同理,凡是不依赖调用栈的版本都值得备一份。
5.4 散列表删除元素后查找失败:探测链被空位切断了
现象:散列表实验的插入和查找都正常,但删除一个中间元素后,另外几个本来能查到的 key 变成了“找不到”。
原因:线性探测在冲突时顺延到下一个空位,删除某个元素后直接把位置置空,就把后续冲突元素的探测链切断,后面的 key 探测到空位会认为“元素不存在”而提前终止。
解决:标准做法是给每个槽位加标记,比如0空位、1有效、-1已删除。查找时遇到-1继续往后探测,插入时遇到0或-1都可以写入。实验报告里要把“删除标记”写进设计说明,否则老师追问“为什么删除后还能查到”时会卡壳。这个点同样高频出现在期末复习里,散列表冲突处理基本必考一题。
5.5 Windows 下中文乱码:源文件编码和控制台代码页不统一
现象:Dev-C++ 运行源码工程,printf 的中文提示变成乱码,或者程序结束前最后几行输出没显示。
原因:Windows 控制台默认代码页是 GBK,而 Dev-C++ 默认源文件编码是 UTF-8,编译器把 UTF-8 字符串原样写进可执行文件,控制台按 GBK 解析自然乱码。
解决:最简单的方式是源文件另存为 GBK 编码,或者编译时加-fexec-charset=GBK。如果用的是 VS Code + MinGW 组合,统一 C/C++ 编译器参数和终端编码即可。还有个土办法:所有中文提示改成英文,报告截图后单独说明中文含义,很多开源源码工程都这么干。注意freopen重定向输出后不要盲目混排中英文,否则out.txt里报告截图效果会很差。
6. 把实验源码变成复试和面试的底气:重构、注释与追问应对
源码跑通只是第一步,这门课的实验价值在期末和复试阶段才会真正体现。我自己的习惯是:实验结束后用一周时间做三轮重构——第一轮把每个函数从void改成有返回值的Status,统一错误处理;第二轮把调试用的printf删掉,换成测试文件里跑断言,编译参数加-Wall清掉所有 warning;第三轮把所有main里的功能拆成Menu()和RunTest()两个函数,这样想单独验证某段算法不会被菜单逻辑干扰。
复试面试时,老师不会让你背代码,而是直接提几个高频追问:链表反转怎么写、快排为什么退化、哈希冲突怎么解决、BFS 为什么能求无权图最短路径。这些都能在这份源码里找到原型:链表反转就是头插法的倒序版;快排退化对应 4.5 节里那两句内层 while 的等号处理;哈希冲突对应避坑清单里的删除标记;BFS 最短路径对应 4.4 节的level[]延展。你把实验代码重新封装成“能讲清楚每一步为什么”的版本,比背十道刷题套路有用得多,尤其面对 408 风格追问时,图和数组的底层实现是绕不开的。
最后一件小事:给源码写一个不超过十行的 README,写清编译环境、测试数据格式、每个文件对应实验的哪个小题。这个习惯我保持到现在,回头翻任何一个工程都能十分钟内上手。希望帮到你,也祝你这次实验一次通过。
本文还有配套的精品资源,点击获取