一个常数空间线性时间算法:删除目录中除最近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 账户上讲述一些不传达任何有用信息的故事。
相似文章
15种排序算法在6分钟内 (2013) [视频]
一个2013年的视频,在6分钟内可视化和听觉化展示15种排序算法,包括选择排序、快速排序和猴子排序。
@iluciddreaming: 磁盘清理工具推荐:Czkawka(Rust 写的,GitHub 31.7k Star)。 能找重复文件、相似图片/视频/音乐、空文件夹、大文件、临时文件,还能清 Exif、优化视频。 速度极快,多线程,支持缓存,后续扫描更快。跨平台 Wi…
推荐一个用Rust编写的开源磁盘清理工具Czkawka,功能强大,支持查找重复文件、相似媒体文件、空文件夹等,跨平台且免费。
@HowToPrompt__: 中国教授发现了40年来图的最快最短路径算法。Dijkstra算法已经lite…
一位中国教授发现了一种新的确定性最短路径算法,通过结合Dijkstra和Bellman-Ford算法并引入前沿缩减,突破了40年来的排序障碍,在稀疏图上实现了O(m log^(2/3) n)的时间复杂度。
公共前缀跳过与自适应排序
本文描述了一项已过期的专利,涉及一种新的内存排序算法,该算法具备公共前缀跳过、自适应性和关键子串缓存等特性,已在Oracle 10gR2中实现,并显著提升了性能。
你的代码很快——如果你运气好的话
本文介绍了一种使用排序网络的无分支快速排序实现,并探讨了现代编译器(特别是Clang)如何在代码以恰当风格编写时,利用无分支指令来优化循环。