简介:面向初学数据结构的C语言学习者,这份课程设计资源以顺序表、单链表、双向链表三大典型线性结构为主线,给出完整的实验设计方案与配套实验报告,适合高校学生完成课程设计、期末复习或自学练手时参考。资源打包为RAR压缩包,共5个文件,包含4个C语言源程序文件和1份docx格式的实验报告文档,整体仅108KB,轻量但内容集中,覆盖顺序表与链表的建立、查找、插入、删除、修改、排序及文件读取等核心操作。已有611人学习下载,可用于快速验证线性表应用思路。具体实现包括:顺序表完成简易学生信息管理系统,单链表解决约瑟夫环与猴子选王问题,双向链表实现长整数相加;代码基于C-Free 5.0编写,逻辑清晰,实验报告对设计过程与算法要点有说明,便于读者对照理解;三种结构从基础操作延伸到综合应用,也可帮助对比顺序存储与链式存储的差异,实操性强,是一份便于动手验证的课设参考资料。
1. 课程设计的最后一道坎:代码能跑不代表能过答辩
C语言数据结构课程设计加实验报告,是学期末最容易翻车的一关。很多人的状态是:代码在本地跑得好好的,答辩时老师换一组测试数据就崩了;报告写了二十多页,被追问一句“这个操作的时间复杂度是多少”就愣住。这门课真正考的不是背定义,而是三件事:把题目拆成合适的数据结构,写出能维护、能复现的 C 代码,再用一份经得起检查的实验报告把设计讲清楚。
下面按“选题拆解 → 代码骨架 → 报告写作 → 踩坑排查”的顺序走一遍。新手可以照步骤做出一套完整交付物,已经开题的人也能拿着这份清单对照检查,看看自己还差在哪个环节。文里的代码都是最短可运行骨架,直接抄进工程、改改业务字段就能用;报告部分给的是老师真正会检查的点,不是排版美观那一套。
2. 选题与结构拆解:把课程设计题目翻译成数据结构
动手写代码之前,最值钱的一步是拆题。课程设计题目看起来五花八门,但拆到底层,核心就是“题目要求的一堆操作,分别对应哪种数据结构、哪个算法”。这一步做好了,后面的编码就是填空;这一步偷懒,代码写到一半必然返工。
2.1 四类常见选题:难度怎么排、怎么选
课程设计题目翻来覆去就那几类,我把它们的核心结构和难度排了一下:
| 题目方向 | 典型题目 | 核心结构 | 难度 | 适合谁 |
|---|---|---|---|---|
| 线性表应用 | 学生成绩管理、图书管理、通讯录 | 单链表、顺序表 | ★ | 第一次做课程设计 |
| 栈和队列 | 表达式求值、迷宫求解、银行业务模拟 | 栈、循环队列 | ★★ | 想练递归和状态转换 |
| 树结构 | 哈夫曼编码、表达式树、二叉排序树 | 二叉树 | ★★★ | 想重点展示递归能力 |
| 图结构 | 校园导航、课程拓扑排序、最小生成树 | 邻接矩阵、邻接表 | ★★★★ | 想冲高分的那批人 |
我的建议是:第一次做就选线性表方向,别一上来就挑战红黑树之类。老师评分看的是数据结构和题目的匹配度、代码是否健壮、报告是否完整,不是题目的华丽程度。一个学生成绩管理系统,链表加折半查找,测试用例齐全,通常比一个跑不通的图形界面项目得分高。教材里的代码多是伪代码风格,没法直接编译,把它翻译成可运行的 C 本身就是课程设计要训练的能力,这一点在报告的需求分析里值得写清楚。
2.2 把题目操作翻译成结构:学生成绩管理系统的拆解
以最经典的学生成绩管理系统为例。动手前先把题目要求逐条列出来,翻译成数据结构操作:
| 题目要求 | 数据结构操作 |
|---|---|
| 添加学生 | 链表尾插(或按学号有序插入) |
| 按学号删除 | 查找 + 删除节点 |
| 修改成绩 | 查找 + 修改数据域 |
| 按成绩排序 | 数组快排,或链表排序 |
| 按分数段统计 | 遍历 + 条件计数 |
| 存盘 / 读盘 | 文件顺序读写 |
为什么选链表而不是数组:插入删除频繁的场景,数组要在中间搬移大量元素,链表改几个指针就行。但链表按学号查找是顺序查找 O(n),如果题目里“按学号精确查询”出现频率更高,反过来用结构体数组加折半查找更划算。C 语言里数组名就是首地址指针,排序时传数组实际传的是指针,这个关系在报告概要设计里值得写一笔。
主程序推荐用“菜单 + 死循环 + switch”的结构,这是课程设计最常见的工程写法:
int main(void) { Stu *head = NULL; loadFromFile(&head); /* 启动时把磁盘数据读回链表 */ int cmd; while (1) { printMenu(); scanf("%d", &cmd); while (getchar() != '\n'); /* 清掉输入缓冲区里的回车 */ switch (cmd) { case 1: head = appendStudent(head); break; case 2: head = deleteStudent(head); break; case 3: modifyStudent(head); break; case 4: sortStudents(head); break; case 5: printList(head); break; case 0: saveToFile(head); break; default: printf("无效命令,请重新输入\n"); } if (cmd == 0) break; } freeList(head); /* 退出前释放整条链表 */ return 0; }这里有两个关键点。第一,head 必须用返回值接收:删除头节点会让链头变化,如果只传Stu *head进去,函数里改的是副本,外面的链头还是旧值,后续操作全乱。第二,while (getchar() != '\n')用来清空 scanf 后残留的换行符,否则下次 scanf 读到换行会直接跳过,菜单程序最常见的玄学 bug 就是从这里来的。
2.3 交付物与时间分配:别把两周压成两天
课程设计要交的东西通常包括三样:可编译的源代码工程、实验报告(含截图和源码附录)、部分老师还会要求 PPT 或演示视频。时间分配上,我一般按 12 天排:
| 阶段 | 时间 | 产出 |
|---|---|---|
| 需求拆解与结构设计 | 第 1-2 天 | 操作表、结构体定义、函数清单 |
| 核心模块编码 | 第 3-6 天 | 可编译运行的主程序 |
| 自测与边界用例 | 第 7-8 天 | 测试记录表 |
| 写实验报告 | 第 9-11 天 | 报告成稿 |
| 预答辩自查 | 第 12 天 | 修复遗留问题 |
这里有个原则值得提前说:先冻结代码,再写报告。很多同学是先写报告后改代码,结果答辩演示的功能和报告截图对不上,这是最冤的失分点。另外别把课程设计硬拖进数据结构期末复习周,那时候一边背算法一边调 bug,两头都顾不上。
3. 核心代码骨架:链表、二叉树、图的最短可运行模板
下面四段代码是课程设计里出现频率最高的骨架。它们不是完整工程,而是“最短可运行模板”:抄下来能编译,改改字段就能对接你的题目。编译时统一加-Wall -g,警告全开能提前暴露一堆问题。
3.1 单链表三件套:插入、删除、遍历
单链表是线性表题目的地基,学生成绩管理、图书管理、通讯录全都绕不开。这里给出插入和删除两个最容易被写错的函数:
#include <stdio.h> #include <stdlib.h> typedef struct Stu { int id; char name[20]; float score; struct Stu *next; } Stu; /* 尾插法:把新节点挂到链表末尾 */ Stu *appendStudent(Stu *head) { Stu *p = (Stu *)malloc(sizeof(Stu)); if (p == NULL) { /* malloc 失败必须处理 */ printf("内存分配失败\n"); return head; } printf("输入学号 姓名 成绩: "); scanf("%d %s %f", &p->id, p->name, &p->score); p->next = NULL; if (head == NULL) { /* 空链表:新节点直接做头 */ return p; } Stu *tail = head; while (tail->next != NULL) { /* 找到最后一个节点 */ tail = tail->next; } tail->next = p; /* 把新节点接在末尾 */ return head; } /* 按学号删除:找到后修改前驱的 next,再 free */ Stu *deleteById(Stu *head, int id) { if (head == NULL) { /* 空表直接返回 */ printf("链表为空\n"); return head; } Stu *cur = head, *prev = NULL; while (cur != NULL && cur->id != id) { prev = cur; cur = cur->next; } if (cur == NULL) { /* 没找到 */ printf("未找到学号 %d\n", id); return head; } if (prev == NULL) { /* 删的是头节点 */ head = cur->next; } else { prev->next = cur->next; /* 中间或尾部节点 */ } free(cur); /* 释放节点内存 */ return head; }尾插的逻辑是:从 head 出发遍历到最后一个节点,把新节点挂上去。这里每次插入都是 O(n),如果想优化,可以维护一个全局尾指针,插入变成 O(1),报告里把这个优化写上也是加分项。删除的难点全在头节点:删除头节点时链头变了,必须更新 head 再返回;删除中间和尾部的逻辑其实一样,都是让前驱的 next 跳过当前节点。free(cur)之后 cur 就成了悬空指针,所以删除函数里最好马上 return,不要再碰 cur,这一点在 5.1 还会展开。
3.2 二叉树:用字符串建树与三种递归遍历
二叉树题目(表达式树、哈夫曼树、二叉排序树)的核心是递归,而递归代码最大的坑是输入方式。我用字符串指针建树,彻底绕开 scanf 和 getchar 混用导致的缓冲区残留问题:
typedef struct BTNode { char data; struct BTNode *lchild; struct BTNode *rchild; } BTNode; /* 按前序序列建树,# 表示空节点,例如 "AB#D##C##" */ BTNode *buildFromStr(char *s, int *idx) { char ch = s[(*idx)++]; /* 取一个字符并后移下标 */ if (ch == '\0' || ch == '#') { return NULL; /* 空节点:递归终止条件 */ } BTNode *root = (BTNode *)malloc(sizeof(BTNode)); root->data = ch; root->lchild = buildFromStr(s, idx); /* 递归建左子树 */ root->rchild = buildFromStr(s, idx); /* 递归建右子树 */ return root; } /* 中序遍历:左 -> 根 -> 右 */ void inOrder(BTNode *root) { if (root == NULL) return; /* 终止条件先写 */ inOrder(root->lchild); printf("%c ", root->data); inOrder(root->rchild); } /* 求树的高度:左右子树取大者再加 1 */ int treeHeight(BTNode *root) { if (root == NULL) return 0; int leftH = treeHeight(root->lchild); int rightH = treeHeight(root->rchild); return (leftH > rightH ? leftH : rightH) + 1; }调用方式是scanf("%s", seq); int idx = 0; BTNode *root = buildFromStr(seq, &idx);,输入序列一行打完,不带空格。idx用指针传递,是因为每次递归取字符后都要让下标前进一位,传值做不到。递归三要素在这个例子里全齐了:终止条件(遇到#或\0返回 NULL)、递推公式(左子树高度和右子树高度取大加 1)、返回值的使用(建树函数把创建的节点返回给上一层)。前序、中序、后序遍历的区别只是 printf 的位置不同,报告里可以把三种遍历放进一个对比小节,相比贴三份几乎一样的代码,老师会更认可你的概括能力。
3.3 图的最短路径:邻接矩阵与 Dijkstra 模板
图方向题目(校园导航、城市间最短路)通常绕不开 Dijkstra。用邻接矩阵实现最直观,也最好在报告里画图说明:
#define INF 0x3f3f3f3f #define MAXN 100 int graph[MAXN][MAXN]; /* 邻接矩阵,graph[i][j]=权值 或 INF */ int dist[MAXN]; /* dist[v] 表示起点到 v 的最短距离 */ int visited[MAXN]; /* 已确定最短路的点集 */ int path[MAXN]; /* path[v] 记录 v 的前驱,用于输出路线 */ void dijkstra(int n, int start) { for (int i = 0; i < n; i++) { dist[i] = graph[start][i]; visited[i] = 0; path[i] = (dist[i] < INF) ? start : -1; } dist[start] = 0; visited[start] = 1; /* 起点已确定 */ for (int k = 1; k < n; k++) { /* 第一步:选出未访问且 dist 最小的点 */ int minDist = INF, u = -1; for (int i = 0; i < n; i++) { if (!visited[i] && dist[i] < minDist) { minDist = dist[i]; u = i; } } if (u == -1) break; /* 剩下的点不可达 */ visited[u] = 1; /* 第二步:用 u 更新相邻点的 dist */ for (int v = 0; v < n; v++) { if (!visited[v] && graph[u][v] < INF && dist[u] + graph[u][v] < dist[v]) { dist[v] = dist[u] + graph[u][v]; path[v] = u; } } } }初始化那里dist[start] = 0要放在visited[start] = 1之前,而且必须在循环里dist[i] = graph[start][i]之后再执行,防止起点到自身有自环时把 dist 覆盖错。INF用0x3f3f3f3f而不是 9999,是因为两个 INF 相加不会溢出 int,dist[u] + graph[u][v]这一步在数据不合法时不会产生负数。path数组是报告里的加分项,输出路径时从终点顺着前驱倒推回起点,比只输出距离更能说明你理解了算法过程。
3.4 排序与查找:快排、折半查找与复杂度素材
排序和查找是实验报告里复杂度分析的常客,也是期末复习时的高频考点。快排手写版和折半查找必须能默写:
void quickSort(int arr[], int left, int right) { if (left >= right) return; /* 终止条件 */ int i = left, j = right; int pivot = arr[left]; /* 取最左为基准 */ while (i < j) { while (i < j && arr[j] >= pivot) j--; /* 右指针找小 */ if (i < j) arr[i++] = arr[j]; while (i < j && arr[i] <= pivot) i++; /* 左指针找大 */ if (i < j) arr[j--] = arr[i]; } arr[i] = pivot; /* 基准归位 */ quickSort(arr, left, i - 1); /* 递归排序左半 */ quickSort(arr, i + 1, right); /* 递归排序右半 */ } int binarySearch(int arr[], int n, int key) { int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; /* 防止 low+high 溢出 */ if (arr[mid] == key) return mid; else if (arr[mid] < key) low = mid + 1; else high = mid - 1; } return -1; /* 找不到返回 -1 */ }折半查找有两个前提必须写进报告:顺序存储、数据有序。链表做不到折半查找,因为取不到中间节点,这个点如果老师追问,很多人会答错。mid = low + (high - low) / 2而不是(low + high) / 2,是因为两个大数相加可能超出 int 范围,这是个经典边界坑。如果老师要求至少实现两种排序对比,冒泡排序是兜底,但报告里要自己算一遍交换次数,别直接抄网上的结论。题目不要求手写排序时,也可以直接用 stdlib.h 里的qsort,不过要在报告里写清楚比较函数的实现。
排序与查找的复杂度对照表,实验报告里可以直接用:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 额外空间 |
|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) |
| 快速排序 | O(n log n) | O(n²) | O(log n) |
| 折半查找 | O(log n) | O(log n) | O(1) |
4. 实验报告怎么写:七段式结构与答辩高频追问
代码写完只算完成一半,课程设计最后得多少分,实验报告和答辩的表现经常起决定性作用。报告不是越厚越好,而是要让老师能在十分钟内找到他想要的东西:你做了什么、怎么做的、测试过没有、遇到问题怎么解决的。
4.1 七段式结构:从需求分析到附录怎么安排
课程设计实验报告一般按一个固定骨架展开,每段篇幅建议如下:
| 段落 | 内容 | 篇幅建议 |
|---|---|---|
| 需求分析 | 题目要求、输入输出边界、功能列表 | 1-2 页 |
| 概要设计 | 结构体定义、模块划分、函数清单 | 2-3 页 |
| 详细设计 | 核心函数流程、关键代码段、数据结构图 | 4-6 页 |
| 调试分析 | 遇到的问题和解决过程 | 1-2 页 |
| 用户手册 | 编译方法、菜单操作说明 | 1 页 |
| 测试结果 | 测试用例表加运行截图 | 2-3 页 |
| 附录 | 完整源码 | 其余 |
需求分析常被当成题目抄写,其实它最该写的是输入输出边界:成绩是整数还是浮点、学号要不要校验重复、文件不存在时怎么处理。概要设计里的函数清单是给老师看模块划分能力的,每个函数一行注释说明参数和返回值。详细设计不用把全部代码贴进去,挑插入删除、排序查找这类核心函数贴关键代码段就够了,完整代码放附录。调试分析是很多人留白的一段,反而最容易得分——写一个真实的 bug 现象、原因、解决办法,比写十页理论都管用。我写报告的习惯是:先写需求分析和测试用例,再回头补详细设计,这样能提前发现功能遗漏。
4.2 测试用例表与复杂度分析:报告里的硬通货
测试结果不能只放几张运行截图,老师要看的是你有没有边界意识。一份合格的测试用例表长这样:
| 编号 | 输入 | 预期输出 | 实际结果 | 覆盖点 |
|---|---|---|---|---|
| T01 | 空链表执行删除 | 提示链表为空 | 通过 | 空表边界 |
| T02 | 删除头节点 | 新头节点生效 | 通过 | 头节点指针更新 |
| T03 | 删除不存在的学号 | 提示未找到 | 通过 | 查找失败分支 |
| T04 | 1000 条记录排序 | 耗时小于 1 秒 | 通过 | 性能验证 |
| T05 | 输入成绩 -5 | 拒绝并提示重新输入 | 失败,已修复 | 输入校验 |
第五行是关键:它展示了一条真实缺陷从发现到修复的过程,配合调试分析段里对 bug 的描述,整份报告的完整度立刻不一样。复杂度分析要注意:不要只写“时间复杂度 O(n)”,要写清楚是哪个操作、平均情况还是最坏情况。链表查找是 O(n) 但头插是 O(1),数组按下标访问是 O(1) 但查找是 O(n),这两组最容易写反。快排最坏情况 O(n²) 要说明发生在序列基本有序且每次取第一个做基准时,能写出这句话,老师就知道你是真懂。
4.3 答辩高频追问:这 6 个问题提前准备
答辩环节老师一般不会整篇念报告,而是挑几个点追问。下面这六个是我见过最常被问的,每个都按“先说结论再说理由”的套路准备:
- 为什么用链表不用数组?答:插入删除只改指针,数组要搬移元素;如果题目查询多于增删,就用数组加折半查找。
- 这个函数时间复杂度多少?最坏呢?答:先反问老师问的是哪个操作,再说平均和最坏两套数字。
- 数据量变成十万条会怎样?答:链表遍历变慢,排序会明显卡顿,需要考虑更快的数据结构或外部排序。
- free 之后指针还能用吗?答:不能,那是悬空指针,再次使用是未定义行为,所以 free 后要置 NULL。
- 折半查找的前提?答:顺序存储加有序,缺一不可,链表做不了折半查找。
- 文件读入的数据格式非法怎么办?答:读取时检查返回值,非法行跳过并计数报告,程序不能崩。
回答的节奏是“结论先行,一句话解释,再加一个例子”。每个问题控制在三十秒以内,说长了老师反而会往深里追问。
5. 避坑清单:课程设计里最常见的 5 个翻车点
下面的坑按踩中概率排序,前两个几乎每届都有人中招,后三个属于“答辩现场被当场抓住”的高发区。
5.1 内存越界与悬空指针:malloc 和 free 不成对
现象:菜单反复添加、删除后,程序内存占用持续上涨;或者删除节点后再次遍历,打印出乱码数据甚至崩溃。
原因:malloc 了没有 free,是内存泄漏;free 之后前驱节点的 next 还指着这块内存,是悬空指针。链表删除最典型的问题是只改指针不释放:
/* 反例:只改指针,少了 free(cur) */ prev->next = cur->next; /* 正例 */ prev->next = cur->next; free(cur);解决:删除路径上必须 free,free 后立刻让函数返回,不要再用 cur 做任何操作;每写一个 malloc,就检查有没有配对的 free。查内存泄漏用工具:Linux 下跑valgrind --leak-check=full ./main,Windows 下用 VS 的调试器看输出窗口的内存泄漏报告,几秒钟就能定位到哪个文件哪一行漏的。
5.2 文件缓冲区:程序退出前数据丢了一半
现象:添加几条记录,退出程序再打开 data.txt,发现最后几条没写进去;或者程序运行中崩溃,整个文件变成空的。
原因:fprintf、fwrite 都带缓冲区,数据先存在内存里,fclose 或 fflush 时才真正落盘。程序异常退出时缓冲区内容直接丢弃;还有人是 fopen 用了"w"模式,把旧文件清空后以为能追加,结果数据全没了。
解决:用一个统一的 saveToFile() 集中写文件,fclose 之前先 fflush;每次 fopen 后立刻检查返回值:
FILE *fp = fopen("data.txt", "w"); if (fp == NULL) { perror("fopen"); /* 打印具体失败原因 */ exit(1); }追加数据用"a"或"ab+",不要用"w"。读取文件时别盲目信任 fscanf 的格式,每条记录按返回值判断是否读成功,读到非法行就跳过并计数,程序才不会在答辩现场崩掉。
5.3 中英文乱码:编辑器、终端、Word 各说各话
现象:换个编辑器重新编译,警告里出现“multi-character character constant”;终端输出中文变成乱码;报告截图里的中文变成方块或问号。
原因:源文件编码、编译器默认编码、终端代码页三方不一致。老环境按 GBK 读文件,新编辑器默认 UTF-8,混在一起就打架。
解决:新工程第一步就统一编码,VS Code 右下角把文件编码显式设为 UTF-8;如果机房的老编译器只认 GBK,就把程序提示信息统一写成英文,既避开乱码,答辩演示时还显得规范。报告截图前把 IDE 背景调成浅色,字体调大,截图比一堆文字描述更有说服力。
5.4 复杂度分析写错:把数组和链表混为一谈
现象:报告里写“单链表查找时间复杂度 O(1)”“折半查找 O(n)”,老师一眼标红。
原因:顺序表按下标访问是 O(1),链表按值查找是 O(n),两者混淆;折半查找把“一次比较”当成“整个查找”,忽略了它要求数组有序的前提。
解决:给每个核心函数在代码注释里标上一行复杂度,报告里每个操作写平均、最坏、空间三列。写完对照排序和查找的复杂度表自查一遍,快排平均 O(n log n)、最坏 O(n²) 一定要说清楚最坏出现在什么时候。
5.5 代码冻结:报告和代码不一致是失分重灾区
现象:答辩演示时,老师让你操作某个功能,界面和报告截图对不上;或者报告里的核心代码段和现场源码明显不是同一个版本,被质疑真实性。
原因:先截图后改功能,改完代码忘了重新截图;报告里的代码段是早期草稿,甚至是从网上拼的。
解决:把“代码冻结”写进计划表——提交前两天停止加功能,只修 bug;然后全部重新生成截图;报告里的代码段直接从最终源码复制粘贴,不要手敲。三步做完再打印装订,能避免绝大部分报告与代码不一致的问题。
6. 调试三板斧:答辩前让代码经得起拷问
6.1 gdb 快速上手:断点、打印、单步
课程设计阶段最常见的调试方式还是 printf,但碰到指针问题,printf 看不到结构关系,这时候 gdb 比任何打印都直观。最常用的命令就那么几条:
gcc -g -Wall -o main main.c # 必须带 -g,否则没有调试信息 gdb ./main (gdb) break deleteById # 在函数入口下断点 (gdb) run # 运行到断点 (gdb) print id # 查看参数值 (gdb) next # 单步执行,不进入函数 (gdb) print cur->next # 查看链表节点的指针 (gdb) continue # 继续运行到下一个断点-g不加,断点处的行号和变量名全是乱的,等于白调。next和step的区别是前者不进入函数内部,后者会跟进去,排查链表的插入删除逻辑时先用 next 看外层流程,怀疑到具体函数再用 step。用 VS Code 跑 C 也一样,F5 启动调试后在监视窗口添加cur->next,链表结构一清二楚,比在终端里猜变量值省事得多。
6.2 边界用例与答辩自检清单
调试完了不代表万事大吉,答辩前把下面这批边界用例全跑一遍:
- 空链表执行插入、删除、排序
- 链表只有一个节点时删除该节点
- 删除尾节点
- 重复学号插入两次
- 输入负数或超范围成绩
- 1000 条数据排序的性能
- 连续运行两次,第二次能否正确读回第一次保存的数据
这些用例就是在 4.2 测试用例表里的素材,每跑一个记一行,报告直接可以用。临交之前再对照一遍检查清单:源码能从零编译吗、-Wall下有没有警告、每个关键函数的复杂度能口头说出来吗、文件读写跑过两轮吗、报告截图和当前代码一致吗。
我见过不少同学交了之后才发现报告截图是旧版,那种感觉比答辩被问住还难受,因为完全有办法避免。这几年帮人改课程设计,我自己的收尾流程固定成三步:代码冻结、报告重新截图、边界用例全跑一遍。前两步保证报告和现场一致,最后一步防止老师换测试数据时崩给你看。这三步走完,剩下的就看临场发挥了。希望帮到你。
本文还有配套的精品资源,点击获取