【问题标题】:Asymptotic complexity of std::remove_ifstd::remove_if 的渐近复杂度
【发布时间】:2023-03-26 13:10:01
【问题描述】:

我正在研究一种数据结构的擦除方法,该方法具有硬编码的最大元素数 N,它依赖于 std::array 来避免堆内存。虽然std::array 只包含 N 个元素,但其中只有一些 M 是“相关”元素,其中 M 小于或等于 N。例如,如果 N 为 10,则数组如下所示:

std::array<int, N> elements = { 0, 1, 2, -1, 4, -1, 6, -1, -1, 9 };

...如果 M 为 7,则只有前 7 个元素是“相关的”,而其他元素被视为垃圾(结尾 { -1, -1, -9 } 是垃圾)。我在这里使用int 作为SO 示例,但实际程序存储实现operator== 的对象。下面是一个删除所有 -1 并更新 M 的工作示例:

#include <algorithm>
#include <array>
#include <iostream>

constexpr unsigned N = 10;
unsigned           M = 7;
std::array<int, N> elements = { 0, 1, 2, -1, 4, -1, 6, -1, -1, 9 };

int main() {
        for (unsigned i = 0; i < M; ++i)
                std::cout << elements[i] << ' ';
        std::cout << '\n';

        auto newEnd = std::remove_if(
                std::begin(elements), std::begin(elements) + M,
                [](const auto& element) {
                        return -1 == element;
                }
        );

        unsigned numDeleted = M - std::distance(std::begin(elements), newEnd);
        M -= numDeleted;
        std::cout << "Num deleted: " << numDeleted << '\n';

        for (unsigned i = 0; i < M; ++i)
                std::cout << elements[i] << ' ';
        std::cout << '\n';

        return 0;
}

我的问题是std::remove_if 的渐近复杂度是多少?我想在std::remove_ifstd::distance 之间,总体上是O(2M) 或O(M),而std::remove_if 是一个更昂贵的操作。但是我不确定std::remove_if 是否为 O(N * M),因为每次删除都会移动元素

编辑:为清楚起见,我知道这应该应用谓词 M 次,但我想知道每次谓词为真时是否应用 N 班次

【问题讨论】:

  • 无论如何std::begin(elements), &amp;elements[M] 是实现特定的行为,因为C++ 标准说std::begin(elements) 将返回elements.begin(),这反过来又返回一个std::array::iterator,它是实现特定类型,而&amp;elements[M] 是一个指针类型
  • @Danh,怎么可能是 O(1)?如果我有一个大小为 100 的数组,90 个元素是“相关的”,并且我认为 90 是 M,那么 std::remove 至少是 O(M)
  • @Danh Complexity 在这里肯定不是O(1),因为它取决于NM,使它们保持不变并不会改变程序复杂性取决于这两个值的事实。
  • @asimes std::begin(elements) + M 应该可以工作并且是标准的。
  • @Danh 如果你有 “正是 std::distance(first, last) 谓词的应用程序。”,那么它肯定不是恒定的,因为它取决于 lastfirst

标签: c++ asymptotic-complexity remove-if


【解决方案1】:

cppreference:

复杂性: 正是std::distance(first, last) 谓词的应用。

移除的元素没有移位操作,因为在调用std::remove_if后它们可能具有未指定的值

【讨论】:

  • 引号指定了谓词的应用次数,而问题具体询问了移位操作的次数。
  • 虽然它在0和M之间迭代,但整体数据结构的长度为N。也许我应该在我的问题中更多地强调它,但我想知道的是N班的可能性(而不是比谓词的 M 应用)
  • @asimes std::remove_if 不关心底层数组,它唯一知道的是std::distance(first, last) == M,所以它保证在O(M) 中运行。如果你有一个带有.size() = M.capacity() = N 的向量,std::remove_if 运行在O(M) 而不是O(N),这里的想法是一样的。
  • @holt 构建一个只知道距离为 m 并以二次时间运行的实现非常容易。
  • @AmiTavory 是的,但是对于M 而不是N,它仍然会以二次时间运行,无论复杂程度如何,唯一的参数都是M
【解决方案2】:

编辑

回想起来,这个答案解决了一个比所问问题更复杂的问题 - 如何在线性时间内实现“推回端”功能。关于提出的具体问题 - 与 remove_if 有关 - @millenimumbug 的回答更好地解决了这个问题。


我明白为什么你会认为复杂度会是 Θ(m n),因为每个 m 删除的项目都可能需要移动 Θ(n) 距离。

实际上可以在时间 Θ(n) 和额外的 O(1) 空间(只需几个额外的迭代器)上做到这一点。

考虑下图,它显示了算法可能实现的迭代。

红色项目是一组连续的已识别项目,此时要删除到最后(您只需要两个点即可记录)。绿色项目是现在正在考虑的项目(另一个指针)。

如果要删除绿色项目,则红色组只需包含它就会变大。这在下图中显示,红色组在其中展开。在下一次迭代中,绿色项将是它的右侧。

如果不是,则所有红色组都需要移过它。一些想法可以让你相信这可以在红色组中以线性时间完成(即使迭代器被保证只是前向迭代器)。

为什么复杂度是线性的?因为你可以想象这相当于绿色元素相对于左组向左移动。原理类似于摊销分析。

下图显示了第二种情况。在下一次迭代中,绿色元素(正在考虑)将再次位于红色组的右侧。

【讨论】:

  • 假设有不止一个红色组,或者真的很讨厌,其他所有元素都是红色的。我很难看到如何避免所有元素向右移动
  • @asimes 红色组仅显示本次迭代中已识别要删除的元素。很有可能在绿点的右侧,交替的元素将被删除或不被删除。从概念上讲,每个要移除的部分都会长出红色部分。每一个在概念上不会被移除的元素都会被移到它的左边(因为这些是前向迭代器,所以红色实际上会移过它,但这样更容易看到复杂性)。
  • 我认为那时可能需要 O(M) 空间,但这是一个很好的答案。我现在可以看到这是如何在 O(M) 时间内实现的
  • 红色组的大小无关紧要,因为我不需要这些元素具有特定值(IOW 我可以覆盖它们) - 请参阅std::remove_if 后置条件。所以比你建议的实现更简单。
  • @asimes 只需查看en.cppreference.com 上的“可能实现”即可获得O(M) 实现。标准保证 O(M) 谓词的应用程序,但不保证移位次数,因为这些移位是移动,这意味着它们不应该很昂贵,但在所有情况下,我怀疑任何编译器都会给你除 O(M) 以外的任何东西整个std::remove_if.
猜你喜欢
  • 2013-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-07
  • 1970-01-01
  • 2010-10-25
  • 1970-01-01
相关资源
最近更新 更多