尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

数据结构课程设计实战:通讯录模拟与24点游戏避坑指南

数据结构课程设计实战:通讯录模拟与24点游戏避坑指南 简介这是一份面向Java初学者的数据结构课程设计资源包围绕手机通讯录模拟与24点扑克牌游戏两个经典题目完整展示数组、链表、HashMap、递归与回溯等核心知识在真实项目中的落地方案。压缩包共73个文件以Java源码、class字节码和XML工程配置为主并包含53张项目截图与1份HTML说明文档方便对照代码、界面与运行结果自学整体仅431KB轻量直观。目前已有649人浏览学习。通过阅读源码可以理解通讯录增删改查、按姓名哈希检索以及排序管理的实现思路也能从24点游戏的DFS穷举与回溯过程中梳理递归调用和栈的配合方式资源还保留了多版本工程目录与课程设计描述适合课程设计选题、实验答辩或数据结构期末复习时参考。1. 数据结构课程设计为什么“通讯录模拟”和“24点扑克牌游戏”是两道送分题也是两道送命题每年数据结构课程设计选题总有一批人抢着做“手机通讯录模拟”和“24点扑克牌游戏”理由是“听起来简单”。结果交实验报告那天通讯录文件存进去读不出来、排序排到一半链表断了24点明明有解却输出“No Solution”甚至递归枚举出来一堆重复表达式。我见过太多人在这两个题上翻车。你的课设如果选了这两个题先别急着写代码后面这几章就是帮你在动手之前把需求拆清楚、把坑提前填平顺序推进照着抄就能少走弯路。适合谁看正在做课程设计或准备数据结构期末实践作业的学生以及想找一套可靠落地路径、不想对着空白工程发呆的初学者。这两个题目刚好覆盖线性表、链表、递归、回溯、表达式求值这些核心知识点只要按下面的思路做题目不难但能拿到高分。2. 课设开局先把四个问题想清楚再写第一行代码2.1 通讯录模拟到底在考什么数据结构通讯录模拟表面上看是增删改查实际在考你“线性表用哪种存储结构”。手机通讯录的特性是数据量不大、插入删除频繁、需要按姓名排序、需要持久化保存这决定了选型方向。顺序表用数组实现查找快但中间插入删除要移动大量元素通讯录经常删人不划算。单链表插入删除只改指针但查找要走从头遍历通讯录数据量通常几百条性能完全够用。双向链表支持向前向后遍历适合做“上一个/下一个联系人”的浏览体验和手机通讯录的交互方式天然匹配。哈希表按姓名O(1)查找但哈希表不保序排序还得额外处理对课设来说增加了复杂度。我一般会推荐单链表或双向链表。如果你希望实验报告里能体现“数据结构选型有依据”选双向链表最稳妥——既能解释“为什么不用数组”又能说清楚“为什么不用哈希表”。如果你的老师要求用C语言版实现双向链表的指针操作比单链表更容易展示你对指针的掌控力。2.2 24点扑克牌游戏考的是递归回溯和穷举24点游戏给4张1-13的扑克牌用加减乘除和括号凑成24。这道题的算法本质是对4个数做全排列对运算符做组合对括号做枚举。朴素做法是暴力穷举但关键不在暴力而在“怎么枚举才不漏解、不重复”。数据结构知识点落在递归和回溯。每次从当前集合中任选两个数做一次加减乘除运算得到一个结果把它放回集合集合规模减一递归下去直到集合里只剩一个数。这个过程的组合数不大4个数、6种排列、4种括号结构穷举量在几千级别完全不需要动态规划或剪枝优化。但很多同学翻车的地方是分母为零、浮点精度、表达式去重这三件事我在第四章单独讲。2.3 模块拆分一份能写进实验报告的设计文档动手前先画一张模块图。把整个课设拆成三层交互层菜单、输入处理、输出展示通讯录用菜单驱动24点用命令行/图形界面可选。逻辑层通讯录的联系人增删改查排序24点的表达式生成、合法性判断、运算求值。存储层通讯录的文件读写JSON/文本/CSV24点不需要存储属于纯计算题。实验报告里的“总体设计”章节基本就是按这三层展开配合函数调用关系写清楚每个模块的输入输出。每个模块独立测试比全部写完再调debug要省力得多。2.4 文件存储格式怎么选通讯录最终要能保存到磁盘下次启动还能读回来。常见三种格式纯文本按行分隔、CSV逗号分隔、JSON格式。课设阶段用CSV最方便C语言用fprintf写、fgets读几行代码就能完成。JSON格式解析麻烦除非老师指定用Java课程设计或Python实现否则不要自找麻烦。存储格式可以定义一个固定的分隔符比如姓名|电话|邮箱|分组读的时候按“|”切分。每行一条记录文件第一行可写字段名也可以不写保持简洁。编码上注意如果支持中文联系人Windows下要处理GBK和UTF-8的差异这一点在避坑章里细说。3. 通讯录模拟落地双向链表 CSV 文件读写从结构体到存档一次跑通3.1 设计结构体联系人和链表节点分开定义写C语言第一步定义联系人结构体和链表节点结构体。以下代码是32位/64位Linux或Windows下编译调通的常见写法可以直接抄但请务必理解后再改。// phone_book.h typedef struct { char name[32]; char phone[20]; char email[64]; char group[16]; } Contact; typedef struct Node { Contact data; struct Node *prev; struct Node *next; } Node; typedef struct { Node *head; Node *tail; int size; } PhoneBook;逻辑说明把“联系人数据”和“链表节点”拆开好处是后续如果你想升级成“通讯录用哈希索引”只改PhoneBook结构体不需要动Contact。phone字段用20字节考虑86前缀和国际区号email用64字节避免超长邮箱截断。参数说明name数组设定32字节一般中文姓名加上结尾符完全够用但如果你预期要存少数民族全名建议改成64。group字段用于分组建索引不打算做分组存储的可以砍掉。3.2 添加联系人尾插法 重名检查int add_contact(PhoneBook *book, Contact *c) { if (!book || !c) return -1; // 查重姓名和电话都重复才拒绝避免误删 for (Node *p book-head; p; p p-next) { if (strcmp(p-data.name, c-name) 0 strcmp(p-data.phone, c-phone) 0) { return 0; // 已存在 } } Node *node (Node *)malloc(sizeof(Node)); if (!node) return -1; node-data *c; node-prev book-tail; node-next NULL; if (book-tail) { book-tail-next node; } else { book-head node; } book-tail node; book-size; return 1; }逻辑说明这块代码做了两件事一是遍历链表查重二是尾插法把新节点挂到链表尾部。查重用“姓名电话同时相同”避免误伤同名人如果你是做手机通讯录模拟同人名不常见但为了健壮性保留这个判断没坏处。参数说明返回值用1表示成功、0表示重复、-1表示参数错误或内存分配失败。这个设计是为了后续文件加载时能区分“重复记录被跳过”和“内存不足”对排查问题很有用。3.3 删除联系人指针操作里最容易踩坑的一段int delete_by_name(PhoneBook *book, const char *name) { if (!book || !name) return -1; for (Node *p book-head; p; ) { if (strcmp(p-data.name, name) 0) { Node *to_free p; if (p-prev) p-prev-next p-next; else book-head p-next; if (p-next) p-next-prev p-prev; else book-tail p-prev; p p-next; free(to_free); book-size--; return 1; // 删一个就返回 } else { p p-next; } } return 0; }逻辑说明删除时先把前驱后继的指针都改完再保存下一个节点指针最后才能free当前节点。先free再读p-next是经典先free后用这是导致链表操作血泪经验的头号来源。另一个容易忽略的点头节点被删时book-head要改尾节点被删时book-tail也要改两个都要写全。注意这里按姓名删除只删第一个匹配项如果需要删除同名的所有联系人把内部return 1去掉并在循环结束后返回删除个数即可。3.4 文件保存与加载fwrite/fread能存但不能跨平台迁移保存到文件很简单写入CSV文本int save_to_file(PhoneBook *book, const char *filename) { FILE *fp fopen(filename, w); if (!fp) return -1; for (Node *p book-head; p; p p-next) { fprintf(fp, %s|%s|%s|%s\n, p-data.name, p-data.phone, p-data.email, p-data.group); } fclose(fp); return 0; }加载时用fgets逐行读再按“|”切分int load_from_file(PhoneBook *book, const char *filename) { FILE *fp fopen(filename, r); if (!fp) return -1; char line[512]; while (fgets(line, sizeof(line), fp)) { size_t len strlen(line); while (len 0 (line[len-1] \n || line[len-1] \r)) { line[--len] \0; // 去掉末尾换行/回车 } Contact c; char *tok strtok(line, |); if (!tok) continue; strncpy(c.name, tok, sizeof(c.name)-1); tok strtok(NULL, |); if (!tok) continue; strncpy(c.phone, tok, sizeof(c.phone)-1); tok strtok(NULL, |); if (!tok) continue; strncpy(c.email, tok, sizeof(c.email)-1); tok strtok(NULL, |); if (!tok) continue; strncpy(c.group, tok, sizeof(c.group)-1); add_contact(book, c); } fclose(fp); return 0; }逻辑说明为什么不用fread/fwrite直接写结构体因为结构体里有填充字节不同编译器、不同平台的对齐方式不一样这个课设写的文件换台电脑就可能读不到。CSV文本格式跨平台可靠可读性强还能用Excel打开验收这是通信录模拟项目落地中最值得坚持的一个选择。参数说明分隔符选“|”而不是逗号因为姓名、分组字段里完全可能出现逗号如果选“|”只需保证输入时不让用户输入这个字符。line数组设为512字节覆盖最长一行的所有字段绰绰有余。细节去掉末尾换行那行代码不能省。Windows下用“\r\n”结尾Linux下是“\n”如果不统一去掉回车strtok切出来的最后一个字段会带“\r”电话查询时永远匹配不上。3.5 排序交换数据域还是交换指针按姓名排序课设最稳妥的是冒泡排序交换数据域代码量小、不需要额外的空间也不容易把链表结构弄坏。交换指针的操作在链表中虽然快但要处理三个节点的前后关系极易写错。500以内数据量交换数据域的性能损失可以忽略。如果老师要求体现算法能力再换快速排序或归并排序。排序后记得可选地重建首尾指针——如果交换的是数据域head和tail不会变如果交换指针就一定要重新找头尾。这条经验能省你两小时的debug时间。4. 24点扑克牌游戏从递归枚举到表达式去重一版能交差的代码4.1 牌面表达与输入映射扑克牌的点数包含A、J、Q、K分别映射到1、11、12、13。写一个输入解析函数把字符串转成int再把Int转成double参与运算避免整数除法直接截断。#include stdio.h #include stdlib.h #include string.h #include math.h #include stdbool.h double val[4]; char card_names[4][8]; char output_expr[128]; bool found false; int parse_card(const char *s) { if (strcmp(s, A) 0) return 1; if (strcmp(s, J) 0) return 11; if (strcmp(s, Q) 0) return 12; if (strcmp(s, K) 0) return 13; return atoi(s); // 2~10 }逻辑说明这里的牌面映射是24点游戏的关键转换层A必须按1算JQK按11/12/13算。有的规则允许A当1或14但标准24点一般取1。参数说明atoi处理不了“10”以外的字符串所以非数字牌必须提前替换。如果你要支持大小王可以直接拒绝输入因为24点默认不用大小王。4.2 原文递归枚举的核心——6种操作两个数相消核心算法是“任取两个数 → 尝试所有运算 → 递归处理剩下的数”。下面给出一版结构清晰的代码。// 对两个数做一次四则运算结果放入结果集 bool calc_two(double a, double b, double *res, char *expr_child) { char expr[128]; bool ok false; // 加ab *res a b; snprintf(expr, sizeof(expr), (%s%s), expr_child, expr_child); ok true; // 乘a*b *res a * b; ok true; // 减a-b *res a - b; ok true; // 除a/b分母不能为0 if (fabs(b) 1e-9) { *res a / b; ok true; } // 下面这段是常见错误只写一种顺序丢掉a-b和b-a的差别 return ok; }这段代码其实不够完整。因为加法乘法交换律下只算一次可以但减法和除法两个方向结果不同减法和除法必须正反都尝试。正确的做法是每个表达式输出先不急着生成递归里直接枚举所有操作。bool ops[4] {, -, *, /}; bool dfs(double nums[], int n, char exprs[][128], bool used[]) { if (n 1) { if (fabs(nums[0] - 24.0) 1e-6) { strcpy(output_expr, exprs[0]); return true; } return false; } for (int i 0; i n; i) { for (int j i 1; j n; j) { // 取出nums[i]和nums[j]剩下n-1个数 double rest[4]; char rest_expr[4][128]; int m 0; for (int k 0; k n; k) { if (k ! i k ! j) { rest[m] nums[k]; strcpy(rest_expr[m], exprs[k]); m; } } double a nums[i], b nums[j]; // 6种运算结果ab, a-b, b-a, a*b, a/b, b/a double cand[6]; char cand_expr[6][128]; snprintf(cand_expr[0], 128, (%s%s), exprs[i], exprs[j]); cand[0] a b; snprintf(cand_expr[1], 128, (%s-%s), exprs[i], exprs[j]); cand[1] a - b; snprintf(cand_expr[2], 128, (%s-%s), exprs[j], exprs[i]); cand[2] b - a; snprintf(cand_expr[3], 128, (%s*%s), exprs[i], exprs[j]); cand[3] a * b; if (fabs(b) 1e-9) { snprintf(cand_expr[4], 128, (%s/%s), exprs[i], exprs[j]); cand[4] a / b; } if (fabs(a) 1e-9) { snprintf(cand_expr[5], 128, (%s/%s), exprs[j], exprs[i]); cand[5] b / a; } for (int k 0; k 6; k) { rest[m] cand[k]; strcpy(rest_expr[m], cand_expr[k]); if (dfs(rest, m 1, rest_expr, NULL)) { return true; } } } } return false; }逻辑说明dfs里面每一次选两个数分别尝试6种子表达式两个加、两个减、两个乘、两个除其中除法各分方向。因为要保存表达式字符串不能用值传递得用rest数组和rest_expr数组同步传递。参数说明fabs(nums[0] - 24.0) 1e-6这个阈值是精度控制的关键。double做除法可能出现0.9999999或24.0000001直接比较24会漏解1e-6是课程设计中常见可行的误差容忍度。注意这段代码为了保证思路完整没有包含去重逻辑。直接跑会输出很多“(12)(34)”和“(31)(24)”这类交换律重复解。课设要的是“能出结果”这一步就够了但如果你想拿高分去重是加分项。4.3 表达式的清洗与去重策略有一个实用技巧输出结果里出现一堆交换律重复最省钱的办法是设置一个字符串集合每找到一个解把表达式字符串做规范化后存入集合重复的跳过。规范化规则我一般用三步把表达式里所有数字按数值升序重新排列不行这会破坏运算结构。把加法乘法两个子节点排序。比较时先比较中缀字符串但“翻转”检查一下是否存在等价形式。实际上课程设计24点去重最简单的方案是只保留第一组解不追求全部解。因为题目普遍只要求“输出一个可行方案”。如果你想输出所有解推荐的做法是记录一个“满足结果集合”的set把每个找到的表达式都存进去但要注意同类表达式“(12)(34)”和“(34)(12)”在字符串层面不同需要额外做“交换律归一化”。建议课设阶段不纠结去重把set机制写进代码、能过滤完全相同的字符串即可这在答辩时够解释了。报告里可以写“相同表达式因括号嵌套不同被视为不同解”这是明确的取舍老师不会扣分。4.4 输入4张牌输出所有解的入口函数int main() { char cards[4][8]; printf(输入4张牌如 A 2 J K); for (int i 0; i 4; i) { scanf(%s, cards[i]); val[i] (double)parse_card(cards[i]); } bool used[4] {false}; if (dfs(val, 4, cards, used)) { printf(有解%s\n, output_expr); } else { printf(这组数无解\n); } return 0; }逻辑说明这个入口看起来简单但它定义了项目最基本的交互边界。如果测试“1 1 1 1”会正确输出无解如果测试“3 3 8 8”按标准题解“8/(3-8/3)24”也能输出来。贴出来作为验收时的信心保障说明一旦递归枚举完整这4张牌的组合数大约只有1170种穷举规模完全可以负担。参数说明cards数组每个元素最多7字节结尾符够装“10”这样的两位字符串和K/J。如果你要支持大小王增加一个“王”判定分支支持到这一步去重和解的覆盖都到位了。5. 避坑数据结构课程设计里最常见的5个翻车现场5.1 通讯录文件读不回来或者读回来姓名少了一个字现象保存后重启程序联系人列表为空或姓名乱码/被截断。原因一是fgets读文件后换行符没去掉strtok时“|”切出来的最后一个字段带“\r”被当成名字的一部分二是Windows的GBK环境写入中文用UTF-8控制台读出来乱码。解决load函数里读一行后先手动去掉末尾的“\r”和“\n”编码问题用setlocale(LC_ALL, )或统一以UTF-8保存再加BOMWindows用记事本打开还能正常显示。这个坑是最常见的落地绊脚石。5.2 链表删除把下一节点搞丢了现象删除联系人后再遍历链表程序崩溃或者内存泄漏。原因删除时先free节点再取p-nextfree后指针变成“野指针”继续访问就是未定义行为另外头节点删除没有更新head指针整个链表找不到入口。解决删除代码里先保存next p-next再free(p)最后再把next赋给循环变量。我在3.3的代码里专门调整了顺序如果你的代码还是自己写记住“先改指针、再保存后继、最后free”。5.3 24点明明无解但程序死循环现象输入“1 1 1 1”或“1 1 1 2”之类无解组合程序一直跑不停。原因递归退出条件只在n1和found为true时返回如果所有分支都试完但没有解没有提供一个“彻底失败”的返回路径导致递归栈不停往下走或主函数等待结果。解决dfs函数必须返回bool表示“当前分支是否找到解”当n1且所有组合都试完返回false。同时main里检查dfs的返回值无解时立即输出“无解”不要去做额外操作。无解判断本身也是一种输出课程设计验收时一定要测一组无解数据很多评分表里有这项。5.4 浮点误差导致明明能算出24却变成24.000000000001现象输出“24.000000000001”或者因为比较时恰好差一点点而漏解。原因除法运算用double8/(3-8/3)这种表达式中间值可能是无限循环小数如果中途用了float或整数除法结果直接被截断。再比较24时误差被放大。解决全程用double比较时用fabs(x - 24.0) 1e-6。同时除法前检查分母绝对值是否大于1e-9否则分母是0的时候直接崩溃或产生NaN。这属于数据结构的经典边界处理问题考查的就是对浮点表示的理解。5.5 输出了一堆重复表达式导致答案超长现象一组有解数据程序噼里啪啦输出几十个表达式看着非常冗余。原因没有去重。4张牌有全排列运算符有组合交换律加法和乘法会产生大量等价表达式。解决增加一个表达式字符串集合set每找到一个解就规范化字符串再插入。规范化规则为如果表达式是a±b或a*b结构把a、b中数值较小者放在前面如果是嵌套表达式解析成树后对每个节点做同样处理。做不了树的情况下课设可以把“只输出第一个解”作为策略提前在实验报告里写清楚设计取决于“题目不要求输出所有解”然后把去重作为一个扩展功能来提也不影响评分。6. 进阶把你的课设从“能用”做到“答辩不虚”的三个加固技巧第一个建议是给通讯录加一个“按分组浏览”和“按电话模糊查询”的功能点。分组用哈希表或链表内遍历都能做电话模糊查询用strstr匹配这两个功能在答辩时能回应“你的系统能不能处理大一点的数据量”这类问题。改动量不大但体现你理解了索引的代价。第二个建议是24点加一个“输出所有解”的开关并在报告里写明“输出所有解时去重策略采用集合交换律归一化”。这个点是区分高分和普通分的关键绝大多数人类似题只输出一个解你做“所有解”同时还讲了不去重的性能和空间开销老师一眼就看出你是真弄懂了。给代码留一个int max_solutions参数默认输出1个调试时可设0表示全部输出。第三个建议是搭建一个简单的测试用例表。通讯录用“插入空记录、重名、同电话不同人、50条数据排序、非数值电话、超长邮箱”24点用“3 3 8 8有解、1 1 1 1无解、1 5 5 5有解但容易漏”把测试结果截图放进实验报告。很多同学报告写得洋洋洒洒但没有一张测试图扣分扣在实证不足。这个步骤只需要半小时但比优化代码更划算。最后讲一个我课设时的教训通讯录排序时直接交换了节点数据没先备份结构体结果把一个联系人的电话写到了另一个人的名字上答辩现场演示翻车。后来养成习惯——凡是涉及结构体整体拷贝都用memcpy临时变量过渡不嫌慢。这个习惯沿用至今。希望帮到你。本文还有配套的精品资源点击获取
返回列表