数据结构设计实战练习题:从备份系统说起

你在公司赶着上线一个文件同步工具,用户一上传文件,系统就得快速记录、归类、查找。这时候,光会用数组、链表还不够,得知道怎么设计合适的数据结构。这就像整理家里衣柜——衣服堆成山时,随便塞肯定不行,得分季节、类型、使用频率,还得能快速找到。

一个真实的场景:增量备份怎么存?

假设你要实现一个增量备份系统,每次只保存变化的文件块。这些块有唯一的ID,还要支持快速查询是否已存在、合并相邻块、按时间排序回放。如果直接用列表一条条扫,数据一大,查一次就得几秒,谁等得起?

这时候,你会想到哈希表存ID做去重,用跳表或平衡树维护时间顺序。把两个结构联动起来,查得快,插得也稳。这不是理论题,是每天备份系统都在处理的事。

实战练习题1:设计一个带版本的键值存储

要求:支持 set(key, value, timestamp),get(key, timestamp) 能查到该时间前最新的值。比如你备份了代码文件的多个版本,想回退到昨天下午三点的状态。

别急着写HashMap嵌套List。想清楚时间戳是递增的,那每个key对应的value历史可以用数组存,查的时候二分搜索。这样插入O(1),查询O(log n),比遍历快多了。

class VersionedKV {
    Map<String, List<Pair<Integer, String>>> data;
    
    void set(String key, String value, int time) {
        data.computeIfAbsent(key, k -> new ArrayList<>()).add(new Pair<>(time, value));
    }
    
    String get(String key, int time) {
        List<Pair<Integer, String>> history = data.get(key);
        if (history == null) return null;
        // 二分查找最后一个 <= time 的项
        int lo = 0, hi = history.size() - 1;
        while (lo < hi) {
            int mid = (lo + hi + 1) / 2;
            if (history.get(mid).time <= time) lo = mid;
            else hi = mid - 1;
        }
        return history.get(lo).time <= time ? history.get(lo).value : null;
    }
}

实战练习题2:设计一个可撤销的文件操作日志

用户每删一个文件,系统记一笔。要支持undo,也就是回滚上一步。这听起来像栈——后进先出,push操作,pop撤销。

但如果要支持撤销多次,还能redo,就得加个“反向栈”。执行操作时压入主栈,撤销时弹出并压入备用栈。redo就反过来。两个栈来回倒腾,像是在时光机里前进后退。

实战练习题3:高效合并备份区间

你的备份不是连续做的,可能早上8:00-8:15,再8:10-8:20,这两个时间段重叠了。怎么合并成一个8:00-8:20的大区间?

这本质是“合并区间”问题。先把所有区间按起始时间排序,然后逐个合并。当前区间的结束时间大于下一个的开始时间,就更新结束时间;否则断开,新开一段。这种结构在日志压缩、空间回收里特别常见。

数据结构不是背概念,是在具体问题里权衡取舍。内存紧就省空间,查询多就换时间。练这些题,不是为了过面试,而是当你坐在电脑前,脑子里能蹦出合适的模型,而不是只会for循环硬扫。