ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据结构上机实验指导书:C语言实现与调试避坑指南

数据结构上机实验指导书:C语言实现与调试避坑指南 简介这份华南农业大学数据结构上机实验指导书面向计算机相关专业学生与数据结构初学者以文档形式系统梳理课程实验内容帮助读者通过动手实践掌握线性表、堆栈、队列、模式匹配、二叉树等核心结构。资源包内含1个doc文件整体约639KB文档按实验单元组织每项实验均包含实验目的、实验内容与实验报告三部分便于对照课堂进度完成上机任务。目录覆盖线性表的数组与链表实现、堆栈的压入弹出、队列的入队出队、暴力算法与KMP模式匹配以及二叉树等模块并引导分析时间与空间复杂度。已有293人学习下载适合作为课程配套练习、期末复习与实验报告撰写的参考帮助读者在编码实践中理解数据组织方式与基本操作为后续算法学习打下基础。1. 一份 176 页的数据结构上机实验指导书到底能帮你省下多少调试时间如果你正在上数据结构这门课或者正在准备考研 408 的机试部分大概率会遇到一个很尴尬的局面课本上的算法伪代码看懂了但真让你用 C 语言把线性表、二叉树、图这些结构从零写出来编译能过、运行不崩、输出格式还对得上完全是另一回事。这份华南农业大学的数据结构上机实验指导书正文 176 页覆盖线性表、堆栈、队列、模式匹配、二叉树、查找、内部排序、图和图的遍历八个实验每个实验都配了实验目的、实验内容和实验报告模板最关键的是——部分题目直接给了完整可编译的代码部分题目留了填空让你补全。它解决的不是“数据结构是什么”的问题而是“这道题我写完了为什么输出不对”的问题。适合正在跟实验课死磕的本科生也适合想拿 C 语言把基础结构手写一遍的考研人。2. 线性表实验顺序存储的初始化、插入、删除到底怎么写才不翻车线性表是整个指导书的第一个实验也是最能暴露 C 语言基本功的地方。指导书里给了顺序表的存储结构定义然后拆成三个题目题目 1 给部分代码让你补全插入、删除、遍历题目 2 让你独立完成两个有序顺序表的合并题目 3 让你原地逆置顺序表。这三个题目的难度是递进的但真正卡住人的往往不是算法思路而是初始化时malloc的返回值判断、插入时realloc的扩容逻辑、以及删除时指针移动的边界。2.1 顺序表初始化为什么你的InitList_Sq总是返回 ERROR指导书里题目 1 给出的InitList_Sq函数体是空的需要你补全。很多同学第一反应是直接写L.elem (ElemType*)malloc(...)然后return OK但这样写有个隐患如果内存分配失败L.elem是 NULL后续所有操作都会崩。指导书在题目 1 的完整代码里给出的写法是这样的int InitList_Sq(SqList L){ L.elem (ElemType*)malloc(LIST_INIT_SIZE * sizeof(ElemType)); if(!L.elem){ printf(NO1); return ERROR; } L.length 0; L.listsize LIST_INIT_SIZE; return OK; }这段代码的逻辑很直白先分配初始容量LIST_INIT_SIZE默认 100个ElemType大小的空间然后判断指针是否为空。如果为空打印错误标记并返回ERROR否则把当前长度置 0把已分配容量记为LIST_INIT_SIZE。参数上LIST_INIT_SIZE和LISTINCREMENT是两个宏前者是初始分配量后者是每次扩容的增量题目 1 里分别定义为 100 和 10。注意listsize记录的是“以sizeof(ElemType)为单位的容量”不是字节数这一点在后面realloc扩容时很关键。2.2 插入与删除指针移动的方向决定了你会不会覆盖数据插入和删除是顺序表实验里最容易写出“看起来对、跑起来错”的两个操作。指导书题目 1 的完整代码里插入函数的写法是int ListInsert_Sq(SqList L, int i, int e){ if(i 1 || i L.length 1) return ERROR; ElemType *newbase, *q, *p; if(L.length L.listsize){ newbase (ElemType*)realloc(L.elem, (L.listsize LISTINCREMENT) * sizeof(ElemType)); if(!newbase) return ERROR; L.elem newbase; L.listsize LISTINCREMENT; } q (L.elem[i-1]); for(p (L.elem[L.length-1]); p q; --p) *(p1) *p; *q e; L.length; return OK; }逻辑分三步先判断插入位置i是否合法合法范围是 1 到L.length1然后检查当前长度是否已经等于容量如果是就用realloc扩容扩容后更新L.elem和L.listsize最后从最后一个元素开始把i-1位置及之后的元素全部后移一位腾出空位写入新元素e长度加一。这里指针移动的方向必须是从后往前如果写反了后面的元素会覆盖前面的数据输出就会变成一堆重复值。删除操作同理只不过方向是从前往后int ListDelete_Sq(SqList L, int i, int e){ ElemType *q, *p; if(i 1 || i L.length) return ERROR; p (L.elem[i-1]); e *p; q L.elem L.length - 1; for(p; p q; p) *(p-1) *p; L.length--; return OK; }删除时先把待删元素的值赋给e带回然后从待删位置的下一个元素开始依次向前覆盖最后长度减一。注意删除的合法范围是 1 到L.length没有1因为不能删除一个不存在的位置。2.3 合并与逆置两个不提供代码的题目怎么下手题目 2 要求把两个非递减有序顺序表 A 和 B 合并成新的非递减有序顺序表 C指导书没有给代码但给了两组自测数据和输出格式。常见做法是先初始化 C然后用两个下标i和j分别遍历 A 和 B每次比较A[i]和B[j]把较小的那个插入 C对应下标后移当其中一个表遍历完后把另一个表剩余元素全部追加到 C。指导书完整代码里MergeList函数用的就是这个思路但注意它调用了GetElem和ListInsert_Sq每次插入都走一遍完整的插入逻辑效率不是最优但胜在代码复用、不容易出错。题目 3 的原地逆置更简单用两个下标i和j分别指向表头和表尾交换L.elem[i]和L.elem[j]然后i、j--直到i j。要求“仍占用原顺序表的空间”意思就是不能新建一个数组只能在原数组上交换。注意题目 2 的完整代码里有一个明显的笔误main函数中读取 B 表元素时循环条件写成了i an应该是i bn。如果你直接抄这段代码输入 B 表元素个数和 A 表不同时B 表会读入错误数量的元素输出自然对不上。这种坑在实验指导书里不算少见抄代码之前先扫一眼循环边界。3. 堆栈、队列与模式匹配从结构定义到 KMP 的落地路径堆栈、队列和模式匹配是三个相对独立的实验但它们在实现套路上有共同点都是先定义存储结构再实现基本操作最后用主函数驱动测试。指导书对这三个实验的题目 1 都给了完整代码题目 2 和题目 3 留白让你独立完成。真正需要花时间的是模式匹配里的 KMP 算法因为暴力匹配谁都能写但 KMP 的next数组求解是很多人第一次接触“用算法算算法”的场景。3.1 堆栈的顺序存储top指针到底指向哪里指导书实验二的堆栈部分顺序栈的结构定义通常是top指针加一个基址。常见做法是top指向栈顶元素的下一个位置这样入栈时先写elem[top]再top出栈时先top--再取elem[top]。也有写法让top直接指向栈顶元素入栈前先top。两种写法都能跑但混用就会出问题。指导书里堆栈实验的完整代码没有在正文中完整展开但从实验报告的模板来看它要求你分析压入、弹出和遍历的时间复杂度说明测试用例会覆盖空栈弹出、满栈压入这些边界。我一般会建议在Push和Pop里各加一个状态判断Push前检查top是否等于stacksizePop前检查top是否等于 0返回ERROR而不是让程序崩掉。3.2 队列的链式与循环实现假溢出是怎么来的队列实验的坑比堆栈更隐蔽。顺序队列如果直接用数组加front和rear两个下标入队时rear出队时front跑几轮之后rear会到达数组末尾但前面明明还有空位——这就是假溢出。指导书实验三的队列部分常见做法是用循环队列把rear和front对MAXQSIZE取模。但循环队列有一个经典问题队空和队满的判断条件都是front rear分不清。解决办法有两种一是牺牲一个存储单元队满条件是(rear1) % MAXQSIZE front二是加一个tag标记最近一次操作是入队还是出队。指导书没有明确指定用哪种但实验报告里要求分析空间复杂度说明它期望你意识到这个取舍。链式队列没有假溢出问题但入队时要判断队列是否为空空队列时front和rear都要指向新结点出队时如果队列变空要把rear置为 NULL否则下次入队会挂在一个已经释放的结点后面。3.3 KMP 模式匹配next数组手算和代码求解的差别模式匹配实验要求实现暴力算法和 KMP 算法。暴力算法两层循环外层遍历主串内层遍历模式串不匹配就回退时间复杂度 O(m*n)。KMP 的核心是next数组它记录的是模式串每个位置之前的最长相等前后缀长度。指导书实验四的完整代码没有在正文中给出但实验报告要求分析两种算法的时间复杂度说明测试数据会包含一些刻意构造的、让暴力算法跑很多次的用例。手算next数组时很多人会混淆“前缀”和“后缀”的边界前缀不能包含最后一个字符后缀不能包含第一个字符。代码求解next数组的常见写法是void GetNext(char *T, int *next){ int i 0, j -1; next[0] -1; while(i strlen(T)){ if(j -1 || T[i] T[j]){ i; j; next[i] j; } else { j next[j]; } } }这段代码里j从 -1 开始next[0]固定为 -1然后i和j同步后移当T[i] T[j]时next[i1] j1否则j回退到next[j]。参数上T是模式串next是输出数组长度至少为strlen(T)1。注意next数组的下标含义next[k]表示模式串前k个字符的最长相等前后缀长度不是第k个字符的某种属性。这个定义如果搞混KMP 主匹配循环里的回退逻辑就会写错。提示指导书实验四的测试样例里模式串可能包含重复字符比如aaaab这种。手算next数组时建议先写出每个位置的前缀集合和后缀集合再取交集的最大长度不要凭感觉填。4. 二叉树、查找与排序递归边界和指针操作是翻车重灾区二叉树、查找和内部排序是指导书里篇幅最大的三个实验从目录看二叉树实验从第 59 页到第 88 页排序实验从第 98 页到第 115 页图实验从第 115 页到第 133 页。这三个实验的共同特点是算法思路本身不复杂但用 C 语言实现时递归的终止条件、指针的赋值顺序、数组下标的边界任何一个写错都会导致运行时错误或者结果不对。4.1 二叉树的遍历为什么你的程序总是报运行时错误二叉树实验通常要求实现先序、中序、后序三种递归遍历以及层序遍历。递归遍历的代码很短但“写出来”和“跑对”之间隔着一堆空指针。常见做法是递归函数的入口先判断当前结点是否为空为空直接返回不为空则按遍历顺序访问结点、递归左子树、递归右子树。层序遍历需要借助队列先把根结点入队然后循环出队、访问、把左右孩子入队直到队列为空。指导书实验五的完整代码没有在正文中展开但从实验报告的要求来看它期望你分析二叉树的各种操作复杂度。这里最容易翻车的地方是用malloc创建结点后忘记初始化左右孩子指针为 NULL导致遍历时访问到野指针。另一个坑是递归函数里把return写在了错误的分支上比如先序递归里先递归左子树再访问根结点输出顺序就变成了中序。4.2 查找折半查找的循环条件和边界查找实验通常包含顺序查找和折半查找。折半查找要求表是有序的核心是low、high、mid三个下标的更新。常见写法是while(low high)mid (low high) / 2如果key elem[mid]则high mid - 1如果key elem[mid]则low mid 1相等则返回mid。循环条件是而不是因为当low high时还有一个元素没比较。如果写成最后一个元素会被漏掉。另外mid的计算如果写成(low high) / 2在low和high都很大时可能溢出更安全的写法是low (high - low) / 2。指导书实验六的查找部分实验报告要求分析时间复杂度和空间复杂度说明测试数据会覆盖查找成功和查找失败两种情况。4.3 内部排序稳定性、时间复杂度和实际跑出来的差别排序实验是指导书里题目最多的部分之一通常要求实现直接插入排序、冒泡排序、简单选择排序、快速排序、堆排序、归并排序等。指导书实验七的排序部分从第 98 页到第 115 页篇幅不小。每种排序的代码都不长但放在一起跑同一组数据时输出顺序可能不一样——因为有些排序是稳定的有些是不稳定的。直接插入、冒泡、归并是稳定的简单选择、快速、堆排序是不稳定的。实验报告里通常会要求你对比不同排序算法在相同数据量下的运行时间这时候要注意数据量太小看不出差别数据量太大又可能超时常见做法是生成 1000 到 10000 个随机数分别跑每种排序并记录时间。快速排序的坑在基准值的选择如果每次都选第一个元素作为基准遇到已经有序的数据会退化成 O(n²)常见优化是随机选基准或者三数取中。注意指导书实验七的标题在目录里写的是“部排序”正文里也是“部排序”这应该是“内部排序”的排版错误。不影响内容理解但检索的时候用“内部排序”更容易找到相关资料。5. 图与图的遍历邻接矩阵和邻接表的选择会影响你后面所有代码图实验是指导书里最后一个实验从第 115 页到第 133 页内容包括图的存储结构、深度优先遍历和广度优先遍历。指导书还附了一个“图算法实验题目”和“团队题目各种排序算法效率分析”说明这个实验有一定的开放性。图实验的第一个决策是选邻接矩阵还是邻接表邻接矩阵实现简单但空间复杂度是 O(n²)适合稠密图邻接表空间复杂度是 O(ne)适合稀疏图。指导书没有强制要求用哪种但后面的遍历代码会依赖这个选择。5.1 邻接矩阵的 DFS 和 BFS访问标记数组不能省邻接矩阵的深度优先遍历通常用一个递归函数实现参数是当前顶点下标和访问标记数组。每次访问一个顶点先把visited[v]置为 1然后遍历矩阵第v行找到所有matrix[v][j] 1且visited[j] 0的顶点递归访问。广度优先遍历需要借助队列先把起始顶点入队并标记然后循环出队、访问、把未访问的邻接点入队并标记。这里最容易犯的错误是忘记在入队时标记visited导致同一个顶点被多次入队输出重复。另一个坑是图的顶点编号从 0 开始还是从 1 开始指导书不同题目可能不一样写代码前先确认输入格式。5.2 邻接表的 DFS 和 BFS边结点的插入顺序会影响遍历序列邻接表的深度优先遍历和广度优先遍历逻辑与邻接矩阵类似但遍历邻接点时不是扫描矩阵的一行而是沿着边链表走。边链表的插入顺序会影响遍历序列如果每次插入新边结点都插在链表头部那么遍历顺序和插入顺序相反如果插在尾部遍历顺序和插入顺序一致。指导书实验八的完整代码没有在正文中展开但实验报告要求分析图的遍历复杂度说明测试数据会包含不同结构的图。常见做法是边结点插入时采用头插法代码简单但遍历序列和输入顺序不一致如果题目要求输出特定顺序就需要改成尾插或者先读入所有边再排序。5.3 课程设计题目图算法和排序效率分析的落地思路指导书最后附了“数据结构课程设计2007 级用仅做参考”包含图算法实验题目和团队题目“各种排序算法效率分析”。图算法题目通常要求实现最小生成树Prim 或 Kruskal或者最短路径Dijkstra 或 Floyd。Prim 算法适合稠密图用邻接矩阵实现维护一个lowcost数组记录每个顶点到当前生成树的最小边权Kruskal 算法适合稀疏图用边数组加并查集实现。排序效率分析的团队题目要求对比不同排序算法在不同数据规模、不同初始有序程度下的运行时间常见做法是生成随机数、逆序数、基本有序数三组数据每组跑 1000、5000、10000 三个规模记录每种排序的耗时并画成表格。这个题目没有标准答案但实验报告里要写清楚测试环境、数据生成方式和结论。6. 把这份指导书用透从抄代码到改代码的进阶习惯这份指导书最大的价值不是那些完整代码而是那些留白的题目。题目 1 给了完整代码你抄一遍能跑通但题目 2 和题目 3 没有代码你必须自己写。我的习惯是先把题目 1 的代码敲一遍编译运行确认输出和指导书上的测试样例一致然后把题目 1 的代码关掉凭记忆重新写一遍写不出来的地方再回去看最后做题目 2 和题目 3遇到卡住的地方先自己想 15 分钟想不出来再翻前面的代码找可复用的函数。这个过程比直接抄所有代码慢但做完一个实验之后线性表的基本操作你基本就忘不掉了。验证代码是否正确不能只看一组测试数据。指导书每个题目都给了两组自测数据第一组通常是规整的、边界不明显的第二组往往包含一些特殊情况。比如线性表题目 2 的第二组数据A 表是12 24 45 62 84 96B 表是15 31 75 86合并后12 15 24 31 45 62 75 84 86 96这组数据能检验你的合并逻辑是否在某个表先遍历完时正确处理了剩余元素。我一般会额外构造第三组数据一个表为空或者两个表有相同元素看看输出是否符合预期。如果这三组都过了代码基本就没问题了。实验核心难点常见翻车点验证方法线性表插入删除的指针移动方向扩容后忘记更新 listsize两组自测数据 空表插入堆栈top 指针的指向约定空栈弹出未判断连续压入直到满栈再弹出队列循环队列的队空队满判断假溢出入队出队交替执行多轮模式匹配next 数组的求解前后缀边界混淆用 aaaab 手算对比代码输出二叉树递归终止条件左右孩子指针未初始化先序中序后序输出对比查找折半查找的循环条件low high 写成 low high查找第一个和最后一个元素排序快速排序的基准选择有序数据退化为 O(n²)用逆序数据测试运行时间图邻接表的边结点插入顺序遍历时忘记标记 visited对比 DFS 和 BFS 输出序列从那以后我每次拿到一份实验指导书都强制自己先关掉完整代码把留白的题目从头写一遍写完再对照指导书的代码找差异。差异往往就是我没考虑到的边界情况。希望帮到你。本文还有配套的精品资源点击获取
返回列表