资讯动态

Java课程设计实战:通讯录模拟与24点扑克牌完整代码拆解

发布时间:2026/10/29 10:12:11 来源:尧图企业网站定制
简介这是一份基于Java实现的数据结构课程设计资源面向高校计算机相关专业学生及需要完成课设的开发者。项目选取手机通讯录模拟与24点扑克牌游戏两个经典场景分别演示链表、动态数组、哈希表、Set去重、递归/回溯算法以及栈等核心结构在实际业务中的落地方式同时涉及增删改查、表达式穷举与结果判定等关键功能设计。压缩包为zip格式共73个文件其中包含53张运行效果或界面截图png、8个工程配置与元数据文件xml、5个源码文件java、5个编译产物class以及模块描述文件iml和说明页html便于对照代码理解执行流程。资源包整体约431KB轻量易用已有649人参与学习下载。通过源码、截图与工程结构的配合读者可快速掌握面向对象建模、集合选型与搜索策略优化等课设核心环节适合作为项目参考或设计思路蓝本。1. 一份能直接写的Java课设通讯录模拟与24点扑克牌的完整拆解数据结构这门课卡住很多人的不是理论而是“老师布置的课设到底怎么写代码”。手机通讯录模拟和24点扑克牌游戏是数据结构课程设计里出现频率最高的两个题恰好覆盖线性表、递归、栈、队列这些核心考点。这套资源不是让你背代码而是把两个题目从数据结构选型到代码实现、再到期末答辩可能被追问的问题全部拆开来讲清楚。适合正在做课设的本科生也适合想拿这两个题目练手、按代码反推数据结构原理的自学者。下面按我的实际开发顺序一套套拆给你看。2. 手机通讯录模拟顺序表、链表与查找排序的代码骨架2.1 先定数据结构通讯录用顺序表还是链表通讯录模拟题目的核心要求是增删改查外加按姓名排序、按电话号码查找。大部分课设文档会建议用线性表但线性表有两种实现方式选错会影响后续写代码的复杂程度。顺序表ArrayList在内存中连续存储按下标访问是O(1)插入和删除需要移动元素。链表LinkedList则相反插入删除快但查找需要遍历。通讯录的特点是“添加联系人”和“删除联系人”频繁且通常不会超过几百条记录。在数据量小的场景下两种结构性能差异可以忽略真正需要权衡的是代码的书写难度和老师答辩时的提问点。我一般建议用顺序表实现理由有三点第一课设代码量少ArrayList的底层逻辑简单不容易写出内存访问越界的bug第二按姓名排序时可以直接调Collections.sort()避免自己实现链表排序的复杂节点交换第三老师追问“为什么用顺序表而不是链表”时你有足够理由——数据量小、查询需求多于插入删除。如果老师的题目明确要求必须用链表那就在节点类里加上prev和next指针多写一个双向链表类否则不要自找麻烦。2.2 联系人对象设计字段别乱加够用就行通讯录的每个条目对应一个联系人对象。很多新手上来就加十几个字段结果排序、查找、文件读写都变得繁琐。课程设计的评分标准不是字段多而是逻辑清晰。我这里给出一个稳妥的字段方案。public class Contact implements Serializable { private String name; // 姓名 private String phone; // 手机号 private String email; // 邮箱 private String groupName; // 分组家人/朋友/同事 public Contact(String name, String phone, String email, String groupName) { this.name name; this.phone phone; this.email email; this.groupName groupName; } // 必须重写toString()方便直接打印联系人信息 Override public String toString() { return 姓名 name | 电话 phone | 邮箱 email | 分组 groupName; } }这个类的设计有几个讲究之处实现Serializable接口是为了后续做文件对象流存储不实现的话写ObjectOutputStream时会直接抛NotSerializableException。toString()必须重写因为无论是显示联系人列表还是调试输出都依赖这个方法。groupName字段不是必须的但加上分组功能后课设的功能列表会多一个“按分组筛选”答辩时更好讲。2.3 通讯录管理器把增删改查封装成类通讯录的核心业务逻辑应该单独放在一个管理类中不要在main方法里堆操作。下面这代码覆盖了最常见的需求添加、删除、修改、按姓名查找、按电话查找、按姓名排序。import java.util.*; public class AddressBook { private ListContact contacts; public AddressBook() { contacts new ArrayList(); } // 添加联系人 public boolean addContact(Contact c) { if (c null || c.getName().isEmpty() || c.getPhone().isEmpty()) { System.out.println(姓名和电话不能为空); return false; } if (findByPhone(c.getPhone()) ! null) { System.out.println(该号码已存在添加失败); return false; } contacts.add(c); return true; } // 按姓名精确查找 public Contact findByName(String name) { for (Contact c : contacts) { if (c.getName().equals(name)) { return c; } } return null; } // 按电话号码查找 public Contact findByPhone(String phone) { for (Contact c : contacts) { if (c.getPhone().equals(phone)) { return c; } } return null; } // 删除联系人按姓名删 public boolean deleteByName(String name) { Contact target findByName(name); if (target null) { System.out.println(未找到该联系人); return false; } contacts.remove(target); return true; } // 按姓名排序用Collections.sort底层是归并排序 public void sortByName() { Collections.sort(contacts, new ComparatorContact() { Override public int compare(Contact o1, Contact o2) { return o1.getName().compareTo(o2.getName()); } }); System.out.println(排序完成); } }添加联系人时做了两件事空值校验和电话查重。很多课设代码只做非空判断忽略电话重复的问题结果用同一个号码添加两次后续查找时返回的对象不确定这是答辩时的减分项。findByPhone和findByName本质都是线性查找时间复杂度O(n)。如果老师要求“查找效率更高”可以扩展为哈希表——按电话号码的末几位建哈希索引但那是加分项先把线性查找写对。2.4 文件持久化对象流与文本文件的选择通讯录程序关闭后数据不能丢必须做持久化。常见的做法有两种对象流序列化和文本文件读写。对象流的代码更简洁但生成的二进制文件不适合老师直接查看文本文件可读性强但需要自己处理字符串解析。我建议两个都写先存成文本文件验证逻辑再改造为对象流。import java.io.*; public class FileUtil { // 用对象流保存联系人列表 public static void saveContacts(ListContact list, String path) { try (ObjectOutputStream oos new ObjectOutputStream( new FileOutputStream(path))) { oos.writeObject(list); System.out.println(保存成功 path); } catch (IOException e) { e.printStackTrace(); } } SuppressWarnings(unchecked) public static ListContact loadContacts(String path) { File f new File(path); if (!f.exists()) { System.out.println(文件不存在返回空列表); return new ArrayList(); } try (ObjectInputStream ois new ObjectInputStream( new FileInputStream(path))) { return (ListContact) ois.readObject(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); return new ArrayList(); } } }try-with-resources写法是Java 7以后的推荐做法相比手动close()更不容易泄露资源。loadContacts中判断文件是否存在非常关键——第一次运行程序时文件还不存在如果不判断直接readObject()会抛出FileNotFoundException程序直接崩溃。反序列化时强制类型转换需要加SuppressWarnings(unchecked)否则编译期会有警告提示虽然不影响运行但被老师看到会扣代码规范的分。2.5 主菜单逻辑while循环加switch是最稳定的组合主程序用死循环加switch做菜单选择这是控制台课设最常规的写法。需要注意的点是Scanner的nextLine()和nextInt()混用时会吞换行符导致后续输入被跳过。常见解决方案是统一用nextLine()读取整行然后手动做类型转换。public static void main(String[] args) { AddressBook book new AddressBook(); ListContact exist FileUtil.loadContacts(contacts.dat); // 将文件中的联系人导入到当前通讯录 for (Contact c : exist) { book.addContact(c); } Scanner sc new Scanner(System.in); while (true) { System.out.println(1.添加联系人 2.删除联系人 3.修改联系人 4.查找联系人 5.显示全部 0.退出); String input sc.nextLine(); int choice; try { choice Integer.parseInt(input); } catch (NumberFormatException e) { System.out.println(请输入数字); continue; } switch (choice) { case 1: System.out.print(输入姓名); String name sc.nextLine(); System.out.print(输入电话); String phone sc.nextLine(); System.out.print(输入邮箱); String email sc.nextLine(); System.out.print(输入分组); String group sc.nextLine(); book.addContact(new Contact(name, phone, email, group)); break; case 2: System.out.print(输入要删除的姓名); book.deleteByName(sc.nextLine()); break; case 0: FileUtil.saveContacts(book.getAllContacts(), contacts.dat); System.out.println(已保存再见); return; default: System.out.println(无效选项); } } }Integer.parseInt(input)处理菜单输入而不是直接用nextInt()是避免换行符问题最稳妥的做法。addContact内部已经有重复号码校验这里不需要重复判断。退出时先保存再return确保数据不丢。如果老师要求每次操作后自动保存可以把saveContacts放到switch的每个case分支里但那样频繁写磁盘性能不必要地下降。3. 24点扑克牌游戏递归回溯与表达式输出的核心实现3.1 理解题目本质为什么说24点考的是递归思想24点游戏的规则是从一副扑克牌中随机抽取4张牌每张牌的点数在1到13之间J/Q/K按11/12/13算用加、减、乘、除四种运算以及括号使计算结果等于24。每张牌必须且只能用一次。从数据结构角度看这个题目有两个考点第一如何遍历所有可能的运算组合——这需要递归回溯第二如何输出带有括号的合法表达式——这是字符串拼接的逻辑。很多同学卡在第二点上递归找出了24但输出的表达式是错的比如“1 2 × 3 4”缺少括号。看问题的本质4个数和3个运算符任意排列组合加上括号优先级总组合数是有限的。暴力枚举的复杂度大约是C(4,1)×C(4×3的排列)在10万次量级完全可以在毫秒级跑完。所以不需要什么启发式算法直接递归遍历所有可能找到第一个解就停止。3.2 递归核心每次合并两个数缩小问题规模传统递归思路是把4个数中任意取两个做加减乘除运算得到一个结果后将结果与剩下两个数组成新的3个数列表递归继续直到只剩一个数。如果这个数接近24则说明找到解。import java.util.*; public class TwentyFourGame { // 保存当前递归过程中的表达式路径 private static final int TARGET 24; private static final double EPSILON 1e-6; public static ListString solve(int[] nums) { ListDouble numbers new ArrayList(); ListString expressions new ArrayList(); for (int n : nums) { numbers.add((double) n); expressions.add(String.valueOf(n)); } boolean found dfs(numbers, expressions); return found ? expressions : null; } private static boolean dfs(ListDouble nums, ListString exprs) { int size nums.size(); if (size 1) { // 浮点数比较不能直接用要允许误差 return Math.abs(nums.get(0) - TARGET) EPSILON; } // 枚举所有两两组合 for (int i 0; i size; i) { for (int j 0; j size; j) { if (i j) continue; // 取出两个数及对应的表达式 double a nums.get(i); double b nums.get(j); String exprA exprs.get(i); String exprB exprs.get(j); ListDouble nextNums new ArrayList(); ListString nextExprs new ArrayList(); for (int k 0; k size; k) { if (k ! i k ! j) { nextNums.add(nums.get(k)); nextExprs.add(exprs.get(k)); } } // 加法ab nextNums.add(a b); nextExprs.add(( exprA exprB )); if (dfs(nextNums, nextExprs)) return true; nextNums.remove(nextNums.size() - 1); nextExprs.remove(nextExprs.size() - 1); // 乘法a*b nextNums.add(a * b); nextExprs.add(( exprA * exprB )); if (dfs(nextNums, nextExprs)) return true; nextNums.remove(nextNums.size() - 1); nextExprs.remove(nextExprs.size() - 1); // 减法a-b注意顺序两种都试 nextNums.add(a - b); nextExprs.add(( exprA - exprB )); if (dfs(nextNums, nextExprs)) return true; nextNums.remove(nextNums.size() - 1); nextExprs.remove(nextExprs.size() - 1); // 除法a/b分母接近0时跳过 if (Math.abs(b) EPSILON) { nextNums.add(a / b); nextExprs.add(( exprA / exprB )); if (dfs(nextNums, nextExprs)) return true; nextNums.remove(nextNums.size() - 1); nextExprs.remove(nextExprs.size() - 1); } } } return false; } }代码中选取两个数时的条件i ! j确保不会把同一个数用两次。EPSILON 1e-6是浮点数比较的容差因为除法运算会产生无限小数比如7 / 3 2.333...直接用判断会导致计算结果是24却判定失败。每次尝试一种运算后要移除刚添加的元素恢复到尝试前的状态这就是“回溯”。a - b和a / b只试一种顺序因为b - a会在后续循环中当i和j互换时被覆盖。3.3 有解判定与无解处理不能只给一种结果24点不是所有牌面都有解。经典的1,1,1,1无论如何都算不出24。程序必须对无解情况给出明确提示而不是死循环或空结果。主调用处需要判断solve方法返回的List是否为null。public static void main(String[] args) { int[] cards drawFourCards(); // 随机抽取4张牌范围1-13 System.out.println(抽到的牌 Arrays.toString(cards)); ListString result TwentyFourGame.solve(cards); if (result ! null) { System.out.println(找到解法 result.get(0)); } else { System.out.println(这组牌无解请重新抽牌); } }这里有一个常见的偷懒写法很多教程把表达式拼接放在print语句里不传入递归。那样虽然能验证有解但无法输出表达式。上面代码把exprs列表作为参数一路传递每一步运算都生成对应的完整表达式最终结果直接可用。注意get(0)取的是最终结果对应的表达式因为当递归结束时exprs列表只剩一个元素。需要控制重复解的话可以在dfs返回true的出口处增加一个HashSet记录表达式去重但课设阶段输出一个解就足够了。3.4 效率与边界递归深度最大是多少这个递归模型的深度固定为3层因为每递归一层减少一个数字。每一层的分支数是P(n,2) × 4n是当前数字个数第一层有4×3×4 48种第二层有3×2×424种第三层有2×1×48种总状态数在万级完全不需要担心性能。如果跑不出来几乎一定是代码死循环而不是计算量问题。真正容易出现的问题在除法分支的边界判断。如果b是0a/b会抛出ArithmeticException。上面的代码用Math.abs(b) EPSILON过滤这里的EPSILON不能太小比如1e-10因为b可能是两个浮点数相除后的结果比如2/30.666...本身就有误差。EPSILON1e-6已经是足够安全的阈值。此外牌面如果是J/Q/K要在读取阶段先映射为11/12/13否则parseInt会直接抛异常。4. 课设避坑从编译报错到答辩提问的五个常见翻车点4.1 中文乱码现象、原因与解决现象控制台输出的中文全部变成“???”或者从文件读回的联系人姓名变成乱码。原因Java源码文件使用了GBK编码保存但编译和运行时的默认字符集是UTF-8导致字符串在内部转换时丢失信息。这是Windows环境下最常见的问题因为Windows记事本默认保存为ANSI编码。解决统一使用UTF-8编码保存源文件并且在编译时显式指定编码格式。在命令行编译时用javac -encoding UTF-8 Main.java。如果使用IDEA在File - Settings - Editor - File Encodings将全局编码和项目编码都设置为UTF-8。4.2 Scanner的nextInt()与nextLine()混用导致输入被跳过现象菜单选择使用nextInt()输入数字然后按回车程序直接跳过后续的姓名输入表现为“还没输入内容就提示错误”。原因nextInt()只读取数字部分不会消费掉数字后面的换行符\n紧接着的nextLine()读取到这个残留的换行符后直接返回空字符串。解决一是统一用nextLine()读取所有输入再用Integer.parseInt()把字符串转成数字二是每次调用nextInt()后额外调用一次nextLine()消费换行符。前者更干净后者容易多发一次无用调用。注意如果用户输入了非数字字符Integer.parseInt()会抛出NumberFormatException需要捕获并提示重新输入。4.3 序列化保存文件后修改了Contact类导致无法读取现象昨天运行程序还能正确保存和读取通讯录今天改了Contact类的某个字段后运行程序加载数据时报InvalidClassException。原因序列化机制会在写入文件时记录一个serialVersionUID它是根据类的字段信息自动计算的。修改类的结构后这个UID变化反序列化时Java检测到版本不匹配拒绝恢复对象。解决在Contact类中显式声明private static final long serialVersionUID 1L;这个值是自己指定的不随字段变化。这样即使以后增加字段旧文件也能反序列化成功新增的字段用默认值填充。这是经验教训我吃过大亏课设做到一半加了分组字段结果所有测试数据全部作废。4.4 24点递归栈溢出和结果错误同时出现现象运行24点求解时某些牌面提示无解但手工验证明明有解或者程序直接报StackOverflowError。原因栈溢出几乎可以确定是递归没有正确的终止条件比如i j的判断缺失导致同一个数被反复使用递归深度无限增加。结果错误的场景通常是因为浮点数直接比较相等导致误判比如20/3*3的结果实际是19.999999而不是20。解决先检查递归终止条件只剩一个数时直接判断i和j必须不相等。再看浮点比较是否用了EPSILON容差。这两个点都用对之后仍然无解大概率是牌面确实无解。可以在网上找一个“24点计算器”交叉验证而不是死磕自己的代码。4.5 答辩时的“口胡翻车”搞不清楚时间复杂度和稳定性现象老师问“你选择的排序方法是什么为什么用这个”学生支支吾吾回答不上来或者把Collections.sort()说成快速排序。原因太多同学会调用Collections.sort但从没看过底层实现。其实Java 8中Collections.sort对List的排序底层使用Arrays.sort对象数组排序使用TimSort一种稳定的归并排序变体时间复杂度最坏O(n log n)空间复杂度O(n)。而ArrayList的内存连续存储决定了随机访问效率高TimSort能充分利用这一点。解决在答辩前把每个方法的时间复杂度、空间复杂度写在一张纸上背下来。例如顺序表的线性查找是O(n)哈希查找是O(1)Collections.sort是O(n log n)。老师提问“为什么通讯录用ArrayList而不是LinkedList”时回答“因为支持快速随机访问且存储连续对缓存友好”远比“大家都用这个”更容易过关。5. 进阶验证给课设加测试用例与答辩演示脚本5.1 用JUnit写几组边界测试课设代码写完不是结束还要向老师证明它是对的。最直接的证明方式是用JUnit写几个测试用例把典型输入和预期输出固化下来运行测试一键通过。老师看到测试代码比看到一堆println更认可你的工程能力。import org.junit.Test; import static org.junit.Assert.*; public class TwentyFourGameTest { Test public void testBasicSolved() { int[] cards {1, 2, 3, 4}; ListString result TwentyFourGame.solve(cards); assertNotNull(有解的情况必须返回表达式, result); } Test public void testNoSolution() { int[] cards {1, 1, 1, 1}; ListString result TwentyFourGame.solve(cards); assertNull(无解的情况必须返回null, result); } Test public void testDuplicateCards() { int[] cards {6, 6, 6, 6}; ListString result TwentyFourGame.solve(cards); assertNotNull(四个6有解, result); assertEquals((6666)24, 24, evaluate(result.get(0)), 1e-6); } }testNoSolution中等号右侧的数字{1,1,1,1}是经过验证的无解组合。这类“反面测试用例”比正面用例更能显示代码的鲁棒性也是评分时老师重点关注的部分。注意assertEquals的断言方法使用了1e-6容差与程序内部的EPSILON保持一致否则测试本身会不稳定。5.2 给通讯录做压力测试的方法通讯录模拟器的数据量通常不大但你可以人为构造一个大规模数据来验证插入和查找的时间增长趋势。具体方法是在main方法里临时生成5000条联系人记录统计排序和查找耗时。// 在临时测试类中运行 long start System.currentTimeMillis(); book.sortByName(); long end System.currentTimeMillis(); System.out.println(5000条记录排序耗时 (end - start) ms);预期结果是排序耗时在几十毫秒量级。如果耗时超过了1秒说明数据结构和算法选择可能有问题比如自己实现了冒泡排序。这时可以主动在答辩时提及“在5000条数据下耗时为xx毫秒满足实际应用需求”这是加分项。注意压力测试的数据是随机生成的不要让用户输入5000次。5.3 答辩演示脚本按这个顺序讲不慌答辩时不要拿着代码从头念而是按“结构 → 功能 → 特点”的顺序演示。我的习惯做法是四步走第一步展示Contact类和AddressBook类的结构指着字段说明“姓名、电话、邮箱、分组对应线性表中的一条记录”。第二步实际运行程序添加三个联系人演示查找功能刻意输入一个重复号码展示添加失败。第三步退出程序重新打开加载文件展示数据确实保存成功。第四步打开24点游戏输入一组已知有解的牌面4,4,10,10展示输出((10*10)-4)/424的结果再输入无解的牌面展示无解提示。这套演示顺序覆盖了文件持久化、逻辑判断、算法输出三个核心评分维度。最后回答老师提问的时候记住一个原则老师问“这个功能还能怎么优化”你要回答的是“未来优化方向”而不是承认当前代码有缺陷。比如老师问你“查找效率还能提升吗”回答“可以引入哈希表把电话号码映射为数组下标将查找复杂度从O(n)降到O(1)”——哪怕你没有实现哈希版本这段话也足以展示你对数据结构的理解深度。对我们这些写过好几个课设的人来说24点递归和通讯录线性表是基础中的基础。它们的好处是逻辑闭环短从输入到输出在一百行内能讲清楚。从那以后我每次写这类课设都会先跑一遍JUnit测试再做十分钟的答辩模拟确认对每个方法的时间复杂度都能脱口而出才敢提交源码包。这些习惯不一定能让你拿到优秀但至少能让答辩现场不冷场——希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑