SWE · OA / Object Design

Meta 云文件压缩:复制、容量收缩与可逆重命名

实现前缀加后缀检索、复制文件、按大小淘汰以及压缩解压,保证失败不改变状态。

题目重点:实现前缀加后缀检索、复制文件、按大小淘汰以及压缩解压,保证失败不改变状态。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练文件全局唯一,copy保留源owner并消耗其额度;查询返回所有同时匹配prefix AND suffix的文件,不是隐含TopN。
  • 训练保留后缀.compressed,正常add禁止此后缀;压缩仅接受偶数大小并减半,解压翻倍。复制需保持压缩标记类型,容量收缩按(-size,name)删除直到used≤capacity。奇数舍入、后缀字面值和恰好容量时边界需要核对。

解题路线

  1. 维护files→(owner,size)、capacity与used,所有操作以同一计数不变量为中心。copy必须先检查源存在、目标未占用、目标标记类型及owner容量,再写入;复制dict不能遗漏容量。
  2. 查询分别检查startswith和endswith,再按大小降序、名字升序。空前/后缀有合法含义。方法名即使含Largest,也不能自动截断为n,合同返回的是全部匹配。
  3. resize先改变容量,再收集该owner现存文件排序删除到used≤cap。相等已经合法;零容量仍可保留零字节文件。基线全扫描比维护一个未做过期校验的heap更可靠。
  4. compress从name改成name+.compressed并把size减半;验证owner、偶数、目标名冲突后才改状态。decompress从末尾切掉一次后缀,不能replace删除文件名中间的同样字符。
  5. 解压需增加原压缩大小这么多额度,检查used+compressedSize≤capacity后执行重命名和更新。失败保留原文件、used与capacity,不先占额后发现名字冲突。

手动走一遍

u容量12,a大小8、b大小4。压缩a成a.compressed(4),used从12降为8;copy该压缩文件为c.compressed(4)后used=12,解压a失败保持不变。缩容到8时三个文件都4,按名字先删a.compressed;不能按旧大小8再删别的。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

class CompressFiles:
    """Training: '.compressed' reserved; even sizes only when compressing.

    Copy preserves owner and consumes quota. Capacity shrink stops at used<=cap.
    All failed operations leave state unchanged. Sizes are integer >=0.
    """
    SUFFIX = '.compressed'
    def __init__(self):
        self.files = {}  # name -> (owner, size)
        self.capacity = {'admin': float('inf')}; self.used = {'admin': 0}

    def add_user(self, user, cap):
        if cap < 0: raise ValueError('negative capacity')
        if user in self.capacity: return False
        self.capacity[user] = cap; self.used[user] = 0; return True

    def add(self, name, size, user='admin'):
        if size < 0: raise ValueError('negative size')
        if name.endswith(self.SUFFIX): return None
        if user not in self.capacity or name in self.files: return None
        if self.used[user]+size > self.capacity[user]: return None
        self.files[name] = (user, size); self.used[user] += size
        return self.capacity[user]-self.used[user]

    def copy(self, src, dst):
        if src not in self.files or dst in self.files: return False
        # Preserve compression marker so reverse rename has unambiguous meaning.
        if src.endswith(self.SUFFIX) != dst.endswith(self.SUFFIX): return False
        user, size = self.files[src]
        if self.used[user]+size > self.capacity[user]: return False
        self.files[dst] = (user, size); self.used[user] += size; return True

    def matches(self, prefix, suffix):
        return sorted(((n, z) for n, (_, z) in self.files.items()
                       if n.startswith(prefix) and n.endswith(suffix)),
                      key=lambda p: (-p[1], p[0]))

    def resize(self, user, cap):
        if cap < 0: raise ValueError('negative capacity')
        if user not in self.capacity or user == 'admin': return None
        self.capacity[user] = cap; removed = 0
        owned = sorted(((n,z) for n,(u,z) in self.files.items() if u==user),
                       key=lambda p: (-p[1], p[0]))
        for name, size in owned:
            if self.used[user] <= cap: break
            del self.files[name]; self.used[user] -= size; removed += 1
        return removed

    def compress(self, user, name):
        if name not in self.files or name.endswith(self.SUFFIX): return None
        owner, size = self.files[name]; dst = name+self.SUFFIX
        if owner!=user or size%2 or dst in self.files: return None
        del self.files[name]; self.files[dst] = (owner, size//2)
        self.used[owner] -= size//2
        return self.capacity[owner]-self.used[owner]

    def decompress(self, user, name):
        if name not in self.files or not name.endswith(self.SUFFIX): return None
        owner, size = self.files[name]; dst=name[:-len(self.SUFFIX)]
        if owner!=user or dst in self.files: return None
        if self.used[owner]+size > self.capacity[owner]: return None
        del self.files[name]; self.files[dst]=(owner,size*2); self.used[owner]+=size
        return self.capacity[owner]-self.used[owner]

复杂度

复制/压缩/解压均摊O(1);匹配排序O(F+M log M),缩容O(F+Fu log Fu)。维护每用户索引能降扫描成本;若用heap需要版本号检查重命名和大小变化留下的旧项。

易错点与追问

容易失分的边界

  • 压缩后只减used却把文件实际size仍写旧值,会立即打破计数。
  • 用整串replace去掉后缀会损坏中间含compressed的文件名。
  • 复制压缩文件成普通名字会让解压语义丢失,接口应明确保留标记。

继续深挖

  • 压缩大小可能奇数时,怎样保存originalSize以保证逆操作?直接floor再×2不可逆。
  • 每用户有百万文件且频繁缩容,怎样用有序索引/带generation的heap减少排序,又保证tie规则?
✓已复制微信号!