一个常数空间线性时间算法:删除目录中除最近10个文件外的所有文件
摘要
本文介绍了一种使用最小优先队列的常数空间线性时间算法,用于删除目录中除最近10个文件外的所有文件,避免了排序所有条目所需的O(n)空间。
<p>假设你有一个包含许多文件的目录,想要删除除最近10个文件外的所有文件。有没有办法告诉<code>FindFirstFile</code>按日期顺序枚举文件?</p>
<p>没有,无法让<code>FindFirstFile</code>按日期顺序枚举文件。<code>FindFirstFile</code>枚举的文件按照文件系统驱动想要的顺序产生。例如,FAT通常按照文件在目录列表中出现的顺序枚举,如果文件是顺序添加的,则可能按创建顺序,如果存在重命名或删除操作,则可能是混乱的顺序。</p>
<p>既然无法控制文件的枚举顺序,你必须自己进行排序。最简单的解决方案是读取所有条目,按最后修改日期排序,然后删除除最后十个外的所有文件。这需要<var>O</var>(<var>n</var>)的空间和<var>O</var>(<var>n</var> log <var>n</var>)的运行时间。</p>
<p>但你可以做得更好。</p>
<p>这项任务需要一个优先队列。优先队列是一种支持以下操作的数据结构,其中<var>n</var>是优先队列中的项数。</p>
<ul>
<li>添加排序: <var>O</var>(log <var>n</var>)</li>
<li>查找最大: <var>O</var>(1)</li>
<li>移除最大: <var>O</var>(log <var>n</var>)</li>
</ul>
<p>以上描述是针对最大优先队列的。还有最小优先队列,其最后两个操作是“查找最小”和“移除最小”。两种形式等价,因为你可以使用反序比较来切换。</p>
<p>我们可以枚举所有文件,按修改日期逐个添加到最小优先队列中。优先队列保存最新的项。如果优先队列大小超过10,则删除优先队列中“最小”(最早)条目对应的文件,并从优先队列中移除该条目。</p>
<p>由于优先队列大小有固定上限,所有操作都在<var>O</var>(1)时间内运行,因为<var>n</var>的值受预定常数限制。(当然,上限越大,<var>O</var>(1)中的常数也越大。)整体算法运行时间为<var>O</var>(<var>n</var>),其中<var>n</var>是目录中的文件数。</p>
<p>以下是解决方案的草图。要获得最小优先堆,我们需要反转<code>dateAscending</code>中的比较方向。</p>
<pre>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) {
// 跳过目录
continue;
}
names.push(wfd);
if (names.size() > files_to_keep) {
DeleteFileW(names.top().cFileName);
names.pop();
}
} while (FindNextFileW(findHandle.get(), &wfd));
}
</pre>
<p>遗憾的是,<code>std::<wbr />priority_<wbr />queue</code>没有推导<code>Comparator</code>类型的推导指引。我们必须显式指定它,并且由于它位于<code>Container</code>之后,我们必须手动写出容器类型,而不是允许推导。</p>
<p>同样遗憾的是,很难在隐藏在<code>priority_<wbr />queue</code>内部的vector上调用<code>reserve()</code>。这意味着<code>names.push()</code>可能抛出异常。至少我们使用了RAII类型(<code>wil::<wbr />unique_<wbr />hfind</code>)来确保查找句柄不会泄漏。</p>
<p>如果你可以使用<code>std::<wbr />inplace_<wbr />vector</code>,则可以使用</p>
<pre>std::priority_queue<WIN32_FIND_DATA,
std::inplace_vector<WIN32_FIND_DATA, files_to_keep + 1>,
decltype(dateAscending)> names(dateAscending);
</pre>
<p>来完全避免内存分配。(这也更清楚地表明算法是常数空间的。)</p>
<p>这是所谓<a href="https://en.wikipedia.org/wiki/Online_algorithm">在线算法</a>的一个例子,该算法增量式地完成工作,而不是等待所有输入才开始。</p>
<p><b>练习</b>:如果任务是删除最旧的10个文件呢?</p>
<p>本文《<a href="https://devblogs.microsoft.com/oldnewthing/20260514-00/?p=112322">一个常数空间线性时间算法:删除目录中除最近10个文件外的所有文件</a>》首发于<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>。</p>
查看缓存全文
缓存时间: 2026/05/16 03:31
# 常空间线性时间算法:删除目录中除最近10个文件外的所有文件 - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260514-00?p=112322
假设你有一个装满文件的目录,想要删除除最近10个文件外的所有文件。有没有办法让 `FindFirstFile` 按日期顺序枚举文件?不,没有办法让 `FindFirstFile` 按日期顺序枚举文件。`FindFirstFile` 枚举的文件是按照文件系统驱动程序想要的任何顺序生成的。例如,FAT 通常按文件在目录列表中的出现顺序枚举,如果文件是按顺序添加的,那可能是创建顺序;如果混杂了重命名或删除操作,则可能是某种混杂顺序。由于你无法控制文件枚举的顺序,你必须自己进行排序。
最简单的解决方案是读取所有条目,按最后修改日期排序,然后删除除最后十个之外的所有文件。这是 O(n) 空间和 O(n log n) 运行时间。但你可以做得更好。
这个任务需要一个优先队列。优先队列是一种支持以下操作的数据结构,其中 n 是优先队列中的项目数。
- 添加排序:O(log n)
- 找到最大:O(1)
- 移除最大:O(log n)
以上描述是针对最大优先队列的。还有一个最小优先队列,后两个操作是“找到最小”和“移除最小”。这两个版本是等价的,因为你可以使用反向比较来从一种切换到另一种。
我们可以做的是枚举所有文件,并将它们逐个添加到按修改日期排序的最小优先队列中。优先队列保存最新的文件。如果优先队列大小超过10,则删除优先队列中“最小”(最早)条目对应的文件,并从优先队列中移除该条目。由于优先队列大小有固定上限,所有操作都在 O(1) 时间内运行,因为 n 被预定常数限制了。(当然,上限越大,O(1) 中的常数也越大。)整体算法因此以 O(n) 时间运行,其中 n 是目录中的文件数。
以下是一个解决方案的草图。为了获得最小优先堆,我们必须反转 `dateAscending` 中的比较含义。
```cpp
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) {
// 跳过目录
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` 的推导指南。我们必须显式指定它,而且因为它位于 `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, 11>, decltype(dateAscending)> names(dateAscending);` 来完全避免内存分配。(这也使算法是常空间这一点更清晰。)
这是所谓的**在线算法**(https://en.wikipedia.org/wiki/Online_algorithm)的一个例子,这种算法逐步完成工作,而不是要求所有输入后才能开始工作。
**练习**:如果任务是删除10个最旧的文件怎么办?
### 分类 ### 主题
## 作者 Raymond Chen
Raymond 参与 Windows 的发展已有30多年。2003年,他创办了一个名为 The Old New Thing 的网站,该网站的受欢迎程度远远超出了他最疯狂的想象,这一发展至今仍让他感到不安。该网站催生了一本书,巧合的是书名也叫 *The Old New Thing*(Addison Wesley 2007)。他偶尔会在 Windows Dev Docs Twitter 账户上讲述一些不传达任何有用信息的故事。
相似文章
我们通过删除文件系统使其速度提升了47倍
microsandbox将其缓慢的用户空间FUSE文件系统替换为内核挂载的EROFS磁盘映像,在文件系统操作上实现了几何平均47倍的速度提升,并消除了虚拟机/主机往返瓶颈。
旋转再探:另一种单向算法
Raymond Chen重新审视了一种用于交换相邻内存块的单向旋转算法,解释了其递归方法和性能特性。
dmtrKovalenko/fff
fff 是一个快速、抗拼写错误的文件搜索工具包,采用 frecency 排序并配备面向 AI 代理的 MCP 服务器,提供高效的、具备 Git 感知的文件与内容搜索功能。
无分支快速排序:性能超越 std::sort 和 pdqsort,提供 C 和 C++ API
一种新的无分支快速排序实现(blqsort)借助排序网络技术,在 Apple M1 和 AMD Ryzen 系统上的性能超越了 std::sort 和 pdqsort,以单头文件形式提供 C 和 C++ 库。其性能提升得益于无分支分区、中位数之中位数枢轴选择以及针对小数组的自定义排序网络。
@gkxspace: 每次发现 mac 存储快满的时候,都会用 Mole + codex 来一次清理,这次又清理了 100多G。 1、先用Mlole一行命令:mo clean https://github.com/tw93/Mole 2、接着再让 Codex …
分享使用开源工具 Mole 配合 Codex 深度清理 Mac 存储的经验,Mole 是一个集清理、卸载、监控于一体的命令行工具,本次清理释放了 100 多 GB 空间。