返回 2026-05-15
⚙️ 工程

微软博客:常数空间线性时间算法删除目录中除最近10个文件外的所有文件A constant-space linear-time algorithm for deleting all but the 10 most recent files in a directory

Raymond Chen在其博客中介绍了一种高效算法,可在常数内存空间和线性时间内删除目录中除最新10个文件外的全部文件。该算法利用文件系统元数据和时间戳排序,无需加载全部文件列表,适用于资源受限环境。实现基于Windows API,代码简洁且性能优异,展示了经典数据结构在系统编程中的巧妙应用。

Raymond Chen

假设你有一个包含大量文件的目录,想要删除除最新创建的10个文件之外的所有文件。能否让 FindFirstFile 按日期顺序枚举这些文件?

不行,无法让 FindFirstFile 按日期顺序枚举文件。FindFirstFile 枚举的文件顺序由文件系统驱动决定,例如 FAT 通常按照目录列表中的顺序枚举,这可能与创建时间一致(如果文件是依次添加的),也可能因重命名或删除操作而变得混乱无序。

由于无法控制文件的枚举顺序,你必须自行完成排序。一种简单的方法是读取所有文件条目,按最后修改时间排序,然后保留最新的十个文件,其余删除。这种方法的空间复杂度为 O(n),运行时间为 O(n log n)。

但我们可以做得更好。

这个问题适合使用优先队列(priority queue)来解决。优先队列是一种支持以下操作的数据结构,其中 n 表示队列中元素的数量:

  • 插入(Add):O(log n)
  • 查找最大值(Find largest):O(1)
  • 移除最大值(Remove largest):O(log n)
  • 上述描述适用于最大优先队列。也存在最小优先队列,其最后两个操作分别是“查找最小值”和“移除最小值”。两种版本本质上是等价的,只需通过反向比较即可相互转换。

    我们的做法是遍历所有文件,并将它们逐个加入一个按修改时间排序的最小优先队列中。该队列始终保留最新的文件。当队列大小超过 10 时,我们删除对应最早(“最小”)条目的文件,并从队列中移除该条目。

    由于优先队列的大小被固定限制在 10 以内,所有操作的时间复杂度均为 O(1),因为 n 被限定为一个预定义的常数。(当然,上限越大,这个常数也越大。)因此整个算法的时间复杂度为 O(n),其中 n 是目录中文件的总数。

    下面是一个解决方案的概要。为了构建最小堆,我们需要反转 dateAscending 的比较逻辑。

    constexpr int files_to_keep = 10;
    
    auto dateAscending = [](const WIN32_FIND_DATA& a, const WIN32_FIND_DATA& b) {
        return CompareFileTime(&a.ftLastWriteTime, &b.ftLastWriteTime) > 0;
        };
    
    std::priority_queue<WIN32_FIND_DATA,
            std::vector<WIN32_FIND_DATA>, decltype(dateAscending)>
            names(dateAscending);
    
    WIN32_FIND_DATA wfd;
    wil::unique_hfind findHandle( FindFirstFileW(L"*.*", &wfd));
    if (findHandle.is_valid())
    {
        do
        {
            if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                // Skip directories
                continue;
            }
    
            names.push(wfd);
            if (names.size() > files_to_keep) {
                DeleteFileW(names.top().cFileName);
                names.pop();
            }
        } while (FindNextFileW(findHandle.get(), &wfd));
    }

    遗憾的是 std::priority_queue 没有提供推导指南来自动推断 Comparator 类型。我们必须显式指定它,并且由于 Comparator 位于 Container 之后,不得不手动写出容器类型,而无法依赖类型推导。

    同样遗憾的是,很难对 priority_queue 内部隐藏的 vector 调用 reserve() 方法。这意味着 names.push() 可能会抛出异常。不过我们使用了 RAII 类型 wil::unique_hfind 来确保不会泄露查找句柄。

    如果你可以使用 std::inplace_vector,就可以完全避免内存分配。

    std::priority_queue<WIN32_FIND_DATA,
            std::inplace_vector<WIN32_FIND_DATA, files_to_keep + 1>,
            decltype(dateAscending)> names(dateAscending);

    (这也更清晰地表明该算法是常量空间复杂度的。)

    这是一个典型的在线算法(online algorithm)示例——这类算法能够逐步处理输入数据,而不需要等待所有输入就绪才开始工作。

    练习:如果任务是删除最旧的 10 个文件呢?

    作者

    雷蒙德参与 Windows 的演进已超过 30 年。2003 年,他创建了一个名为 The Old New Thing 的网站,其受欢迎程度远超他的想象,这一发展至今仍让他感到一阵阵莫名的不安。该网站催生了一本同名书籍《The Old New Thing》(Addison Wesley, 2007)。他偶尔会在 Windows Dev Docs 的 Twitter 账号上出现,讲述一些毫无用处的故事。

    需要完整排版与评论请前往来源站点阅读。