307
社区成员
发帖
与我相关
我的任务
分享本单元主要考察根据 JML 给出的规格编写 Java 代码的能力和编写JUnit的能力。
模拟网络社区的用户关注关系网络及视频互动,维护关注关系的整个图。
add_user id(int) age(int) name(String)
upload_video uploaderId(int) videoId(int)
follow_user id1(int) id2(int)
unfollow_user id1(int) id2(int)
watch_video userId(int) videoId(int)
query_received_unwatched_videos userId(int)
query_up_followers_age_ratio upId(int)
query_mutual_following_sum
query_shortest_path id1(int) id2(int)
升级在线视频平台模拟系统,新增硬币经济体系、互动(点赞/投币/转发/评论) 以及 粉丝勋章 功能。
add_user_coins userId(int) coins(int)
upload_video uploaderId(int) videoId(int) type(String)
like_video userId(int) videoId(int)
coin_video userId(int) videoId(int) amount(int)
forward_video userId(int) videoId(int) followerId(int)
send_comment userId(int) videoId(int) commentId(int) comment(String)
clean_spam_comments videoId(int) keyword(String)
query_best_contributor upId(int)
query_most_popular_video type(String)
purchase_medal userId(int) videoId(int) amount(int)
queryLongestDecSeq
进一步升级在线视频平台模拟系统,新增智能推荐系统。
recommend_video user_id(int)
recommend_Nth_up user_id(int) rank(int)
query_most_influential_up type(String)
query_user_profile user_id(int)
queryGlobalBestContributor
本单元是依据规格来实现相关代码,规格只是描述该方法所需要满足的限制和达到的效果,并未明确指出未达到相应的效果所实现的相关细节。如果一味按照规格表层的描述来翻译的话,很可能在一层层的循环中超时。
选取恰当的容器就十分重要了。对于 Unit3 而言含 id 对象使用 HashMap<Integer, Object> 较为合适,在查找上就只有O(1)的时间复杂度了。对于 receivedVideo 成员,有从头部插入,删除特定成员,询问前五个内容等操作,由于需要高效函数和维护其内部顺序,理论上使用 LinkedList 较为合适。
本次作业中,所有核心对象(用户、视频)均具有唯一 ID,因此使用HashMap<Integer, Object> 进行存储是最直接且高效的选择:
Network 中维护 users 和 videos**:分别用 HashMap<Integer, User> 和 HashMap<Integer, Video>,实现 O(1) 的查找、添加、删除。User 内部容器:following、followers、watchedVideos、likedVideos 等均以 HashMap<Integer, User/Video> 存储,保证快速判断是否存在及访问。receivedVideos 需要支持头部插入、删除指定元素、查询前 5 个。理论上 LinkedList<Video> 可直接满足操作,但 LinkedList 底层为双向链表,节点在堆中离散分布,CPU 缓存不友好,遍历和删除的常数较大。更优的实现是:自建 Node 类(包含 Video 对象、前驱后继指针),再用 HashMap<Integer, Node> 维护 ID 到节点的映射,同时维护链表头指针。这样删除任意节点可 O(1) 完成,遍历前 5 个也仅需 O(5) 时间。contributions 记录每个贡献者的投币总数,用 HashMap<Integer, Integer>,并在更新时动态维护 bestContributorId 和 bestContribution。Video 内部容器:comments 使用 LinkedHashMap<Integer, String>,既保证按插入顺序迭代,又能通过 ID 快速判断重复和删除。cleanSpamComments 需要遍历并删除满足条件的评论,使用 Iterator 安全删除。社交网络本质是一个有向图(关注关系):
User 是图中的一个节点,出边为 following(关注的人),入边为 followers(粉丝)。followUser 和 unfollowUser 时,同时更新双方的两个 HashMap,复杂度 O(1)。queryShortestPath,直接对 following 做 BFS,时间复杂度 O(N + E),其中 N 为用户数,E 为关注边数。queryLongestDecSeq,需要沿着 following 方向找到最长的年龄严格递减链。通过记忆化 DFS 实现,每个节点只计算一次,总复杂度 O(N + E)。大部分方法(如 addUser、uploadVideo、watchVideo)本身为 O(1) 或 O(k)(k 为粉丝数),无需额外优化。
queryMutualFollowingSum优化策略:在 Network 中维护一个 mutualCount 变量,初始为 0。
followUser(id1, id2) 成功时,如果 id2 已经关注了 id1(即存在互关),则 mutualCount++。unfollowUser(id1, id2) 成功时,如果 id2 仍然关注 id1(取关后互关消失),则 mutualCount--。这样查询方法直接返回 mutualCount,O(1) 时间,避免每次 O(N²) 的全量统计。
queryBestContributor优化策略:在 User 中维护 bestContributorId 和 bestContribution。
beCoined(sponsorId, amount) 被调用时:contributions 映射。newValue。newValue > bestContribution 或 (newValue == bestContribution && sponsorId < bestContributorId),则更新最佳贡献者信息。bestContributorId,O(1) 时间,避免每次遍历所有贡献者。cleanSpamComments优化策略:由于评论长度 ≤ 50,关键词长度 ≤ 8,总评论数可能较多(一个视频可有数千条评论),直接对每条评论调用 String.contains 或自己实现 KMP 均可。但需要注意:
Iterator 边遍历边删除,避免 ConcurrentModificationException。使用 KMP 统计出现次数,复杂度 O(总字符数),在数据范围内完全可行。recommendVideo 与 recommendNthUprecommendVideo**:需要遍历所有视频,计算 computeVideoScore(user, video)。由于视频总数 V ≤ 10000,每次推荐 O(V) 可接受。但若该指令被频繁调用,可考虑缓存每个用户的推荐结果(需注意用户观看历史变化时无效缓存)。recommendNthUp**:需要遍历所有用户(N ≤ 10000),对每个未关注的候选 UP 主计算 computeUpScore(需遍历 7 种类型,常数小),然后排序 O(N log N)。整体 O(N log N + 7N) 在 10⁴ 规模下安全。queryLongestDecSeq优化策略:使用记忆化 DFS。
dfs(user) 返回从该用户出发的最长递减链长度。HashMap<Integer, Integer> 缓存结果,避免重复计算。receivedVideos 的删除操作原代码中使用 LinkedList 存储 receivedVideos,删除时调用 removeIf,时间复杂度 O(L)。优化方法:
HashMap<Integer, Node> 维护 ID 到节点的映射。这一优化在收到大量视频(理论上一个用户可能收到所有视频)且频繁观看(即删除)时尤为重要。

在第二次强测时,因为忽略了receivedVideos可能存在来自follower上传的视频和other forward来的视频是同一个,在watchVideo是采用receivedVideos.remove(video.getId());就只会删除第一个ID符合的视频,会遗漏其余同名的视频,所以需要修复为**receivedVideos.removeIf(videoId -> videoId == video.getId());**,至于产生的性能不足,已在上文receivedVideos 的删除操作中讨论。
JML(Java Modeling Language)是一种形式化的规格描述语言,它用逻辑断言来精确描述 Java 类/方法的行为契约。
逐条翻译 JML 为代码:
@requires → 前置条件判断,抛出对应异常。
@assignable → 明确哪些对象/属性可修改,避免副作用污染无关容器。
@ensures → 用代码实现后置状态。复杂的 \old 表达式需要提前保存旧值。
@signals → 异常类型与触发条件。
设计内部容器与辅助变量:JML 中的 instance model(如 following、contributions)是抽象模型,需要选择合适的数据结构实现并保证不变量(如 contributors.length == contributions.length)
课程要求为特定方法(如 queryMutualFollowingSum、cleanSpamComments、recommendNthUp)编写 JUnit 测试,主要的思考方向有
strictEquals 方法),调用后对比未被 assignable 列出的属性是否真的没有变化。assertThrows 验证特定异常被抛出,并检查异常消息或类型。equals 通常需要比较对象内容,而不仅仅是 id。课程组提供的 strictEquals 方法就是用来做深比较的。测试时不要直接用 == 比较 User 或 Video 对象。receivedVideos 的迭代顺序时,测试不应依赖顺序。但某些方法(如 queryReceivedUnwatchedVideos)明确要求“最新的在前”,此时需要验证顺序。queryUpFollowersAgeRatio 返回 double[],比较时应使用 assertEquals(expected, actual, 1e-6)。在 Unit 3 的开发中,我尝试使用大模型辅助理解规格和生成基础测试。
你是否发现了自己/别人的JML的bug? 在传递过程中,需求,边界是否发生了变化? 今后多人组队编程时,你认为怎么做才能统一所有人对任务需求和实现方法的理解?采用什么措施(或者指定规则)可以减少组内成员间的信息差?
在自然语言和JML语言交替传递的过程中,我们组出现了无中生有、逻辑充要性问题等问题,具体体现如下:
@requires 是否完整?@signals 是否互斥且覆盖所有异常路径?\forall、\exists、\num_of)是否正确?UserIdNotFoundException 优先于 SelfSubscriptionException)。\old 的使用规则:约定在非 pure 方法中,\old 只能用于基本类型或不可变对象,避免深层拷贝带来的性能与歧义。