题目重点:设计add/remove/move和bookmark,让书的位置变化不破坏身份。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练合同采用唯一bookId,书架顺序可变;move按相邻bookId定位,不承诺按第k项定位O(1)。
- 书签引用bookId;删除一本书时删除所有引用它的书签。书签页码合法性和跨用户权限需补充确认。
解题路线
- bookId→节点哈希定位,双向链表保存顺序;插入到指定节点之前、删除、移到另一节点前均可O(1)改指针。首尾哨兵简化边界。
- bookmarkId→bookmark记录,bookId→bookmarkId集合维护反向引用。增删书签同时更新两者;删书遍历反向集合删除,再摘链表节点。
- 把删书与删书签放一个事务或锁内,避免并发新增悬空引用。数据库可用外键级联,但列表移动的排序键、冲突处理及事务吞吐仍要独立设计。
手动走一遍
书序A,B,C,书签m1→B、m2→B。move B before A后为B,A,C,两个书签仍指向B;remove B后两书签都被清理。如果客户端发move(B,before=B),定义幂等无操作;before不存在则返回错误而非放到不明位置。
规模与取舍
内存版按ID操作期望O(1);删书有b个书签时Ω(b)实际删除成本,不能声称整次O(1)。按rank访问链表O(n),大规模可用带size的平衡树O(logn)。
易错点与追问
容易失分的边界
- 只存书的位置序号会在移动/插入后全部失效。
- 只删链表节点不清哈希与反向引用,会出现悬空对象或内存泄漏。
继续深挖
- 海量用户同一书的书签是否真应删?个人书架副本与全局书对象生命周期怎么区分?
- 撤销删除/历史读取怎样保留tombstone而不恢复过期引用?
